From ee380ae30610343a2da887a491bbf8ee1d9a2d0d Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Tue, 10 Sep 2024 21:25:26 +0100 Subject: [PATCH 01/85] [5.x] Prevent saving value of `parent` field to entry data (#10725) --- src/Http/Controllers/CP/Collections/EntriesController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Http/Controllers/CP/Collections/EntriesController.php b/src/Http/Controllers/CP/Collections/EntriesController.php index e23d0b8038..0b412664d1 100644 --- a/src/Http/Controllers/CP/Collections/EntriesController.php +++ b/src/Http/Controllers/CP/Collections/EntriesController.php @@ -248,7 +248,7 @@ public function update(Request $request, $collection, $entry) ->save(); }); - $values->forget('parent'); + $entry->remove('parent'); } } From 5ca20e2443462fbc937e39a8c634fe2fc3276ed9 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Wed, 11 Sep 2024 16:11:08 +0100 Subject: [PATCH 02/85] [5.x] When augmenting terms, `entries_count` should only consider published entries (#10727) Co-authored-by: duncanmcclean Co-authored-by: Jason Varga --- src/Stache/Repositories/TermRepository.php | 11 ++++++++++- src/Taxonomies/AugmentedTerm.php | 14 ++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/Stache/Repositories/TermRepository.php b/src/Stache/Repositories/TermRepository.php index 2c0ec7e227..be09c040ce 100644 --- a/src/Stache/Repositories/TermRepository.php +++ b/src/Stache/Repositories/TermRepository.php @@ -7,6 +7,7 @@ use Statamic\Exceptions\TaxonomyNotFoundException; use Statamic\Exceptions\TermNotFoundException; use Statamic\Facades\Collection; +use Statamic\Facades\Entry; use Statamic\Facades\Taxonomy; use Statamic\Stache\Query\TermQueryBuilder; use Statamic\Stache\Stache; @@ -138,7 +139,7 @@ public function make(?string $slug = null): Term return app(Term::class)->slug($slug); } - public function entriesCount(Term $term): int + public function entriesCount(Term $term, ?string $status = null): int { $items = $this->store->store($term->taxonomyHandle()) ->index('associations') @@ -153,6 +154,14 @@ public function entriesCount(Term $term): int $items = $items->where('collection', $collection->handle()); } + if ($status) { + return Entry::query() + ->whereIn('id', $items->pluck('entry')->all()) + ->when($collection, fn ($query) => $query->where('collection', $collection->handle())) + ->whereStatus($status) + ->count(); + } + return $items->count(); } diff --git a/src/Taxonomies/AugmentedTerm.php b/src/Taxonomies/AugmentedTerm.php index 4c9c06e958..dd82c49a27 100644 --- a/src/Taxonomies/AugmentedTerm.php +++ b/src/Taxonomies/AugmentedTerm.php @@ -3,6 +3,8 @@ namespace Statamic\Taxonomies; use Statamic\Data\AbstractAugmented; +use Statamic\Facades\Blink; +use Statamic\Facades\Term; use Statamic\Query\StatusQueryBuilder; use Statamic\Statamic; @@ -80,4 +82,16 @@ public function title() return $this->wrapValue($title, 'title'); } + + public function entriesCount() + { + $key = vsprintf('term-published-entries-count-%s-%s', [ + $this->data->id(), + optional($this->data->collection())->handle(), + ]); + + return Blink::once($key, function () { + return Term::entriesCount($this->data, 'published'); + }); + } } From 121607dff9c22142ad4a6ba3e4c6ac11eaf55546 Mon Sep 17 00:00:00 2001 From: indykoning <15870933+indykoning@users.noreply.github.com> Date: Wed, 11 Sep 2024 18:33:05 +0200 Subject: [PATCH 03/85] [5.x] Improve ImageGenerator Exception handling (#10786) Co-authored-by: Jason Varga --- src/Http/Controllers/CP/Assets/ThumbnailController.php | 4 ++++ src/Imaging/ImageGenerator.php | 3 +-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Http/Controllers/CP/Assets/ThumbnailController.php b/src/Http/Controllers/CP/Assets/ThumbnailController.php index d69c4e0aba..8c0e1c2c94 100644 --- a/src/Http/Controllers/CP/Assets/ThumbnailController.php +++ b/src/Http/Controllers/CP/Assets/ThumbnailController.php @@ -3,7 +3,9 @@ namespace Statamic\Http\Controllers\CP\Assets; use Illuminate\Support\Facades\Cache; +use League\Flysystem\UnableToReadFile; use League\Glide\Server; +use Statamic\Exceptions\NotFoundHttpException; use Statamic\Facades\Asset; use Statamic\Facades\Config; use Statamic\Facades\Image; @@ -110,6 +112,8 @@ private function generate() $this->asset, $preset ? ['p' => $preset] : [] ); + } catch (UnableToReadFile $e) { + throw new NotFoundHttpException; } finally { Cache::forget($this->mutex()); } diff --git a/src/Imaging/ImageGenerator.php b/src/Imaging/ImageGenerator.php index 0970e14043..bd38934577 100644 --- a/src/Imaging/ImageGenerator.php +++ b/src/Imaging/ImageGenerator.php @@ -11,7 +11,6 @@ use League\Glide\Server; use Statamic\Contracts\Assets\Asset; use Statamic\Events\GlideImageGenerated; -use Statamic\Exceptions\NotFoundHttpException; use Statamic\Facades\Asset as Assets; use Statamic\Facades\Config; use Statamic\Facades\File; @@ -258,7 +257,7 @@ private function generate($image) try { $path = $this->server->makeImage($image, $this->params); } catch (GlideFileNotFoundException $e) { - throw new NotFoundHttpException; + throw UnableToReadFile::fromLocation($image); } GlideImageGenerated::dispatch($path, $this->params); From 1eb381d2ef23cd939a95a60375313fbc9d6388fd Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Wed, 11 Sep 2024 18:37:26 +0100 Subject: [PATCH 04/85] [5.x] Fix broken state of "Parent" field when saving Home entry with Revisions (#10726) --- .../Controllers/CP/Collections/ExtractsFromEntryFields.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Http/Controllers/CP/Collections/ExtractsFromEntryFields.php b/src/Http/Controllers/CP/Collections/ExtractsFromEntryFields.php index 523c96a72a..114b7eaecf 100644 --- a/src/Http/Controllers/CP/Collections/ExtractsFromEntryFields.php +++ b/src/Http/Controllers/CP/Collections/ExtractsFromEntryFields.php @@ -19,8 +19,8 @@ protected function extractFromFields($entry, $blueprint) if ($entry->hasStructure()) { $values['parent'] = array_filter([optional($entry->parent())->id()]); - if ($entry->revisionsEnabled() && $entry->has('parent')) { - $values['parent'] = [$entry->get('parent')]; + if ($entry->revisionsEnabled() && $parent = $entry->get('parent')) { + $values['parent'] = [$parent]; } } From 975470ac32dce2ad044765e262d1b503e1395683 Mon Sep 17 00:00:00 2001 From: Maurice Date: Thu, 12 Sep 2024 15:55:57 +0200 Subject: [PATCH 05/85] [5.x] Add submitting state for confirmation modal to better visualise a running action (#10699) Co-authored-by: Jason Varga --- .../assets/Browser/CreateFolder.vue | 7 +++++- .../blueprints/BlueprintResetter.vue | 6 +++++ resources/js/components/data-list/Action.vue | 11 ++++++++-- resources/js/components/data-list/Actions.js | 19 ++++++++++------ .../components/modals/ConfirmationModal.vue | 22 ++++++++++++++----- 5 files changed, 49 insertions(+), 16 deletions(-) diff --git a/resources/js/components/assets/Browser/CreateFolder.vue b/resources/js/components/assets/Browser/CreateFolder.vue index 5a82d54af1..ba04482b8e 100644 --- a/resources/js/components/assets/Browser/CreateFolder.vue +++ b/resources/js/components/assets/Browser/CreateFolder.vue @@ -3,6 +3,7 @@ @@ -42,6 +43,7 @@ export default { buttonText: __('Create'), directory: this.initialDirectory, errors: {}, + submitting: false, } }, @@ -59,11 +61,15 @@ export default { title: this.title }; + this.submitting = true; + this.$axios.post(url, payload).then(response => { this.$toast.success(__('Folder created')); this.$emit('created', response.data); }).catch(e => { this.handleErrors(e); + }).finally(() => { + this.submitting = false; }); }, @@ -81,7 +87,6 @@ export default { }, created() { - this.$keys.bindGlobal('enter', this.submit) this.$keys.bindGlobal('esc', this.cancel) }, diff --git a/resources/js/components/blueprints/BlueprintResetter.vue b/resources/js/components/blueprints/BlueprintResetter.vue index b4e30be1f7..f7d983b0d1 100644 --- a/resources/js/components/blueprints/BlueprintResetter.vue +++ b/resources/js/components/blueprints/BlueprintResetter.vue @@ -5,6 +5,7 @@ :bodyText="modalBody" :buttonText="__('Reset')" :danger="true" + :busy="submitting" @confirm="confirmed" @cancel="cancel" > @@ -36,6 +37,7 @@ export default { return { resetting: false, redirectFromServer: null, + submitting: false, } }, @@ -69,6 +71,8 @@ export default { }, confirmed() { + this.submitting = true; + this.$axios.delete(this.resetUrl) .then(response => { this.redirectFromServer = data_get(response, 'data.redirect'); @@ -76,6 +80,7 @@ export default { }) .catch(() => { this.$toast.error(__('Something went wrong')); + this.submitting = false; }); }, @@ -92,6 +97,7 @@ export default { this.$toast.success(__('Reset')); this.$emit('reset'); + this.submitting = false; }, cancel() { diff --git a/resources/js/components/data-list/Action.vue b/resources/js/components/data-list/Action.vue index af870a14d8..4580fa6833 100644 --- a/resources/js/components/data-list/Action.vue +++ b/resources/js/components/data-list/Action.vue @@ -8,6 +8,7 @@ :title="action.title" :danger="action.dangerous" :buttonText="runButtonText" + :busy="running" @confirm="confirm" @cancel="reset" > @@ -71,6 +72,7 @@ export default { confirming: false, fieldset: {tabs:[{fields:this.action.fields}]}, values: this.action.values, + running: false, } }, @@ -113,6 +115,9 @@ export default { }, methods: { + onDone() { + this.running = false; + }, select() { if (this.action.confirm) { @@ -120,11 +125,13 @@ export default { return; } - this.$emit('selected', this.action, this.values); + this.running = true; + this.$emit('selected', this.action, this.values, this.onDone); }, confirm() { - this.$emit('selected', this.action, this.values); + this.running = true; + this.$emit('selected', this.action, this.values, this.onDone); }, reset() { diff --git a/resources/js/components/data-list/Actions.js b/resources/js/components/data-list/Actions.js index a72480fdbc..1a168f0629 100644 --- a/resources/js/components/data-list/Actions.js +++ b/resources/js/components/data-list/Actions.js @@ -30,8 +30,7 @@ export default { }, methods: { - - run(action, values) { + run(action, values, done) { this.$emit('started'); this.errors = {}; @@ -43,11 +42,17 @@ export default { values }; - this.$axios.post(this.url, payload, { responseType: 'blob' }).then(response => { - response.headers['content-disposition'] - ? this.handleFileDownload(response) // Pass blob response for downloads - : this.handleActionSuccess(response); // Otherwise handle as normal, converting from JSON - }).catch(error => this.handleActionError(error.response)); + this.$axios + .post(this.url, payload, { responseType: 'blob' }) + .then((response) => { + response.headers['content-disposition'] + ? this.handleFileDownload(response) // Pass blob response for downloads + : this.handleActionSuccess(response); // Otherwise handle as normal, converting from JSON + }) + .catch((error) => this.handleActionError(error.response)) + .finally(() => { + if (done) done() + }); }, handleActionSuccess(response) { diff --git a/resources/js/components/modals/ConfirmationModal.vue b/resources/js/components/modals/ConfirmationModal.vue index 5bd032f0fa..a6ff9da357 100644 --- a/resources/js/components/modals/ConfirmationModal.vue +++ b/resources/js/components/modals/ConfirmationModal.vue @@ -4,17 +4,20 @@
{{ __(title) }}
-
+

{{ __('Are you sure?') }}

+
+ +
-
@@ -48,13 +51,16 @@ export default { disabled: { type: Boolean, default: false, + }, + busy: { + type: Boolean, + default: false, } }, data() { return { escBinding: null, - enterBinding: null, } }, @@ -66,10 +72,14 @@ export default { methods: { dismiss() { + if (this.busy) return; + this.$emit('cancel') }, submit() { - this.$emit('confirm') + if (this.busy) return; + + this.$emit('confirm'); } }, @@ -77,7 +87,7 @@ export default { this.escBinding = this.$keys.bind('esc', this.dismiss) }, - beforeDestroy() { + beforeDestroy() { this.escBinding.destroy() }, } From 6e5d6a20572274c1d71227ea7edda1049c8d1396 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Thu, 12 Sep 2024 15:21:08 -0400 Subject: [PATCH 06/85] [5.x] Update CSRF token when session expiry login modal is closed (#10794) --- resources/js/components/SessionExpiry.vue | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/resources/js/components/SessionExpiry.vue b/resources/js/components/SessionExpiry.vue index a305bea8e1..10cb47cb2d 100644 --- a/resources/js/components/SessionExpiry.vue +++ b/resources/js/components/SessionExpiry.vue @@ -121,6 +121,10 @@ export default { } this.lastCount = Vue.moment(); + }, + + isShowingLogin(showing, wasShowing) { + if (showing && !wasShowing) this.updateCsrfToken(); } }, From fcc44619756105aed1e4fd3e7a87f4495cc359ca Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Fri, 13 Sep 2024 16:27:10 +0100 Subject: [PATCH 07/85] [5.x] Reset previous filters when you finish reordering (#10797) Co-authored-by: Jason Varga --- resources/js/components/entries/Listing.vue | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/resources/js/components/entries/Listing.vue b/resources/js/components/entries/Listing.vue index 6b56a0e4ea..8550f58bbf 100644 --- a/resources/js/components/entries/Listing.vue +++ b/resources/js/components/entries/Listing.vue @@ -140,6 +140,7 @@ export default { currentSite: this.site, initialSite: this.site, pushQuery: true, + previousFilters: null, } }, @@ -214,6 +215,7 @@ export default { }, reorder() { + this.previousFilters = this.activeFilters; this.filtersReset(); // When reordering, we *need* a site, since mixing them up would be awkward. @@ -227,6 +229,8 @@ export default { }, cancelReordering() { + this.resetToPreviousFilters(); + this.request(); }, @@ -260,6 +264,14 @@ export default { this.$toast.error(__('Something went wrong')); }); }, + + resetToPreviousFilters() { + this.filtersReset(); + + if (this.previousFilters) this.filtersChanged(this.previousFilters); + + this.previousFilters = null; + } } } From 9722f739ab15e8237b5036879fe74141f943e1c7 Mon Sep 17 00:00:00 2001 From: Michael Aerni Date: Tue, 17 Sep 2024 12:22:00 -0400 Subject: [PATCH 08/85] [5.x] Add entry password protection (#10800) Co-authored-by: Jason Varga --- config/protect.php | 1 + .../Protectors/Password/Controller.php | 27 +++- .../Protect/Protectors/Password/Guard.php | 22 --- .../Protectors/Password/PasswordProtector.php | 50 ++++++- tests/Auth/Protect/PasswordEntryTest.php | 134 +++++++++++++++++- tests/Auth/Protect/PasswordProtectionTest.php | 3 +- 6 files changed, 200 insertions(+), 37 deletions(-) delete mode 100644 src/Auth/Protect/Protectors/Password/Guard.php diff --git a/config/protect.php b/config/protect.php index 383a2d02cf..4e78f78a7f 100644 --- a/config/protect.php +++ b/config/protect.php @@ -44,6 +44,7 @@ 'password' => [ 'driver' => 'password', 'allowed' => ['secret'], + 'field' => null, 'form_url' => null, ], diff --git a/src/Auth/Protect/Protectors/Password/Controller.php b/src/Auth/Protect/Protectors/Password/Controller.php index 072afc7a5a..dcc0d5fdf0 100644 --- a/src/Auth/Protect/Protectors/Password/Controller.php +++ b/src/Auth/Protect/Protectors/Password/Controller.php @@ -2,6 +2,8 @@ namespace Statamic\Auth\Protect\Protectors\Password; +use Statamic\Auth\Protect\ProtectorManager; +use Statamic\Facades\Data; use Statamic\Facades\Site; use Statamic\Http\Controllers\Controller as BaseController; use Statamic\View\View; @@ -31,9 +33,7 @@ public function store() return back()->withErrors(['token' => __('statamic::messages.password_protect_token_invalid')], 'passwordProtect'); } - $guard = new Guard($this->getScheme()); - - if (! $guard->check($this->password)) { + if (! $this->driver()->isValidPassword($this->password)) { return back()->withErrors(['password' => __('statamic::messages.password_protect_incorrect_password')], 'passwordProtect'); } @@ -43,6 +43,13 @@ public function store() ->redirect(); } + private function driver(): PasswordProtector + { + return app(ProtectorManager::class) + ->driver($this->getScheme()) + ->setData(Data::find($this->getReference())); + } + protected function getScheme() { return $this->tokenData['scheme']; @@ -53,12 +60,18 @@ protected function getUrl() return $this->tokenData['url']; } + protected function getReference() + { + return $this->tokenData['reference']; + } + protected function storePassword() { - session()->put( - "statamic:protect:password.passwords.{$this->getScheme()}", - $this->password - ); + $sessionKey = $this->driver()->isValidLocalPassword($this->password) + ? "statamic:protect:password.passwords.ref.{$this->getReference()}" + : "statamic:protect:password.passwords.scheme.{$this->getScheme()}"; + + session()->put($sessionKey, $this->password); return $this; } diff --git a/src/Auth/Protect/Protectors/Password/Guard.php b/src/Auth/Protect/Protectors/Password/Guard.php deleted file mode 100644 index 3ec5f0f168..0000000000 --- a/src/Auth/Protect/Protectors/Password/Guard.php +++ /dev/null @@ -1,22 +0,0 @@ -config = config("statamic.protect.schemes.$scheme"); - } - - public function check($password) - { - $allowed = Arr::get($this->config, 'allowed', []); - - return in_array($password, $allowed); - } -} diff --git a/src/Auth/Protect/Protectors/Password/PasswordProtector.php b/src/Auth/Protect/Protectors/Password/PasswordProtector.php index d7af6a5722..bafab41153 100644 --- a/src/Auth/Protect/Protectors/Password/PasswordProtector.php +++ b/src/Auth/Protect/Protectors/Password/PasswordProtector.php @@ -16,7 +16,7 @@ class PasswordProtector extends Protector */ public function protect() { - if (empty(Arr::get($this->config, 'allowed', []))) { + if (empty($this->schemePasswords()) && ! $this->localPasswords()) { throw new ForbiddenHttpException(); } @@ -33,11 +33,52 @@ public function protect() } } + protected function schemePasswords() + { + return Arr::get($this->config, 'allowed', []); + } + + public function localPasswords() + { + if (! $field = Arr::get($this->config, 'field')) { + return []; + } + + return Arr::wrap($this->data->$field); + } + public function hasEnteredValidPassword() { - return (new Guard($this->scheme))->check( - session("statamic:protect:password.passwords.{$this->scheme}") - ); + if ( + ($password = session("statamic:protect:password.passwords.scheme.{$this->scheme}")) + && $this->isValidSchemePassword($password) + ) { + return true; + } + + if ( + ($password = session("statamic:protect:password.passwords.ref.{$this->data->reference()}")) + && $this->isValidLocalPassword($password) + ) { + return true; + } + + return false; + } + + public function isValidPassword(string $password): bool + { + return $this->isValidSchemePassword($password) || $this->isValidLocalPassword($password); + } + + public function isValidSchemePassword(string $password): bool + { + return in_array($password, $this->schemePasswords()); + } + + public function isValidLocalPassword(string $password): bool + { + return in_array($password, $this->localPasswords()); } protected function isPasswordFormUrl() @@ -64,6 +105,7 @@ protected function generateToken() session()->put("statamic:protect:password.tokens.$token", [ 'scheme' => $this->scheme, 'url' => $this->url, + 'reference' => $this->data->reference(), ]); return $token; diff --git a/tests/Auth/Protect/PasswordEntryTest.php b/tests/Auth/Protect/PasswordEntryTest.php index 07bf031e8e..cd8a89e1dd 100644 --- a/tests/Auth/Protect/PasswordEntryTest.php +++ b/tests/Auth/Protect/PasswordEntryTest.php @@ -2,10 +2,11 @@ namespace Tests\Auth\Protect; +use Facades\Statamic\Auth\Protect\Protectors\Password\Token; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; -use Tests\TestCase; -class PasswordEntryTest extends TestCase +class PasswordEntryTest extends PageProtectionTestCase { #[Test] public function it_returns_back_with_error_if_theres_no_token() @@ -31,6 +32,7 @@ public function it_returns_back_with_error_if_the_wrong_password_is_entered() session()->put('statamic:protect:password.tokens.test-token', [ 'scheme' => 'password-scheme', 'url' => '/target-url', + 'reference' => 'entry::test', ]); $this @@ -47,6 +49,7 @@ public function it_returns_back_with_error_if_the_wrong_password_is_entered() public function it_allows_access_if_allowed_password_was_entered() { $this->withoutExceptionHandling(); + config(['statamic.protect.schemes.password-scheme' => [ 'driver' => 'password', 'form_url' => '/password-entry', @@ -56,6 +59,7 @@ public function it_allows_access_if_allowed_password_was_entered() session()->put('statamic:protect:password.tokens.test-token', [ 'scheme' => 'password-scheme', 'url' => '/target-url', + 'reference' => 'entry::test', ]); $this @@ -64,7 +68,131 @@ public function it_allows_access_if_allowed_password_was_entered() 'password' => 'the-password', ]) ->assertRedirect('http://localhost/target-url') - ->assertSessionHas('statamic:protect:password.passwords.password-scheme', 'the-password') + ->assertSessionHas('statamic:protect:password.passwords.scheme.password-scheme', 'the-password') + ->assertSessionMissing('statamic:protect:password.tokens.test-token'); + } + + #[Test] + #[DataProvider('localPasswordProvider')] + public function it_allows_access_if_local_password_was_entered( + $passwordFieldInContent, + $submittedPassword, + ) { + config(['statamic.protect.schemes.password-scheme' => [ + 'driver' => 'password', + 'allowed' => ['the-scheme-password'], + 'field' => 'password', + ]]); + + Token::shouldReceive('generate')->andReturn('test-token'); + + $this->createPage('test', ['data' => ['protect' => 'password-scheme', 'password' => $passwordFieldInContent]]); + + $this->get('test') + ->assertSessionHas('statamic:protect:password.tokens.test-token', [ + 'scheme' => 'password-scheme', + 'url' => 'http://localhost/test', + 'reference' => 'entry::test', + ]); + + $this + ->post('/!/protect/password', [ + 'token' => 'test-token', + 'password' => $submittedPassword, + ]) + ->assertRedirect('http://localhost/test') + ->assertSessionHas('statamic:protect:password.passwords.ref.entry::test', $submittedPassword) + ->assertSessionMissing('statamic:protect:password.passwords.password-scheme') + ->assertSessionMissing('statamic:protect:password.tokens.test-token'); + } + + public static function localPasswordProvider() + { + return [ + 'string' => [ + 'value' => 'the-local-password', + 'submitted' => 'the-local-password', + ], + 'array with single value' => [ + 'value' => ['the-local-password'], + 'submitted' => 'the-local-password', + ], + 'array with multiple values' => [ + 'value' => ['first-local-password', 'second-local-password'], + 'submitted' => 'second-local-password', + ], + ]; + } + + #[Test] + public function it_prefers_the_local_password_over_the_scheme_password() + { + config(['statamic.protect.schemes.password-scheme' => [ + 'driver' => 'password', + 'allowed' => ['the-scheme-password'], + 'field' => 'password', + ]]); + + Token::shouldReceive('generate')->andReturn('test-token'); + + $this->createPage('test', ['data' => ['protect' => 'password-scheme', 'password' => 'the-scheme-password']]); + + $this->get('test') + ->assertSessionHas('statamic:protect:password.tokens.test-token', [ + 'scheme' => 'password-scheme', + 'url' => 'http://localhost/test', + 'reference' => 'entry::test', + ]); + + $this + ->post('/!/protect/password', [ + 'token' => 'test-token', + 'password' => 'the-scheme-password', + ]) + ->assertRedirect('http://localhost/test') + ->assertSessionHas('statamic:protect:password.passwords.ref.entry::test', 'the-scheme-password') + ->assertSessionMissing('statamic:protect:password.passwords.password-scheme') ->assertSessionMissing('statamic:protect:password.tokens.test-token'); } + + #[Test] + public function it_can_use_the_same_local_password_multiple_times() + { + config(['statamic.protect.schemes.password-scheme' => [ + 'driver' => 'password', + 'allowed' => ['the-scheme-password'], + 'field' => 'password', + ]]); + + Token::shouldReceive('generate')->andReturn('test-token'); + + $this->createPage('test', ['data' => ['protect' => 'password-scheme', 'password' => 'the-local-password']]); + $this->createPage('test-2', ['data' => ['protect' => 'password-scheme', 'password' => 'the-local-password']]); + + $this->get('test') + ->assertRedirect('http://localhost/!/protect/password?token=test-token') + ->assertSessionHas('statamic:protect:password.tokens.test-token', [ + 'scheme' => 'password-scheme', + 'url' => 'http://localhost/test', + 'reference' => 'entry::test', + ]); + + $this + ->post('/!/protect/password', [ + 'token' => 'test-token', + 'password' => 'the-local-password', + ]) + ->assertRedirect('http://localhost/test') + ->assertSessionHas('statamic:protect:password.passwords.ref.entry::test', 'the-local-password'); + + $this->get('test')->assertOk(); + + $this->get('test-2') + ->assertRedirect('http://localhost/!/protect/password?token=test-token') + ->assertSessionHas('statamic:protect:password.tokens.test-token', [ + 'scheme' => 'password-scheme', + 'url' => 'http://localhost/test-2', + 'reference' => 'entry::test-2', + ]); + } } diff --git a/tests/Auth/Protect/PasswordProtectionTest.php b/tests/Auth/Protect/PasswordProtectionTest.php index 4d2258b91b..973896d43c 100644 --- a/tests/Auth/Protect/PasswordProtectionTest.php +++ b/tests/Auth/Protect/PasswordProtectionTest.php @@ -31,6 +31,7 @@ public function redirects_to_password_form_url_and_generates_token() ->assertSessionHas('statamic:protect:password.tokens.test-token', [ 'scheme' => 'password-scheme', 'url' => 'http://localhost/test', + 'reference' => 'entry::test', ]); } @@ -58,7 +59,7 @@ public function allow_access_if_password_has_been_entered_for_that_scheme() 'allowed' => ['the-password'], ]]); - session()->put('statamic:protect:password.passwords.password-scheme', 'the-password'); + session()->put('statamic:protect:password.passwords.scheme.password-scheme', 'the-password'); $this ->requestPageProtectedBy('password-scheme') From 6b9eb8eafdc12d1e6a401394d759e1a22c6ff221 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Wed, 18 Sep 2024 10:38:26 -0400 Subject: [PATCH 09/85] [5.x] Set path on asset folder when moving (#10813) --- src/Assets/AssetFolder.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Assets/AssetFolder.php b/src/Assets/AssetFolder.php index c4a300799b..2e96f11a92 100644 --- a/src/Assets/AssetFolder.php +++ b/src/Assets/AssetFolder.php @@ -133,7 +133,7 @@ public function delete() }); $cache->save(); - AssetFolderDeleted::dispatch($this); + AssetFolderDeleted::dispatch(clone $this); return $this; } @@ -183,6 +183,8 @@ public function move($parent, $name = null) $this->container()->assets($oldPath)->each->move($newPath); $this->delete(); + $this->path($newPath); + return $folder; } From 540bad1a4593c55bbe8a5470d7c08e967aa17b96 Mon Sep 17 00:00:00 2001 From: Erin Dalzell Date: Wed, 18 Sep 2024 08:20:11 -0700 Subject: [PATCH 10/85] [5.x] Hide Visit URL and Live Preview if term has no template (#10789) --- resources/js/components/terms/PublishForm.vue | 5 +++-- resources/views/terms/edit.blade.php | 1 + src/Http/Controllers/CP/Taxonomies/TermsController.php | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/resources/js/components/terms/PublishForm.vue b/resources/js/components/terms/PublishForm.vue index 8bf78f858e..b398ceaef8 100644 --- a/resources/js/components/terms/PublishForm.vue +++ b/resources/js/components/terms/PublishForm.vue @@ -299,6 +299,7 @@ export default { createAnotherUrl: String, listingUrl: String, previewTargets: Array, + hasTemplate: Boolean, }, data() { @@ -361,11 +362,11 @@ export default { }, showLivePreviewButton() { - return !this.isCreating && this.isBase && this.livePreviewUrl; + return !this.isCreating && this.isBase && this.livePreviewUrl && this.showVisitUrlButton; }, showVisitUrlButton() { - return !!this.permalink; + return !!this.permalink && this.hasTemplate; }, isBase() { diff --git a/resources/views/terms/edit.blade.php b/resources/views/terms/edit.blade.php index 6be8d5041d..4df0254892 100644 --- a/resources/views/terms/edit.blade.php +++ b/resources/views/terms/edit.blade.php @@ -38,6 +38,7 @@ :preview-targets="{{ json_encode($previewTargets) }}" :initial-item-actions="{{ json_encode($itemActions) }}" item-action-url="{{ cp_route('taxonomies.terms.actions.run', $taxonomy) }}" + :has-template="{{ $str::bool($hasTemplate) }}" > @endsection diff --git a/src/Http/Controllers/CP/Taxonomies/TermsController.php b/src/Http/Controllers/CP/Taxonomies/TermsController.php index 77446b793d..5054ab8de5 100644 --- a/src/Http/Controllers/CP/Taxonomies/TermsController.php +++ b/src/Http/Controllers/CP/Taxonomies/TermsController.php @@ -144,6 +144,7 @@ public function edit(Request $request, $taxonomy, $term) 'breadcrumbs' => $this->breadcrumbs($taxonomy), 'previewTargets' => $taxonomy->previewTargets()->all(), 'itemActions' => Action::for($term, ['taxonomy' => $taxonomy->handle(), 'view' => 'form']), + 'hasTemplate' => view()->exists($term->template()), ]; if ($request->wantsJson()) { From 5ed7379c1ad3eb342d7f956cb8f92a49d03716e5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Sep 2024 14:13:01 -0400 Subject: [PATCH 11/85] [5.x] Bump vite from 4.5.3 to 4.5.5 (#10810) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index bf0966bd4e..bf0f35503e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -93,7 +93,7 @@ "laravel-vite-plugin": "^0.7.2", "postcss": "^8.4.31", "tailwindcss": "^3.3.0", - "vite": "^4.5.3" + "vite": "^4.5.5" } }, "node_modules/@ampproject/remapping": { @@ -9799,9 +9799,9 @@ } }, "node_modules/vite": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz", - "integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==", + "version": "4.5.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.5.tgz", + "integrity": "sha512-ifW3Lb2sMdX+WU91s3R0FyQlAyLxOzCSCP37ujw0+r5POeHPwe6udWVIElKQq8gk3t7b8rkmvqC6IHBpCff4GQ==", "dev": true, "dependencies": { "esbuild": "^0.18.10", diff --git a/package.json b/package.json index 1d77025f17..b898d87d3f 100644 --- a/package.json +++ b/package.json @@ -98,6 +98,6 @@ "laravel-vite-plugin": "^0.7.2", "postcss": "^8.4.31", "tailwindcss": "^3.3.0", - "vite": "^4.5.3" + "vite": "^4.5.5" } } From 31bcd473edb7ffb9c8711a477eed1a9f5932afdd Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Wed, 18 Sep 2024 15:46:37 -0400 Subject: [PATCH 12/85] [5.x] Add option to exclude flag emojis from countries dictionary (#10817) --- resources/lang/en/messages.php | 1 + src/Dictionaries/Countries.php | 11 ++++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/resources/lang/en/messages.php b/resources/lang/en/messages.php index 395923e103..e4047e131a 100644 --- a/resources/lang/en/messages.php +++ b/resources/lang/en/messages.php @@ -69,6 +69,7 @@ 'collections_sort_direction_instructions' => 'The default sort direction.', 'collections_preview_target_refresh_instructions' => 'Automatically refresh the preview while editing. Disabling this will use postMessage.', 'collections_taxonomies_instructions' => 'Connect entries in this collection to taxonomies. Fields will be automatically added to publish forms.', + 'dictionaries_countries_emojis_instructions' => 'Whether flag emojis should be included in the labels.', 'dictionaries_countries_region_instructions' => 'Optionally filter the countries by region.', 'duplicate_action_warning_localization' => 'This entry is a localization. The origin entry will be duplicated.', 'duplicate_action_warning_localizations' => 'One or more selected entries are localizations. In those cases, the origin entry will be duplicated instead.', diff --git a/src/Dictionaries/Countries.php b/src/Dictionaries/Countries.php index 669c20ec15..a6dfe71aaf 100644 --- a/src/Dictionaries/Countries.php +++ b/src/Dictionaries/Countries.php @@ -19,7 +19,10 @@ class Countries extends BasicDictionary protected function getItemLabel(array $item): string { - return "{$item['emoji']} {$item['name']}"; + return vsprintf('%s%s', [ + ($this->config['emojis'] ?? true) ? "{$item['emoji']} " : '', + $item['name'], + ]); } protected function fieldItems() @@ -31,6 +34,12 @@ protected function fieldItems() 'type' => 'select', 'options' => $this->regions, ], + 'emojis' => [ + 'display' => __('Emojis'), + 'instructions' => __('statamic::messages.dictionaries_countries_emojis_instructions'), + 'type' => 'toggle', + 'default' => true, + ], ]; } From 1caf2ad1839bd82a8dd9ba5c97300958588fc1f5 Mon Sep 17 00:00:00 2001 From: Michael Aerni Date: Wed, 18 Sep 2024 16:24:20 -0400 Subject: [PATCH 13/85] [5.x] Make limit modifier work with query builders (#10818) Co-authored-by: Jason Varga --- src/Modifiers/CoreModifiers.php | 4 ++++ tests/Modifiers/LimitTest.php | 15 +++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/Modifiers/CoreModifiers.php b/src/Modifiers/CoreModifiers.php index be967c58fd..1bba376576 100644 --- a/src/Modifiers/CoreModifiers.php +++ b/src/Modifiers/CoreModifiers.php @@ -1452,6 +1452,10 @@ public function limit($value, $params) { $limit = Arr::get($params, 0, 0); + if (Compare::isQueryBuilder($value)) { + return $value->limit($limit); + } + if ($value instanceof Collection) { return $value->take($limit); } diff --git a/tests/Modifiers/LimitTest.php b/tests/Modifiers/LimitTest.php index e3b7275892..68da0ce7dd 100644 --- a/tests/Modifiers/LimitTest.php +++ b/tests/Modifiers/LimitTest.php @@ -5,6 +5,7 @@ use Illuminate\Support\Collection; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\Test; +use Statamic\Contracts\Query\Builder; use Statamic\Modifiers\Modify; use Tests\TestCase; @@ -34,8 +35,18 @@ public function it_limits_collections(): void $this->assertEquals(['one', 'two', 'three'], $limited->all()); } - public function modify($arr, $limit) + #[Test] + public function it_limits_builders(): void + { + $query = \Mockery::mock(Builder::class); + $query->shouldReceive('limit')->with(2)->once()->andReturnSelf(); + + $limited = $this->modify($query, 2); + $this->assertSame($query, $limited); + } + + public function modify($value, $limit) { - return Modify::value($arr)->limit($limit)->fetch(); + return Modify::value($value)->limit($limit)->fetch(); } } From 1eab47d0f765ca8cf3e8073667870e5521333b7a Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Thu, 19 Sep 2024 17:46:51 +0100 Subject: [PATCH 14/85] [5.x] Prevent concurrent requests to the Marketplace API (#10815) Co-authored-by: duncanmcclean Co-authored-by: Jason Varga --- src/Marketplace/Client.php | 51 +++++++++++++++++++++++++++----------- 1 file changed, 36 insertions(+), 15 deletions(-) diff --git a/src/Marketplace/Client.php b/src/Marketplace/Client.php index f508469854..c8730e7720 100644 --- a/src/Marketplace/Client.php +++ b/src/Marketplace/Client.php @@ -3,10 +3,16 @@ namespace Statamic\Marketplace; use Facades\GuzzleHttp\Client as Guzzle; +use Illuminate\Cache\NoLock; +use Illuminate\Contracts\Cache\LockProvider; +use Illuminate\Contracts\Cache\LockTimeoutException; +use Illuminate\Contracts\Cache\Repository; use Illuminate\Support\Facades\Cache; class Client { + const LOCK_KEY = 'statamic.marketplace.lock'; + /** * @var string */ @@ -23,9 +29,9 @@ class Client protected $verifySsl = true; /** - * @var int + * @var Repository */ - protected $cache; + private $store; /** * Instantiate marketplace API wrapper. @@ -47,26 +53,41 @@ public function __construct() */ public function get($endpoint, $params = []) { - $endpoint = collect([$this->domain, self::API_PREFIX, $endpoint])->implode('/'); + $lock = $this->lock(static::LOCK_KEY, 10); + + try { + $lock->block(5); + + $endpoint = collect([$this->domain, self::API_PREFIX, $endpoint])->implode('/'); - $key = 'marketplace-'.md5($endpoint.json_encode($params)); + $key = 'marketplace-'.md5($endpoint.json_encode($params)); - return Cache::rememberWithExpiration($key, function () use ($endpoint, $params) { - $response = Guzzle::request('GET', $endpoint, [ - 'verify' => $this->verifySsl, - 'query' => $params, - ]); + return $this->cache()->rememberWithExpiration($key, function () use ($endpoint, $params) { + $response = Guzzle::request('GET', $endpoint, [ + 'verify' => $this->verifySsl, + 'query' => $params, + ]); - $json = json_decode($response->getBody(), true); + $json = json_decode($response->getBody(), true); - return [$this->cache => $json]; - }); + return [60 => $json]; + }); + } catch (LockTimeoutException $e) { + return $this->cache()->get($key); + } finally { + $lock->release(); + } } - public function cache($cache = 60) + private function cache(): Repository { - $this->cache = $cache; + return $this->store ??= Cache::store(); + } - return $this; + private function lock(string $key, int $seconds) + { + return $this->cache()->getStore() instanceof LockProvider + ? $this->cache()->lock($key, $seconds) + : new NoLock($key, $seconds); } } From be249c2088c4f23b7df7cf0efe09c2c1d37e8673 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Thu, 19 Sep 2024 18:41:58 +0100 Subject: [PATCH 15/85] [5.x] Improve addons listing (#10812) Co-authored-by: Jack McDade --- resources/js/components/AddonDetails.vue | 78 +++++++++++------------- resources/views/addons/index.blade.php | 1 + src/Marketplace/AddonsQuery.php | 4 +- 3 files changed, 41 insertions(+), 42 deletions(-) diff --git a/resources/js/components/AddonDetails.vue b/resources/js/components/AddonDetails.vue index 4dbfdded78..a455ae0eab 100644 --- a/resources/js/components/AddonDetails.vue +++ b/resources/js/components/AddonDetails.vue @@ -2,48 +2,48 @@
- -
- - -

+
+
+
- -
-
-
-
-
+
+
+
+ +
+
+
+
+
+
+
{{ downloads }}
+
-
- -
-
-
{{ downloads }}
+
+
+ + +

+
+
- -
@@ -121,10 +121,6 @@ import AddonEditions from './addons/Editions.vue'; this.downloads = response.data.package.downloads.total; }); }, - - showComposerInstructions() { - this.modalOpen = true; - }, } } diff --git a/resources/views/addons/index.blade.php b/resources/views/addons/index.blade.php index 331587496d..529a0d0957 100644 --- a/resources/views/addons/index.blade.php +++ b/resources/views/addons/index.blade.php @@ -2,6 +2,7 @@ @extends('statamic::layout') @section('title', __('Addons')) +@section('wrapper_class', 'max-w-3xl') @section('content') diff --git a/src/Marketplace/AddonsQuery.php b/src/Marketplace/AddonsQuery.php index 6662039c6a..e88eadc8c9 100644 --- a/src/Marketplace/AddonsQuery.php +++ b/src/Marketplace/AddonsQuery.php @@ -41,7 +41,9 @@ public function get() $params = [ 'page' => $this->page, 'search' => $this->search, - 'filter' => ['statamic' => '3,4'], + 'filter' => ['statamic' => '3,4,5'], + 'sort' => 'most-popular', + 'perPage' => 12, ]; if ($this->installed) { From 5db2012cc6bce755eb46296eddcd2f7614ea7279 Mon Sep 17 00:00:00 2001 From: Steven Grant Date: Mon, 23 Sep 2024 20:58:38 +1200 Subject: [PATCH 16/85] [5.x] Fix small typo (#10824) Update system.php small typo --- config/system.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/system.php b/config/system.php index 5d8151c7e3..0bc9192b40 100644 --- a/config/system.php +++ b/config/system.php @@ -8,7 +8,7 @@ |-------------------------------------------------------------------------- | | The license key for the corresponding domain from your Statamic account. - | Without a key entered, your app will considered to be in Trial Mode. + | Without a key entered, your app will be considered to be in Trial Mode. | | https://statamic.dev/licensing#trial-mode | From 1a1aa5683d9f80f524117078b67a09f51ba680ec Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Mon, 23 Sep 2024 11:45:57 -0400 Subject: [PATCH 17/85] [5.x] Fix toasts in actions not being shown (#10828) --- resources/js/components/ToastBus.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/resources/js/components/ToastBus.js b/resources/js/components/ToastBus.js index b025589783..522eabdbc8 100644 --- a/resources/js/components/ToastBus.js +++ b/resources/js/components/ToastBus.js @@ -19,9 +19,18 @@ class ToastBus { intercept() { this.instance.$axios.interceptors.response.use(response => { - const toasts = response?.data?._toasts ?? [] + const data = response?.data; - toasts.forEach(toast => this.instance.$toast[toast.type](toast.message, {duration: toast.duration})) + if (!data) return response; + + const promise = data instanceof Blob + ? data.text().then(text => JSON.parse(text)) + : new Promise(resolve => resolve(data)); + + promise.then(json => { + const toasts = json._toasts ?? []; + toasts.forEach(toast => this.instance.$toast[toast.type](toast.message, {duration: toast.duration})) + }); return response; }); From 6db65ff2b820b6c21d8b3c1a88c1d74c5289e981 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Schwei=C3=9Finger?= Date: Mon, 23 Sep 2024 21:00:04 +0200 Subject: [PATCH 18/85] [5.x] Improve feedback when action fails (#10264) Co-authored-by: Jason Varga --- .../js/components/data-list/HasActions.js | 4 +++- resources/lang/de.json | 1 + resources/lang/de_CH.json | 1 + resources/lang/fr.json | 1 + resources/lang/nl.json | 1 + src/Actions/Delete.php | 16 +++++++++++++++- src/Actions/DeleteMultisiteEntry.php | 16 +++++++++++++++- src/Actions/Publish.php | 18 +++++++++++++++--- src/Actions/Unpublish.php | 18 +++++++++++++++--- src/Data/Publishable.php | 6 +++++- src/Http/Controllers/CP/ActionController.php | 10 +++++++++- 11 files changed, 81 insertions(+), 11 deletions(-) diff --git a/resources/js/components/data-list/HasActions.js b/resources/js/components/data-list/HasActions.js index 6b05e31209..6e58a50db1 100644 --- a/resources/js/components/data-list/HasActions.js +++ b/resources/js/components/data-list/HasActions.js @@ -14,7 +14,9 @@ export default { this.$events.$emit('clear-selections'); this.$events.$emit('reset-action-modals'); - if (response.message !== false) { + if (response.success === false) { + this.$toast.error(response.message || __("Action failed")); + } else { this.$toast.success(response.message || __("Action completed")); } diff --git a/resources/lang/de.json b/resources/lang/de.json index 5c27a393d9..8626052c5b 100644 --- a/resources/lang/de.json +++ b/resources/lang/de.json @@ -20,6 +20,7 @@ "A valid blueprint is required.": "Ein gültiger Blueprint ist erforderlich.", "Above": "Oberhalb", "Action completed": "Aktion abgeschlossen", + "Action failed": "Aktion fehlgeschlagen", "Activate Account": "Account aktivieren", "Activation URL": "Aktivierungs-URL", "Active": "Aktiv", diff --git a/resources/lang/de_CH.json b/resources/lang/de_CH.json index 706fb3ddea..6b00b1b5b8 100644 --- a/resources/lang/de_CH.json +++ b/resources/lang/de_CH.json @@ -20,6 +20,7 @@ "A valid blueprint is required.": "Ein gültiger Blueprint ist erforderlich.", "Above": "Oberhalb", "Action completed": "Aktion abgeschlossen", + "Action failed": "Aktion fehlgeschlagen", "Activate Account": "Account aktivieren", "Activation URL": "Aktivierungs-URL", "Active": "Aktiv", diff --git a/resources/lang/fr.json b/resources/lang/fr.json index 5c0b233132..2ae9a8a1c4 100644 --- a/resources/lang/fr.json +++ b/resources/lang/fr.json @@ -20,6 +20,7 @@ "A valid blueprint is required.": "Un Blueprint valide est exigé.", "Above": "Au dessus", "Action completed": "Action terminée", + "Action failed": "Action échouée", "Activate Account": "Activer le compte", "Activation URL": "URL d'activation", "Active": "actif", diff --git a/resources/lang/nl.json b/resources/lang/nl.json index 19d771d78a..c2e65ce364 100644 --- a/resources/lang/nl.json +++ b/resources/lang/nl.json @@ -20,6 +20,7 @@ "A valid blueprint is required.": "Een geldige blueprint is vereist.", "Above": "Boven", "Action completed": "Actie voltooid", + "Action failed": "Actie gefaald", "Activate Account": "Activeer Account", "Activation URL": "Activatie-url", "Active": "Actief", diff --git a/src/Actions/Delete.php b/src/Actions/Delete.php index 30dbd99877..4c6da3259f 100644 --- a/src/Actions/Delete.php +++ b/src/Actions/Delete.php @@ -59,7 +59,21 @@ public function bypassesDirtyWarning(): bool public function run($items, $values) { - $items->each->delete(); + $failures = $items->reject(fn ($entry) => $entry->delete()); + $total = $items->count(); + + if ($failures->isNotEmpty()) { + $success = $total - $failures->count(); + if ($total === 1) { + throw new \Exception(__('Item could not be deleted')); + } elseif ($success === 0) { + throw new \Exception(__('Items could not be deleted')); + } else { + throw new \Exception(__(':success/:total items were deleted', ['total' => $total, 'success' => $success])); + } + } + + return trans_choice('Item deleted|Items deleted', $total); } public function redirect($items, $values) diff --git a/src/Actions/DeleteMultisiteEntry.php b/src/Actions/DeleteMultisiteEntry.php index 3eb19e32b8..4d4b70e995 100644 --- a/src/Actions/DeleteMultisiteEntry.php +++ b/src/Actions/DeleteMultisiteEntry.php @@ -53,7 +53,21 @@ public function run($items, $values) $items->each->deleteDescendants(); } - $items->each->delete(); + $failures = $items->reject(fn ($entry) => $entry->delete()); + $total = $items->count(); + + if ($failures->isNotEmpty()) { + $success = $total - $failures->count(); + if ($total === 1) { + throw new \Exception(__('Entry could not be deleted')); + } elseif ($success === 0) { + throw new \Exception(__('Entries could not be deleted')); + } else { + throw new \Exception(__(':success/:total entries were deleted', ['total' => $total, 'success' => $success])); + } + } + + return trans_choice('Entry deleted|Entries deleted', $total); } private function canChangeBehavior(): bool diff --git a/src/Actions/Publish.php b/src/Actions/Publish.php index d573174cbc..14ee1c9eb3 100644 --- a/src/Actions/Publish.php +++ b/src/Actions/Publish.php @@ -49,8 +49,20 @@ public function buttonText() public function run($entries, $values) { - $entries->each(function ($entry) { - $entry->publish(['user' => User::current()]); - }); + $failures = $entries->reject(fn ($entry) => $entry->publish(['user' => User::current()])); + $total = $entries->count(); + + if ($failures->isNotEmpty()) { + $success = $total - $failures->count(); + if ($total === 1) { + throw new \Exception(__('Entry could not be published')); + } elseif ($success === 0) { + throw new \Exception(__('Entries could not be published')); + } else { + throw new \Exception(__(':success/:total entries were published', ['total' => $total, 'success' => $success])); + } + } + + return trans_choice('Entry published|Entries published', $total); } } diff --git a/src/Actions/Unpublish.php b/src/Actions/Unpublish.php index 2d27e0f8dc..c30e807da3 100644 --- a/src/Actions/Unpublish.php +++ b/src/Actions/Unpublish.php @@ -44,8 +44,20 @@ public function buttonText() public function run($entries, $values) { - $entries->each(function ($entry) { - $entry->unpublish(['user' => User::current()]); - }); + $failures = $entries->reject(fn ($entry) => $entry->unpublish(['user' => User::current()])); + $total = $entries->count(); + + if ($failures->isNotEmpty()) { + $success = $total - $failures->count(); + if ($total === 1) { + throw new \Exception(__('Entry could not be unpublished')); + } elseif ($success === 0) { + throw new \Exception(__('Entries could not be unpublished')); + } else { + throw new \Exception(__(':success/:total entries were unpublished', ['total' => $total, 'success' => $success])); + } + } + + return trans_choice('Entry unpublished|Entries unpublished', $total); } } diff --git a/src/Data/Publishable.php b/src/Data/Publishable.php index 79c46f7c2b..861628b9d7 100644 --- a/src/Data/Publishable.php +++ b/src/Data/Publishable.php @@ -23,7 +23,11 @@ public function publish($options = []) return $this->publishWorkingCopy($options); } - $this->published(true)->save(); + $saved = $this->published(true)->save(); + + if (! $saved) { + return false; + } return $this; } diff --git a/src/Http/Controllers/CP/ActionController.php b/src/Http/Controllers/CP/ActionController.php index 62a3860058..5a022df989 100644 --- a/src/Http/Controllers/CP/ActionController.php +++ b/src/Http/Controllers/CP/ActionController.php @@ -2,6 +2,7 @@ namespace Statamic\Http\Controllers\CP; +use Exception; use Illuminate\Http\Request; use Statamic\Facades\Action; use Statamic\Facades\User; @@ -35,8 +36,14 @@ public function run(Request $request) abort_unless($unauthorized->isEmpty(), 403, __('You are not authorized to run this action.')); $values = $action->fields()->addValues($request->all())->process()->values()->all(); + $successful = true; - $response = $action->run($items, $values); + try { + $response = $action->run($items, $values); + } catch (Exception $e) { + $response = empty($msg = $e->getMessage()) ? __('Action failed') : $msg; + $successful = false; + } if ($redirect = $action->redirect($items, $values)) { return [ @@ -52,6 +59,7 @@ public function run(Request $request) } $response = $response ?: []; + $response['success'] = $successful; if (Arr::get($context, 'view') === 'form') { $response['data'] = $this->getItemData($items->first(), $context); From ddbf1bb6e8779aa3e7b99606dcf3fa6efb453679 Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Tue, 24 Sep 2024 14:10:04 -0400 Subject: [PATCH 19/85] [5.x] Fix CP nav ordering for when preferences are stored in JSON SQL columns (#10809) --- .../Navigation/NavPreferencesNormalizer.php | 56 +++- src/CP/Navigation/NavTransformer.php | 117 +++----- .../NavPreferencesNormalizerTest.php | 272 +++++++++++++++++- tests/CP/Navigation/NavTransformerTest.php | 58 ++-- 4 files changed, 378 insertions(+), 125 deletions(-) diff --git a/src/CP/Navigation/NavPreferencesNormalizer.php b/src/CP/Navigation/NavPreferencesNormalizer.php index 681670b041..5d12eab177 100644 --- a/src/CP/Navigation/NavPreferencesNormalizer.php +++ b/src/CP/Navigation/NavPreferencesNormalizer.php @@ -2,6 +2,7 @@ namespace Statamic\CP\Navigation; +use Illuminate\Support\Collection; use Statamic\Support\Arr; use Statamic\Support\Str; @@ -72,11 +73,12 @@ protected function normalize() { $navConfig = collect($this->preferences); - $normalized = collect()->put('reorder', $reorder = $navConfig->get('reorder', false)); + $normalized = collect()->put('reorder', (bool) $reorder = $navConfig->get('reorder', false)); $sections = collect($navConfig->get('sections') ?? $navConfig->except('reorder')); - $sections = $sections + $sections = $this + ->normalizeToInheritsFromReorder($sections, $reorder) ->prepend($sections->pull('top_level') ?? '@inherit', 'top_level') ->map(fn ($config, $section) => $this->normalizeSectionConfig($config, $section)) ->reject(fn ($config) => $config['action'] === '@inherit' && ! $reorder) @@ -115,7 +117,7 @@ protected function normalizeSectionConfig($sectionConfig, $sectionKey) $normalized->put('display', $sectionConfig->get('display', false)); - $normalized->put('reorder', $reorder = $sectionConfig->get('reorder', false)); + $normalized->put('reorder', (bool) $reorder = $sectionConfig->get('reorder', false)); $items = collect($sectionConfig->get('items') ?? $sectionConfig->except([ 'action', @@ -123,7 +125,8 @@ protected function normalizeSectionConfig($sectionConfig, $sectionKey) 'reorder', ])); - $items = $items + $items = $this + ->normalizeToInheritsFromReorder($items, $reorder) ->map(fn ($config, $itemId) => $this->normalizeItemConfig($itemId, $config, $sectionKey)) ->keyBy(fn ($config, $itemId) => $this->normalizeItemId($itemId, $config)) ->filter() @@ -187,14 +190,23 @@ protected function normalizeItemConfig($itemId, $itemConfig, $sectionKey, $remov } } - // If item has children, normalize those items as well. - if ($children = $normalized->get('children')) { - $normalized->put('children', collect($children) - ->map(fn ($childConfig, $childId) => $this->normalizeChildItemConfig($childId, $childConfig, $sectionKey)) - ->keyBy(fn ($childConfig, $childId) => $this->normalizeItemId($childId, $childConfig)) - ->all()); + // Normalize `reorder` bool. + if ($reorder = $normalized->get('reorder', false)) { + $normalized->put('reorder', (bool) $reorder); } + // Normalize `children`. + $children = $this + ->normalizeToInheritsFromReorder($normalized->get('children', []), $reorder) + ->map(fn ($childConfig, $childId) => $this->normalizeChildItemConfig($childId, $childConfig, $sectionKey)) + ->keyBy(fn ($childConfig, $childId) => $this->normalizeItemId($childId, $childConfig)) + ->all(); + + // Only output `children` in normalized output if there are any. + $children + ? $normalized->put('children', $children) + : $normalized->forget('children'); + $allowedKeys = array_merge(['action'], static::ALLOWED_NAV_ITEM_MODIFICATIONS); return $normalized->only($allowedKeys)->all(); @@ -234,11 +246,14 @@ protected function normalizeChildItemConfig($itemId, $itemConfig, $sectionKey) ]; } - if (is_array($itemConfig)) { - Arr::forget($itemConfig, 'children'); + $normalized = $this->normalizeItemConfig($itemId, $itemConfig, $sectionKey, false); + + if (is_array($normalized)) { + Arr::forget($normalized, 'reorder'); + Arr::forget($normalized, 'children'); } - return $this->normalizeItemConfig($itemId, $itemConfig, $sectionKey, false); + return $normalized; } /** @@ -272,6 +287,21 @@ protected function itemIsInOriginalSection($itemId, $currentSectionKey) return Str::startsWith($itemId, "$currentSectionKey::"); } + /** + * Normalize to legacy style inherits from new `reorder: []` array schema, introduced to sidestep ordering issues in SQL. + */ + protected function normalizeToInheritsFromReorder(array|Collection $items, array|bool $reorder): Collection + { + if (! is_array($reorder)) { + return collect($items); + } + + return collect($reorder) + ->flip() + ->map(fn () => '@inherit') + ->merge($items); + } + /** * Get normalized preferences. * diff --git a/src/CP/Navigation/NavTransformer.php b/src/CP/Navigation/NavTransformer.php index 2570d4fe99..2f1f8001eb 100644 --- a/src/CP/Navigation/NavTransformer.php +++ b/src/CP/Navigation/NavTransformer.php @@ -12,7 +12,6 @@ class NavTransformer protected $coreNav; protected $submitted; protected $config; - protected $reorderedMinimums; /** * Instantiate nav transformer. @@ -59,10 +58,9 @@ protected function removeEmptyCustomSections($submitted) */ protected function transform() { - $this->config['reorder'] = $this->itemsAreReordered( - $this->coreNav->pluck('display_original'), - collect($this->submitted)->pluck('display_original'), - 'sections' + $this->config['reorder'] = $this->getReorderedItems( + $this->coreNav->map(fn ($section) => $this->transformSectionKey($section)), + collect($this->submitted)->map(fn ($section) => $this->transformSectionKey($section)), ); $this->config['sections'] = collect($this->submitted) @@ -110,10 +108,9 @@ protected function transformSection($section, $sectionKey) $items = Arr::get($section, 'items', []); - $transformed['reorder'] = $this->itemsAreReordered( + $transformed['reorder'] = $this->getReorderedItems( $this->coreNav->pluck('items', 'display_original')->get($displayOriginal, collect())->map->id(), collect($items)->pluck('id'), - $sectionKey ); $transformed['items'] = $this->transformItems($items, $sectionKey); @@ -134,7 +131,7 @@ protected function transformItems($items, $parentId, $transformingChildItems = f return collect($items) ->map(fn ($item) => array_merge($item, ['id' => $this->transformItemId($item, $item['id'], $parentId, $items)])) ->keyBy('id') - ->map(fn ($item, $itemId) => $this->transformItem($item, $itemId, $parentId, $transformingChildItems)) + ->map(fn ($item, $itemId) => $this->transformItem($item, $itemId, $transformingChildItems)) ->all(); } @@ -174,11 +171,10 @@ protected function transformItemId($item, $id, $parentId, $items) * * @param array $item * @param string $itemId - * @param string $parentId * @param bool $isChild * @return array */ - protected function transformItem($item, $itemId, $parentId, $isChild = false) + protected function transformItem($item, $itemId, $isChild = false) { $transformed = Arr::get($item, 'manipulations', []); @@ -210,10 +206,9 @@ protected function transformItem($item, $itemId, $parentId, $isChild = false) $transformed['reorder'] = false; if ($children && $originalHasChildren && ! in_array($transformed['action'], ['@alias', '@create'])) { - $transformed['reorder'] = $this->itemsAreReordered( + $transformed['reorder'] = $this->getReorderedItems( $originalItem->resolveChildren()->children()->map->id()->all(), collect($children)->keys()->all(), - $itemId ); } @@ -254,14 +249,12 @@ protected function transformItemUrl($url) } /** - * Check if items are being reordered. + * Check if items are being reordered and return minimum list of item keys required to replicate saved order. * * @param array $originalList * @param array $newList - * @param string $parentKey - * @return bool */ - protected function itemsAreReordered($originalList, $newList, $parentKey) + protected function getReorderedItems($originalList, $newList): bool|array { $itemsAreReordered = collect($originalList) ->intersect($newList) @@ -271,21 +264,22 @@ protected function itemsAreReordered($originalList, $newList, $parentKey) ->reject(fn ($pair) => $pair->first() === $pair->last()) ->isNotEmpty(); - if ($itemsAreReordered) { - $this->trackReorderedMinimums($originalList, $newList, $parentKey); + if (! $itemsAreReordered) { + return false; } - return $itemsAreReordered; + return collect($newList) + ->take($this->calculateMinimumItemsForReorder($originalList, $newList)) + ->all(); } /** - * Track minimum number of items needed for reorder config. + * Calculate minimum number of items needed for reorder config. * * @param array $originalList * @param array $newList - * @param string $parentKey */ - protected function trackReorderedMinimums($originalList, $newList, $parentKey) + protected function calculateMinimumItemsForReorder($originalList, $newList): int { $continueRejecting = true; @@ -301,7 +295,7 @@ protected function trackReorderedMinimums($originalList, $newList, $parentKey) }) ->count(); - $this->reorderedMinimums[$parentKey] = max(1, $minimumItemsCount - 1); + return max(1, $minimumItemsCount - 1); } /** @@ -326,15 +320,18 @@ protected function findOriginalItem($id) */ protected function minify() { - $this->config['sections'] = collect($this->config['sections']) - ->map(fn ($section, $key) => $this->minifySection($section, $key)) + $sections = collect($this->config['sections']) + ->map(fn ($section) => $this->minifySection($section)) + ->pipe(fn ($sections) => $this->rejectInherits($sections)); + + $reorder = collect(Arr::get($this->config, 'reorder') ?: []) + ->reject(fn ($section) => $section === 'top_level') + ->values() ->all(); - if ($this->config['reorder'] === true) { - $this->config['sections'] = $this->rejectUnessessaryInherits($this->config['sections'], 'sections'); - } else { - $this->config = $this->rejectAllInherits($this->config['sections']); - } + $this->config = $reorder + ? array_filter(compact('reorder', 'sections')) + : $sections; // If the config is completely null after minifying, ensure we save an empty array. // For example, if we're transforming this config for a user's nav preferences, @@ -351,21 +348,17 @@ protected function minify() * Minify tranformed section. * * @param array $section - * @param string $sectionKey * @return mixed */ - protected function minifySection($section, $sectionKey) + protected function minifySection($section) { $action = Arr::get($section, 'action'); $section['items'] = collect($section['items']) - ->map(fn ($item, $key) => $this->minifyItem($item, $key)) - ->all(); + ->map(fn ($item) => $this->minifyItem($item)) + ->pipe(fn ($items) => $this->rejectInherits($items)); - if ($section['reorder'] === true) { - $section['items'] = $this->rejectUnessessaryInherits($section['items'], $sectionKey); - } else { - $section['items'] = $this->rejectAllInherits($section['items']); + if (! $section['reorder']) { Arr::forget($section, 'reorder'); } @@ -390,21 +383,16 @@ protected function minifySection($section, $sectionKey) * Minify tranformed item. * * @param array $item - * @param string $itemKey + * @param bool $isChild * @return array */ - protected function minifyItem($item, $itemKey, $isChild = false) + protected function minifyItem($item, $isChild = false) { - $action = Arr::get($item, 'action'); - $item['children'] = collect($item['children'] ?? []) - ->map(fn ($item, $childId) => $this->minifyItem($item, $childId, true)) - ->all(); + ->map(fn ($item) => $this->minifyItem($item, true)) + ->pipe(fn ($items) => $this->rejectInherits($items)); - if ($item['reorder'] === true) { - $item['children'] = $this->rejectUnessessaryInherits($item['children'], $itemKey); - } else { - $item['children'] = $this->rejectAllInherits($item['children']); + if (! $item['reorder']) { Arr::forget($item, 'reorder'); } @@ -432,7 +420,7 @@ protected function minifyItem($item, $itemKey, $isChild = false) * @param array $items * @return array */ - protected function rejectAllInherits($items) + protected function rejectInherits($items) { $items = collect($items)->reject(fn ($item) => $item === '@inherit'); @@ -443,37 +431,6 @@ protected function rejectAllInherits($items) return $items->all(); } - /** - * Reject unessessary `@inherit`s at end of array. - * - * @param array $items - * @param string $parentKey - * @return array - */ - protected function rejectUnessessaryInherits($items, $parentKey) - { - if (! $reorderedMinimum = $this->reorderedMinimums[$parentKey] ?? false) { - return $items; - } - - $keyValuePairs = collect($items) - ->map(fn ($item, $key) => ['key' => $key, 'value' => $item]) - ->values() - ->keyBy(fn ($keyValuePair, $index) => $index + 1); - - $trailingInherits = $keyValuePairs - ->reverse() - ->takeUntil(fn ($item) => $item['value'] !== '@inherit'); - - $modifiedMinimum = $keyValuePairs->count() - $trailingInherits->count(); - - $actualMinimum = max($reorderedMinimum, $modifiedMinimum); - - return collect($items) - ->take($actualMinimum) - ->all(); - } - /** * Get config. * diff --git a/tests/CP/Navigation/NavPreferencesNormalizerTest.php b/tests/CP/Navigation/NavPreferencesNormalizerTest.php index c0cfbf4870..44ccafd8b8 100644 --- a/tests/CP/Navigation/NavPreferencesNormalizerTest.php +++ b/tests/CP/Navigation/NavPreferencesNormalizerTest.php @@ -134,6 +134,13 @@ public function it_ensures_top_level_section_is_always_first_returned_section() 'top_level' => ['content::collections::pages' => '@alias'], ])['sections'])); + // With `reorder: []` array + $this->assertEquals(['top_level', 'content'], array_keys($this->normalize([ + 'reorder' => ['content', 'top_level'], + 'content' => ['fields::blueprints' => '@alias'], + 'top_level' => ['content::collections::pages' => '@alias'], + ])['sections'])); + // With `reorder: true` and sections properly nested $this->assertEquals(['top_level', 'content'], array_keys($this->normalize([ 'reorder' => true, @@ -142,6 +149,15 @@ public function it_ensures_top_level_section_is_always_first_returned_section() 'top_level' => ['content::collections::pages' => '@alias'], ], ])['sections'])); + + // With `reorder: []` array and sections properly nested + $this->assertEquals(['top_level', 'content'], array_keys($this->normalize([ + 'reorder' => ['content', 'top_level'], + 'sections' => [ + 'content' => ['fields::blueprints' => '@alias'], + 'top_level' => ['content::collections::pages' => '@alias'], + ], + ])['sections'])); } #[Test] @@ -199,6 +215,14 @@ public function it_doesnt_remove_inherit_action_sections_when_actually_reorderin 'tools' => '@inherit', ])['sections'])); + // With `reorder: []` array + $this->assertEquals(['top_level', 'users', 'tools'], array_keys($this->normalize([ + 'reorder' => ['top_level', 'users', 'tools'], + 'users' => [ + 'content::collections::profiles' => '@move', + ], + ])['sections'])); + // With `reorder: true` and sections properly nested $this->assertEquals(['top_level', 'users', 'tools'], array_keys($this->normalize([ 'reorder' => true, @@ -210,6 +234,16 @@ public function it_doesnt_remove_inherit_action_sections_when_actually_reorderin 'tools' => '@inherit', ], ])['sections'])); + + // With `reorder: []` array and sections properly nested + $this->assertEquals(['top_level', 'users', 'tools'], array_keys($this->normalize([ + 'reorder' => ['top_level', 'users', 'tools'], + 'sections' => [ + 'users' => [ + 'content::collections::profiles' => '@move', + ], + ], + ])['sections'])); } #[Test] @@ -243,6 +277,18 @@ public function it_doesnt_remove_inherit_action_items_when_actually_reordering() ], ])['sections']['content']['items'])); + // With `reorder: []` array + $this->assertEquals($expected, array_keys($this->normalize([ + 'content' => [ + 'reorder' => [ + 'content::collections::pages', + 'content::collections::posts', + 'content::collections::profiles', + ], + 'content::collections::posts' => ['display' => 'Posterinos'], + ], + ])['sections']['content']['items'])); + // With `reorder: true` and sections properly nested $this->assertEquals($expected, array_keys($this->normalize([ 'content' => [ @@ -254,6 +300,20 @@ public function it_doesnt_remove_inherit_action_items_when_actually_reordering() ], ], ])['sections']['content']['items'])); + + // With `reorder: []` array and sections properly nested + $this->assertEquals($expected, array_keys($this->normalize([ + 'content' => [ + 'reorder' => [ + 'content::collections::pages', + 'content::collections::posts', + 'content::collections::profiles', + ], + 'items' => [ + 'content::collections::posts' => ['display' => 'Posterinos'], + ], + ], + ])['sections']['content']['items'])); } #[Test] @@ -265,7 +325,19 @@ public function it_defaults_action_to_modify_when_modifying_in_original_section( 'content' => [ 'reorder' => true, 'content::collections::pages' => [ - $modifier => 'test', + $modifier => [], + ], + ], + ]), 'sections.content.items.content::collections::pages.action')); + + // With `reorder: []` array + $this->assertEquals('@modify', Arr::get($this->normalize([ + 'content' => [ + 'reorder' => [ + 'content::collections::pages', + ], + 'content::collections::pages' => [ + $modifier => [], ], ], ]), 'sections.content.items.content::collections::pages.action')); @@ -276,7 +348,21 @@ public function it_defaults_action_to_modify_when_modifying_in_original_section( 'reorder' => true, 'items' => [ 'content::collections::pages' => [ - $modifier => 'test', + $modifier => [], + ], + ], + ], + ]), 'sections.content.items.content::collections::pages.action')); + + // With `reorder: []` array and sections properly nested + $this->assertEquals('@modify', Arr::get($this->normalize([ + 'content' => [ + 'reorder' => [ + 'content::collections::pages', + ], + 'items' => [ + 'content::collections::pages' => [ + $modifier => [], ], ], ], @@ -299,6 +385,15 @@ public function it_defaults_action_to_inherit_when_reordering_in_original_sectio ], ]), 'sections.content.items.content::collections::pages.action')); + // With `reorder: []` array + $this->assertEquals('@inherit', Arr::get($this->normalize([ + 'content' => [ + 'reorder' => [ + 'content::collections::pages', + ], + ], + ]), 'sections.content.items.content::collections::pages.action')); + // With `reorder: true` and sections properly nested $this->assertEquals('@inherit', Arr::get($this->normalize([ 'content' => [ @@ -308,6 +403,15 @@ public function it_defaults_action_to_inherit_when_reordering_in_original_sectio ], ], ]), 'sections.content.items.content::collections::pages.action')); + + // With `reorder: []` array and sections properly nested + $this->assertEquals('@inherit', Arr::get($this->normalize([ + 'content' => [ + 'reorder' => [ + 'content::collections::pages', + ], + ], + ]), 'sections.content.items.content::collections::pages.action')); } #[Test] @@ -538,12 +642,12 @@ public function it_normalizes_an_example_config() 'sections' => [ 'top_level' => [ 'action' => false, - 'reorder' => false, 'display' => false, + 'reorder' => false, 'items' => [ 'top_level::dashboard' => [ - 'action' => '@modify', 'display' => 'Dashboard Confessional', + 'action' => '@modify', ], $topLevelBlueprintsId => [ 'action' => '@alias', @@ -555,12 +659,12 @@ public function it_normalizes_an_example_config() ], 'content' => [ 'action' => false, - 'reorder' => false, 'display' => false, + 'reorder' => false, 'items' => [ $contentBlueprintsId => [ - 'action' => '@alias', 'display' => 'Content Blueprints', + 'action' => '@alias', ], 'user::profiles' => [ 'action' => '@create', @@ -571,13 +675,167 @@ public function it_normalizes_an_example_config() ], 'fields' => [ 'action' => '@hide', + 'display' => false, 'reorder' => false, + 'items' => [], + ], + ], + ]; + + $this->assertSame($expected, $nav); + } + + #[Test] + public function it_normalizes_example_config_with_legacy_reordering_style() + { + $nav = $this->normalize([ + 'reorder' => true, + 'sections' => [ + 'fields' => '@inherit', + 'content' => [ + 'reorder' => true, + 'items' => [ + 'content::globals' => '@inherit', + 'content::collections' => [ + 'action' => '@modify', + 'reorder' => true, + 'children' => [ + 'content::collections::pages' => '@inherit', + 'content::collections::articles' => [ + 'action' => '@modify', + 'display' => 'Featured Articles', + ], + ], + ], + 'content::assets' => '@inherit', + ], + ], + ], + ]); + + $expected = [ + 'reorder' => true, + 'sections' => [ + 'top_level' => [ + 'action' => false, + 'display' => false, + 'reorder' => false, + 'items' => [], + ], + 'fields' => [ + 'action' => false, + 'display' => false, + 'reorder' => false, + 'items' => [], + ], + 'content' => [ + 'action' => false, + 'display' => false, + 'reorder' => true, + 'items' => [ + 'content::globals' => [ + 'action' => '@inherit', + ], + 'content::collections' => [ + 'action' => '@modify', + 'reorder' => true, + 'children' => [ + 'content::collections::pages' => [ + 'action' => '@inherit', + ], + 'content::collections::articles' => [ + 'action' => '@modify', + 'display' => 'Featured Articles', + ], + ], + ], + 'content::assets' => [ + 'action' => '@inherit', + ], + ], + ], + ], + ]; + + $this->assertSame($expected, $nav); + } + + #[Test] + public function it_normalizes_example_config_with_new_array_reordering_style() + { + $nav = $this->normalize([ + 'reorder' => [ + 'fields', + ], + 'sections' => [ + 'content' => [ + 'reorder' => [ + 'content::globals', + 'content::collections', + 'content::assets', + ], + 'items' => [ + 'content::collections' => [ + 'action' => '@modify', + 'reorder' => [ + 'content::collections::pages', + ], + 'children' => [ + 'content::collections::articles' => [ + 'action' => '@modify', + 'display' => 'Featured Articles', + ], + ], + ], + ], + ], + ], + ]); + + $expected = [ + 'reorder' => true, + 'sections' => [ + 'top_level' => [ + 'action' => false, 'display' => false, + 'reorder' => false, + 'items' => [], + ], + 'fields' => [ + 'action' => false, + 'display' => false, + 'reorder' => false, 'items' => [], ], + 'content' => [ + 'action' => false, + 'display' => false, + 'reorder' => true, + 'items' => [ + 'content::globals' => [ + 'action' => '@inherit', + ], + 'content::collections' => [ + 'action' => '@modify', + 'reorder' => true, + 'children' => [ + 'content::collections::pages' => [ + 'action' => '@inherit', + ], + 'content::collections::articles' => [ + 'action' => '@modify', + 'display' => 'Featured Articles', + ], + ], + ], + 'content::assets' => [ + 'action' => '@inherit', + ], + ], + ], ], ]; - $this->assertEquals($expected, $nav); + $this->assertSame($expected, $nav); } } diff --git a/tests/CP/Navigation/NavTransformerTest.php b/tests/CP/Navigation/NavTransformerTest.php index de006c73f5..fa867884bb 100644 --- a/tests/CP/Navigation/NavTransformerTest.php +++ b/tests/CP/Navigation/NavTransformerTest.php @@ -820,13 +820,12 @@ public function it_can_reorder_items() $expected = [ 'content' => [ - 'reorder' => true, - 'items' => [ - 'content::navigation' => '@inherit', - 'content::taxonomies' => '@inherit', - 'content::assets' => '@inherit', + 'reorder' => [ + 'content::navigation', + 'content::taxonomies', + 'content::assets', + // 'Collections' and 'Globals' items are omitted because they are redundant in this case ], - // 'Collections' and 'Globals' items are omitted because they are redundant in this case ], ]; @@ -864,16 +863,18 @@ public function it_can_reorder_custom_and_modified_items() $expected = [ 'content' => [ - 'reorder' => true, + 'reorder' => [ + 'content::navigation', + 'content::taxonomies', + 'content::assets', + 'content::collections', + 'content::globals', + ], 'items' => [ - 'content::navigation' => '@inherit', 'content::taxonomies' => [ 'action' => '@modify', 'display' => 'Favourite Taxonomies', ], - 'content::assets' => '@inherit', - 'content::collections' => '@inherit', - 'content::globals' => '@inherit', 'content::custom_item' => [ 'action' => '@create', 'display' => 'Custom Item', @@ -1039,11 +1040,10 @@ public function it_can_reorder_sections() ]); $expected = [ - 'reorder' => true, - 'sections' => [ - 'top_level' => '@inherit', - 'fields' => '@inherit', - 'tools' => '@inherit', + 'reorder' => [ + // 'Top Level' is omitted because it'll always be top level + 'fields', + 'tools', // 'Content', 'Settings', and 'Users' sections are omitted because they are redundant in this case ], ]; @@ -1085,15 +1085,16 @@ public function it_can_reorder_custom_and_modified_sections() ]); $expected = [ - 'reorder' => true, + 'reorder' => [ + 'fields', + 'tools', + 'content', + 'users', + ], 'sections' => [ - 'top_level' => '@inherit', 'fields' => [ 'content::collections' => '@alias', ], - 'tools' => '@inherit', - 'content' => '@inherit', - 'users' => '@inherit', 'custom_section' => [ 'display' => 'Custom Section', 'action' => '@create', @@ -1247,7 +1248,11 @@ public function it_can_transform_complex_json_payload_copied_from_actual_vue_sub $transformed = $this->transform(json_decode('[{"display":"Top Level","display_original":"Top Level","action":false,"items":[{"id":"top_level::dashboard","manipulations":[],"children":[]},{"id":"content::collections::posts","manipulations":{"action":"@alias"},"children":[]},{"id":"tools::updates","manipulations":{"action":"@move"},"children":[]},{"id":"new_top_level_item","manipulations":{"action":"@create","display":"New Top Level Item","url":"\/new-top-level-item"},"children":[{"id":"new_child_item","manipulations":{"action":"@create","display":"New Child Item","url":"\/new-child-item"},"children":[]}]}]},{"display":"Fields","display_original":"Fields","action":false,"items":[{"id":"fields::blueprints","manipulations":{"display":"Blueprints Renamed","action":"@modify"},"children":[]},{"id":"fields::fieldsets","manipulations":[],"children":[]}]},{"display":"Content Renamed","display_original":"Content","action":false,"items":[{"id":"content::collections::pages","manipulations":{"action":"@move"},"children":[]},{"id":"content::collections","manipulations":{"action":"@modify"},"children":[{"id":"content::collections::posts","manipulations":{"display":"Posterinos","action":"@modify"},"children":[]}]},{"id":"content::navigation","manipulations":[],"children":[{"id":"content::navigation::nav_test","manipulations":[],"children":[]}]},{"id":"content::taxonomies","manipulations":[],"children":[]},{"id":"content::assets","manipulations":[],"children":[{"id":"content::assets::assets","manipulations":[],"children":[]},{"id":"content::assets::essthree","manipulations":[],"children":[]}]}]},{"display":"Custom Section","display_original":"Custom Section","action":"@create","items":[{"id":"custom_section::new_item","manipulations":{"action":"@create","display":"New Item","url":"\/new-item"},"children":[]},{"id":"content::taxonomies::tags","manipulations":{"action":"@move"},"children":[]},{"id":"content::globals","manipulations":{"action":"@move"},"children":[{"id":"content::globals::global","manipulations":[],"children":[]}]}]},{"display":"Tools","display_original":"Tools","action":false,"items":[{"id":"tools::forms","manipulations":[],"children":[{"id":"tools::forms::test","manipulations":[],"children":[]}]},{"id":"tools::addons","manipulations":[],"children":[]},{"id":"tools::utilities","manipulations":[],"children":[{"id":"tools::utilities::cache","manipulations":[],"children":[]},{"id":"tools::utilities::email","manipulations":[],"children":[]},{"id":"tools::utilities::licensing","manipulations":[],"children":[]},{"id":"tools::utilities::php_info","manipulations":[],"children":[]},{"id":"tools::utilities::search","manipulations":[],"children":[]}]}]},{"display":"Users","display_original":"Users","action":false,"items":[{"id":"users::users","manipulations":[],"children":[]},{"id":"users::groups","manipulations":[],"children":[]},{"id":"users::permissions","manipulations":[],"children":[{"id":"users::permissions::author","manipulations":[],"children":[]},{"id":"users::permissions::not_social_media_manager","manipulations":[],"children":[]},{"id":"users::permissions::social_media_manager","manipulations":[],"children":[]}]}]}]', true)); $expected = [ - 'reorder' => true, + 'reorder' => [ + 'fields', + 'content', + 'custom_section', + ], 'sections' => [ 'top_level' => [ 'content::collections::posts' => '@alias', @@ -1273,7 +1278,12 @@ public function it_can_transform_complex_json_payload_copied_from_actual_vue_sub ], 'content' => [ 'display' => 'Content Renamed', - 'reorder' => true, + 'reorder' => [ + 'content::collections::pages', + 'content::collections', + 'content::navigation', + 'content::taxonomies', + ], 'items' => [ 'content::collections::pages' => '@move', 'content::collections' => [ @@ -1285,8 +1295,6 @@ public function it_can_transform_complex_json_payload_copied_from_actual_vue_sub ], ], ], - 'content::navigation' => '@inherit', - 'content::taxonomies' => '@inherit', ], ], 'custom_section' => [ From a375508d5f6ca900efa18f24a7a44afed206eff5 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Tue, 24 Sep 2024 17:02:24 -0400 Subject: [PATCH 20/85] changelog --- CHANGELOG.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1dcd3a90cb..2e5760132f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,32 @@ # Release Notes +## 5.26.0 (2024-09-24) + +### What's new +- Improve feedback when action fails [#10264](https://github.com/statamic/cms/issues/10264) by @simonerd +- Add option to exclude flag emojis from countries dictionary [#10817](https://github.com/statamic/cms/issues/10817) by @jasonvarga +- Add entry password protection [#10800](https://github.com/statamic/cms/issues/10800) by @aerni +- Add submitting state for confirmation modal to better visualise a running action [#10699](https://github.com/statamic/cms/issues/10699) by @morhi + +### What's fixed +- Fix CP nav ordering for when preferences are stored in JSON SQL columns [#10809](https://github.com/statamic/cms/issues/10809) by @jesseleite +- Fix toasts in actions not being shown [#10828](https://github.com/statamic/cms/issues/10828) by @jasonvarga +- Fix small typo [#10824](https://github.com/statamic/cms/issues/10824) by @1stevengrant +- Improve addons listing [#10812](https://github.com/statamic/cms/issues/10812) by @duncanmcclean +- Prevent concurrent requests to the Marketplace API [#10815](https://github.com/statamic/cms/issues/10815) by @duncanmcclean +- Make limit modifier work with query builders [#10818](https://github.com/statamic/cms/issues/10818) by @aerni +- Hide Visit URL and Live Preview if term has no template [#10789](https://github.com/statamic/cms/issues/10789) by @edalzell +- Set path on asset folder when moving [#10813](https://github.com/statamic/cms/issues/10813) by @jasonvarga +- Reset previous filters when you finish reordering [#10797](https://github.com/statamic/cms/issues/10797) by @duncanmcclean +- Update CSRF token when session expiry login modal is closed [#10794](https://github.com/statamic/cms/issues/10794) by @jasonvarga +- Fix broken state of "Parent" field when saving Home entry with Revisions [#10726](https://github.com/statamic/cms/issues/10726) by @duncanmcclean +- Improve ImageGenerator Exception handling [#10786](https://github.com/statamic/cms/issues/10786) by @indykoning +- When augmenting terms, `entries_count` should only consider published entries [#10727](https://github.com/statamic/cms/issues/10727) by @duncanmcclean +- Prevent saving value of `parent` field to entry data [#10725](https://github.com/statamic/cms/issues/10725) by @duncanmcclean +- Bump vite from 4.5.3 to 4.5.5 [#10810](https://github.com/statamic/cms/issues/10810) by @dependabot + + + ## 5.25.0 (2024-09-10) ### What's new From 563d731f4a9e14f399a38415f2765d0a9a0cfbfe Mon Sep 17 00:00:00 2001 From: Michael Aerni Date: Wed, 25 Sep 2024 12:50:11 -0400 Subject: [PATCH 21/85] [5.x] Fix issue when using Livewire with full measure static caching (#10306) --- src/StaticCaching/Replacers/NoCacheReplacer.php | 12 ++++++++++-- .../StaticCaching/FullMeasureStaticCachingTest.php | 14 +++++++------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/StaticCaching/Replacers/NoCacheReplacer.php b/src/StaticCaching/Replacers/NoCacheReplacer.php index d685c965a4..c153199005 100644 --- a/src/StaticCaching/Replacers/NoCacheReplacer.php +++ b/src/StaticCaching/Replacers/NoCacheReplacer.php @@ -3,6 +3,7 @@ namespace Statamic\StaticCaching\Replacers; use Illuminate\Http\Response; +use Illuminate\Support\Str; use Statamic\Facades\StaticCache; use Statamic\StaticCaching\Cacher; use Statamic\StaticCaching\Cachers\FileCacher; @@ -78,8 +79,15 @@ private function modifyFullMeasureResponse(Response $response) $contents = $response->getContent(); if ($cacher->shouldOutputJs()) { - $js = $cacher->getNocacheJs(); - $contents = str_replace('', '', $contents); + $insertBefore = collect([ + Str::position($contents, ''), + ])->filter()->min(); + + $js = ""; + + $contents = Str::substrReplace($contents, $js, $insertBefore, 0); } $contents = str_replace('NOCACHE_PLACEHOLDER', $cacher->getNocachePlaceholder(), $contents); diff --git a/tests/StaticCaching/FullMeasureStaticCachingTest.php b/tests/StaticCaching/FullMeasureStaticCachingTest.php index ed31ae1425..447b4c87de 100644 --- a/tests/StaticCaching/FullMeasureStaticCachingTest.php +++ b/tests/StaticCaching/FullMeasureStaticCachingTest.php @@ -58,7 +58,7 @@ public function index() })::register(); $this->withFakeViews(); - $this->viewShouldReturnRaw('layout', '{{ template_content }}'); + $this->viewShouldReturnRaw('layout', '{{ template_content }}'); $this->viewShouldReturnRaw('default', '{{ example_count }} {{ nocache }}{{ example_count }}{{ /nocache }}'); $this->createPage('about'); @@ -75,14 +75,14 @@ public function index() $region = app(Session::class)->regions()->first(); // Initial response should be dynamic and not contain javascript. - $this->assertEquals('1 2', $response->getContent()); + $this->assertEquals('1 2', $response->getContent()); // The cached response should have the nocache placeholder, and the javascript. $this->assertTrue(file_exists($this->dir.'/about_.html')); - $this->assertEquals(vsprintf('1 %s%s', [ + $this->assertEquals(vsprintf('%s1 %s', [ + '', $region->key(), 'Loading...', - '', ]), file_get_contents($this->dir.'/about_.html')); } @@ -135,7 +135,7 @@ public function index() public function it_should_add_the_javascript_if_there_is_a_csrf_token() { $this->withFakeViews(); - $this->viewShouldReturnRaw('layout', '{{ template_content }}'); + $this->viewShouldReturnRaw('layout', '{{ template_content }}'); $this->viewShouldReturnRaw('default', '{{ csrf_token }}'); $this->createPage('about'); @@ -149,11 +149,11 @@ public function it_should_add_the_javascript_if_there_is_a_csrf_token() ->assertOk(); // Initial response should be dynamic and not contain javascript. - $this->assertEquals(''.csrf_token().'', $response->getContent()); + $this->assertEquals(''.csrf_token().'', $response->getContent()); // The cached response should have the token placeholder, and the javascript. $this->assertTrue(file_exists($this->dir.'/about_.html')); - $this->assertEquals(vsprintf('STATAMIC_CSRF_TOKEN%s', [ + $this->assertEquals(vsprintf('%sSTATAMIC_CSRF_TOKEN', [ '', ]), file_get_contents($this->dir.'/about_.html')); } From 465652bed912d0bc8b4bb935a13a328470613628 Mon Sep 17 00:00:00 2001 From: Damien HUTEAU Date: Wed, 25 Sep 2024 21:58:32 +0200 Subject: [PATCH 22/85] [5.x] Use existing getUrlsCacheKey method instead of duplicating the creation logic (#10836) Co-authored-by: Duncan McClean --- src/StaticCaching/Cachers/AbstractCacher.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/StaticCaching/Cachers/AbstractCacher.php b/src/StaticCaching/Cachers/AbstractCacher.php index eb125047d2..4f81543fd7 100644 --- a/src/StaticCaching/Cachers/AbstractCacher.php +++ b/src/StaticCaching/Cachers/AbstractCacher.php @@ -138,11 +138,9 @@ public function cacheDomain($domain = null) */ public function getUrls($domain = null) { - $domain = $domain ?: $this->getBaseUrl(); - - $domain = $this->makeHash($domain); + $key = $this->getUrlsCacheKey($domain); - return collect($this->cache->get($this->normalizeKey($domain.'.urls'), [])); + return collect($this->cache->get($key, [])); } /** From b206af951ea1f63b729559d681c6a4df73bb8229 Mon Sep 17 00:00:00 2001 From: Emmanuel Beauchamps Date: Wed, 25 Sep 2024 22:29:20 +0200 Subject: [PATCH 23/85] [5.x] French translations (#10839) --- resources/lang/fr.json | 19 +++++++++++++++++-- resources/lang/fr/messages.php | 1 + 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/resources/lang/fr.json b/resources/lang/fr.json index 2ae9a8a1c4..33a402bc10 100644 --- a/resources/lang/fr.json +++ b/resources/lang/fr.json @@ -10,6 +10,10 @@ ":count\/:total characters": "count caractères :count\/:total", ":file uploaded": ":file téléchargé", ":start-:end of :total": ":start-:end sur :total", + ":success\/:total entries were deleted": ":success\/:total entrées ont été suprimées", + ":success\/:total entries were published": ":success\/:total entrées ont été publiées", + ":success\/:total entries were unpublished": ":success\/:total entrées ont été dépubliées", + ":success\/:total items were deleted": ":success\/:total éléments ont été supprimés", ":title Field": "Champ :title", "A blueprint with that name already exists.": "Un Blueprint nommé ainsi existe déjà.", "A fieldset with that name already exists.": "Un jeu de champs nommé ainsi existe déjà.", @@ -389,6 +393,7 @@ "Email Address": "Adresse email", "Email Content": "Contenu de l'email", "Email Subject": "Sujet de l'email", + "Emojis": "Emojis", "Enable Input Rules": "Activer les règles de saisie", "Enable Line Wrapping": "Activer le retour à la ligne", "Enable Paste Rules": "Activer les règles de collage", @@ -399,15 +404,24 @@ "Enter any internal or external URL.": "Renseigner n'importe quel lien URL interne ou externe.", "Enter URL": "Renseigner l'URL", "Entries": "Entrées", + "Entries could not be deleted": "Les entrées n'ont pas pu être supprimées", + "Entries could not be published": "Les entrées n'ont pas pu être publiées", + "Entries could not be unpublished": "Les entrées n'ont pas pu être dépubliées", "Entries successfully reordered": "Entrées réorganisées avec succès", "Entry": "Entrée", + "Entry could not be deleted": "L'entrée n'a pas pu être supprimée", + "Entry could not be published": "L'entrée n'a pas pu être publiée", + "Entry could not be unpublished": "L'entrée n'a pas pu être dépubliée", "Entry created": "Entrée créée", "Entry deleted": "Entrée supprimée", + "Entry deleted|Entries deleted": "Entrée supprimée|Entrées supprimées", "Entry has a published version": "L'entrée a une version publiée", "Entry has not been published": "L'entrée n'a pas été publiée", "Entry has unpublished changes": "L'entrée a des modifications non publiées", "Entry link": "Lien vers l'entrée", + "Entry published|Entries published": "Entrée publiée|Entrées publiées", "Entry saved": "Entrée enregistrée", + "Entry unpublished|Entries unpublished": "Entrée dépubliée|Entrées dépubliées", "equals": "égal à", "Equals": "Egal à", "Escape Markup": "Echapper le balisage", @@ -534,7 +548,6 @@ "Insert Asset": "Insérer une ressource", "Insert Image": "Insérer une image", "Insert Link": "Insérer un lien", - "Install": "Installer", "Installed": "Installé", "Instructions": "Instructions", "Instructions Position": "Positionnement des instructions", @@ -546,6 +559,9 @@ "Is": "Est", "Isn't": "N'est pas", "Italic": "Italique", + "Item could not be deleted": "L'élément n'a pas pu être supprimé", + "Item deleted|Items deleted": "Elément supprimé|Eléments supprimés", + "Items could not be deleted": "Les éléments n'ont pas pu être supprimés", "Key": "Clé", "Key Mappings": "Mappages des touches", "Keyboard Shortcuts": "Raccourcis clavier", @@ -988,7 +1004,6 @@ "Unauthorized": "Non autorisé", "Uncheck All": "Décocher tout", "Underline": "Souligner", - "Uninstall": "Désinstaller", "Unlink": "Dissocier", "Unlisted Addons": "Addons non répertoriés", "Unordered List": "Liste non ordonnée", diff --git a/resources/lang/fr/messages.php b/resources/lang/fr/messages.php index e11a53f200..535b1b7c56 100644 --- a/resources/lang/fr/messages.php +++ b/resources/lang/fr/messages.php @@ -69,6 +69,7 @@ 'collections_route_instructions' => 'La route contrôle le modèle d’URL des entrées. Apprenez-en plus dans la [documentation](https://statamic.dev/collections#routing).', 'collections_sort_direction_instructions' => 'Le sens de tri par défaut.', 'collections_taxonomies_instructions' => 'Reliez les entrées de cette collection à des taxonomies. Les champs seront automatiquement ajoutés aux formulaires.', + 'dictionaries_countries_emojis_instructions' => 'Inclure les émojis de drapeaux dans les étiquettes ?', 'dictionaries_countries_region_instructions' => 'Filtrez éventuellement les pays par région.', 'duplicate_action_localizations_confirmation' => 'Êtes-vous sûr de vouloir effectuer cette action ? Les traductions seront également dupliquées.', 'duplicate_action_warning_localization' => 'Cette entrée est une traduction. L’entrée originale sera dupliquée.', From 5e7f3caac5c583581b0de6990090c4556985a0bb Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Wed, 25 Sep 2024 21:30:42 +0100 Subject: [PATCH 24/85] [5.x] Allow for large field configs in filters (#10822) --- resources/js/components/publish/FieldMeta.vue | 2 +- routes/cp.php | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/resources/js/components/publish/FieldMeta.vue b/resources/js/components/publish/FieldMeta.vue index e2d6623010..7c540b0ee2 100644 --- a/resources/js/components/publish/FieldMeta.vue +++ b/resources/js/components/publish/FieldMeta.vue @@ -63,7 +63,7 @@ export default { value: this.value, }; - this.$axios.get(cp_url('fields/field-meta'), { params }).then(response => { + this.$axios.post(cp_url('fields/field-meta'), params).then(response => { this.meta = response.data.meta; this.value = response.data.value; this.loading = false; diff --git a/routes/cp.php b/routes/cp.php index 075c9ff4ab..3048c10f64 100644 --- a/routes/cp.php +++ b/routes/cp.php @@ -252,6 +252,7 @@ Route::post('edit', [FieldsController::class, 'edit'])->name('fields.edit'); Route::post('update', [FieldsController::class, 'update'])->name('fields.update'); Route::get('field-meta', [MetaController::class, 'show']); + Route::post('field-meta', [MetaController::class, 'show']); Route::delete('fieldsets/{fieldset}/reset', [FieldsetController::class, 'reset'])->name('fieldsets.reset'); Route::resource('fieldsets', FieldsetController::class)->except(['show']); Route::get('blueprints', [BlueprintController::class, 'index'])->name('blueprints.index'); From 69c1fff12fdb33cd5613d76ec37254591008bbd0 Mon Sep 17 00:00:00 2001 From: Ryan Mitchell Date: Wed, 25 Sep 2024 22:38:41 +0100 Subject: [PATCH 25/85] [5.x] Add Nav & Collection Tree Saving events (#10625) Co-authored-by: Jason Varga --- resources/js/components/navigation/View.vue | 4 ++ .../js/components/structures/PageTree.vue | 4 ++ src/Events/CollectionTreeSaving.php | 23 +++++++++ src/Events/NavTreeSaving.php | 23 +++++++++ .../Collections/CollectionTreeController.php | 4 +- .../Navigation/NavigationTreeController.php | 3 +- src/Structures/CollectionTree.php | 6 +++ src/Structures/NavTree.php | 6 +++ src/Structures/Tree.php | 11 +++++ tests/Data/Structures/CollectionTreeTest.php | 48 ++++++++++++++----- tests/Data/Structures/NavTreeTest.php | 26 ++++++++++ 11 files changed, 144 insertions(+), 14 deletions(-) create mode 100644 src/Events/CollectionTreeSaving.php create mode 100644 src/Events/NavTreeSaving.php diff --git a/resources/js/components/navigation/View.vue b/resources/js/components/navigation/View.vue index c741272f5b..4681af6597 100644 --- a/resources/js/components/navigation/View.vue +++ b/resources/js/components/navigation/View.vue @@ -421,6 +421,10 @@ export default { }, treeSaved(response) { + if (! response.data.saved) { + return this.$toast.error(`Couldn't save tree`) + } + this.replaceGeneratedIds(response.data.generatedIds); this.changed = false; diff --git a/resources/js/components/structures/PageTree.vue b/resources/js/components/structures/PageTree.vue index 2170ee128d..a9c572dacc 100644 --- a/resources/js/components/structures/PageTree.vue +++ b/resources/js/components/structures/PageTree.vue @@ -207,6 +207,10 @@ export default { }; return this.$axios.patch(this.submitUrl, payload).then(response => { + if (! response.data.saved) { + return this.$toast.error(`Couldn't save tree`) + } + this.$emit('saved', response); this.$toast.success(__('Saved')); this.initialPages = this.pages; diff --git a/src/Events/CollectionTreeSaving.php b/src/Events/CollectionTreeSaving.php new file mode 100644 index 0000000000..02537e2f81 --- /dev/null +++ b/src/Events/CollectionTreeSaving.php @@ -0,0 +1,23 @@ +tree = $tree; + } + + /** + * 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/NavTreeSaving.php b/src/Events/NavTreeSaving.php new file mode 100644 index 0000000000..8b1f05b7cb --- /dev/null +++ b/src/Events/NavTreeSaving.php @@ -0,0 +1,23 @@ +tree = $tree; + } + + /** + * 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/Http/Controllers/CP/Collections/CollectionTreeController.php b/src/Http/Controllers/CP/Collections/CollectionTreeController.php index 5c848b4e19..fb8acc556c 100644 --- a/src/Http/Controllers/CP/Collections/CollectionTreeController.php +++ b/src/Http/Controllers/CP/Collections/CollectionTreeController.php @@ -46,7 +46,9 @@ public function update(Request $request, $collection) // if somehow the root would end up having child pages, which isn't allowed. $contents = $structure->validateTree($contents, $request->site); - $tree->tree($contents)->save(); + return [ + 'saved' => $tree->tree($contents)->save(), + ]; } private function toTree($items) diff --git a/src/Http/Controllers/CP/Navigation/NavigationTreeController.php b/src/Http/Controllers/CP/Navigation/NavigationTreeController.php index 7cecc54201..67c9a2f477 100644 --- a/src/Http/Controllers/CP/Navigation/NavigationTreeController.php +++ b/src/Http/Controllers/CP/Navigation/NavigationTreeController.php @@ -51,10 +51,11 @@ public function update(Request $request, $nav) $tree = $this->reorderTree($request->pages); - $nav->in($request->site)->tree($tree)->save(); + $saved = $nav->in($request->site)->tree($tree)->save(); return [ 'generatedIds' => $this->generatedIds, + 'saved' => $saved, ]; } diff --git a/src/Structures/CollectionTree.php b/src/Structures/CollectionTree.php index 3af66f3865..6213dd3c5b 100644 --- a/src/Structures/CollectionTree.php +++ b/src/Structures/CollectionTree.php @@ -7,6 +7,7 @@ use Statamic\Contracts\Structures\CollectionTreeRepository; use Statamic\Events\CollectionTreeDeleted; use Statamic\Events\CollectionTreeSaved; +use Statamic\Events\CollectionTreeSaving; use Statamic\Facades\Blink; use Statamic\Facades\Collection; use Statamic\Facades\Site; @@ -45,6 +46,11 @@ protected function dispatchSavedEvent() CollectionTreeSaved::dispatch($this); } + protected function dispatchSavingEvent() + { + return CollectionTreeSaving::dispatch($this); + } + protected function dispatchDeletedEvent() { CollectionTreeDeleted::dispatch($this); diff --git a/src/Structures/NavTree.php b/src/Structures/NavTree.php index 3dd7caba93..0faa887696 100644 --- a/src/Structures/NavTree.php +++ b/src/Structures/NavTree.php @@ -7,6 +7,7 @@ use Statamic\Contracts\Structures\NavTreeRepository; use Statamic\Events\NavTreeDeleted; use Statamic\Events\NavTreeSaved; +use Statamic\Events\NavTreeSaving; use Statamic\Facades\Blink; use Statamic\Facades\Nav; use Statamic\Facades\Site; @@ -43,6 +44,11 @@ protected function dispatchSavedEvent() NavTreeSaved::dispatch($this); } + protected function dispatchSavingEvent() + { + return NavTreeSaving::dispatch($this); + } + protected function dispatchDeletedEvent() { NavTreeDeleted::dispatch($this); diff --git a/src/Structures/Tree.php b/src/Structures/Tree.php index af7214fd90..1caff9d5d4 100644 --- a/src/Structures/Tree.php +++ b/src/Structures/Tree.php @@ -156,6 +156,10 @@ public function findByEntry($id) public function save() { + if ($this->dispatchSavingEvent() === false) { + return false; + } + $this->cachedFlattenedPages = null; Blink::forget('collection-structure-flattened-pages-collection*'); @@ -166,6 +170,8 @@ public function save() $this->dispatchSavedEvent(); $this->syncOriginal(); + + return true; } public function delete() @@ -186,6 +192,11 @@ protected function dispatchSavedEvent() // } + protected function dispatchSavingEvent() + { + // + } + protected function dispatchDeletedEvent() { // diff --git a/tests/Data/Structures/CollectionTreeTest.php b/tests/Data/Structures/CollectionTreeTest.php index d591862e99..e5c848e0a1 100644 --- a/tests/Data/Structures/CollectionTreeTest.php +++ b/tests/Data/Structures/CollectionTreeTest.php @@ -2,7 +2,9 @@ namespace Tests\Data\Structures; +use Illuminate\Support\Facades\Event; use PHPUnit\Framework\Attributes\Test; +use Statamic\Events\CollectionTreeSaving; use Statamic\Facades\Blink; use Statamic\Facades\Collection; use Statamic\Structures\CollectionTree; @@ -16,16 +18,6 @@ class CollectionTreeTest extends TestCase use PreventSavingStacheItemsToDisk; use UnlinksPaths; - private $directory; - - public function setUp(): void - { - parent::setUp(); - - $stache = $this->app->make('stache'); - $stache->store('collection-trees')->directory($this->directory = '/path/to/structures/collections'); - } - #[Test] public function it_can_get_and_set_the_handle() { @@ -61,7 +53,7 @@ public function it_gets_the_path() $collection = Collection::make('pages')->structureContents(['root' => true]); Collection::shouldReceive('findByHandle')->with('pages')->andReturn($collection); $tree = $collection->structure()->makeTree('en'); - $this->assertEquals('/path/to/structures/collections/pages.yaml', $tree->path()); + $this->assertEquals($this->fakeStacheDirectory.'/content/structures/collections/pages.yaml', $tree->path()); } #[Test] @@ -75,7 +67,7 @@ public function it_gets_the_path_when_using_multisite() $collection = Collection::make('pages')->structureContents(['root' => true]); Collection::shouldReceive('findByHandle')->with('pages')->andReturn($collection); $tree = $collection->structure()->makeTree('en'); - $this->assertEquals('/path/to/structures/collections/en/pages.yaml', $tree->path()); + $this->assertEquals($this->fakeStacheDirectory.'/content/structures/collections/en/pages.yaml', $tree->path()); } #[Test] @@ -118,4 +110,36 @@ public function it_does_a_diff() $this->assertEquals(['1.1', '2.2', '2.3'], $diff->moved()); $this->assertEquals(['1.1'], $diff->ancestryChanged()); } + + #[Test] + public function it_fires_a_saving_event() + { + Event::fake(); + + $collection = Collection::make('test')->structureContents(['root' => true]); + Collection::shouldReceive('findByHandle')->with('test')->andReturn($collection); + + $tree = $collection->structure()->makeTree('en'); + $tree->save(); + + Event::assertDispatched(CollectionTreeSaving::class); + + $this->assertFileExists($tree->path()); + } + + #[Test] + public function returning_false_in_collection_tree_saving_stops_saving() + { + Event::listen(CollectionTreeSaving::class, function (CollectionTreeSaving $event) { + return false; + }); + + $collection = Collection::make('test')->structureContents(['root' => true]); + Collection::shouldReceive('findByHandle')->with('test')->andReturn($collection); + + $tree = $collection->structure()->makeTree('en'); + $tree->save(); + + $this->assertFileDoesNotExist($tree->path()); + } } diff --git a/tests/Data/Structures/NavTreeTest.php b/tests/Data/Structures/NavTreeTest.php index 88d9e93972..7122e3ab50 100644 --- a/tests/Data/Structures/NavTreeTest.php +++ b/tests/Data/Structures/NavTreeTest.php @@ -3,7 +3,9 @@ namespace Tests\Data\Structures; use Facades\Statamic\Structures\BranchIds; +use Illuminate\Support\Facades\Event; use PHPUnit\Framework\Attributes\Test; +use Statamic\Events\NavTreeSaving; use Statamic\Facades\Blink; use Statamic\Facades\File; use Statamic\Facades\Nav; @@ -113,4 +115,28 @@ public function it_doesnt_save_tree_when_ensuring_ids_if_nothing_changed() $this->assertEquals($existingTree, $tree->tree()); $this->assertEquals($existingFileContents, File::get($tree->path())); } + + #[Test] + public function it_fires_a_saving_event() + { + Event::fake(); + + $nav = tap(Nav::make('links'))->save(); + tap($nav->makeTree('en', [['id' => 'the-id', 'title' => 'Branch']]))->save(); + + Event::assertDispatched(NavTreeSaving::class); + } + + #[Test] + public function returning_false_in_nav_tree_saving_stops_saving() + { + Event::listen(NavTreeSaving::class, function (NavTreeSaving $event) { + return false; + }); + + $nav = tap(Nav::make('links'))->save(); + $tree = tap($nav->makeTree('en', [['id' => 'the-id', 'title' => 'Branch']]))->save(); + + $this->assertFileDoesNotExist($tree->path()); + } } From 13329cdeea0b580f2be7e4b349835e381cb5fd2f Mon Sep 17 00:00:00 2001 From: Daniel Kaufmann Date: Thu, 26 Sep 2024 15:49:29 +0200 Subject: [PATCH 26/85] [5.x] German translations (#10849) --- resources/lang/de.json | 31 ++++++++++++++++++++++++++--- resources/lang/de/fieldtypes.php | 6 ++++++ resources/lang/de/messages.php | 2 ++ resources/lang/de/moment.php | 16 +++++++-------- resources/lang/de/validation.php | 2 +- resources/lang/de_CH.json | 29 +++++++++++++++++++++++++-- resources/lang/de_CH/fieldtypes.php | 6 ++++++ resources/lang/de_CH/messages.php | 2 ++ resources/lang/de_CH/moment.php | 16 +++++++-------- resources/lang/de_CH/validation.php | 2 +- 10 files changed, 89 insertions(+), 23 deletions(-) diff --git a/resources/lang/de.json b/resources/lang/de.json index 8626052c5b..4113c5ef78 100644 --- a/resources/lang/de.json +++ b/resources/lang/de.json @@ -10,6 +10,10 @@ ":count\/:total characters": ":count\/:total Zeichen", ":file uploaded": ":file hochgeladen", ":start-:end of :total": ":start–:end von :total", + ":success\/:total entries were deleted": ":success\/:total Einträge wurden gelöscht", + ":success\/:total entries were published": ":success\/:total Einträge wurden veröffentlicht", + ":success\/:total entries were unpublished": ":success\/:total Einträge wurden nicht veröffentlicht", + ":success\/:total items were deleted": ":success\/:total Einträge wurden gelöscht", ":title Field": ":title Feld", "A blueprint with that name already exists.": "Ein Blueprint mit dieser Bezeichnung gibt es bereits.", "A fieldset with that name already exists.": "Ein Fieldset mit dieser Bezeichnung gibt es bereits.", @@ -87,7 +91,7 @@ "and :count more": "und :count mehr", "Antlers": "Antlers", "Any of the following conditions pass": "eine der folgenden Bedingungen erfüllt ist", - "Any unsaved changes will not be duplicated into the new entry.": "Alle ungesicherten Änderungen werden nicht in den neuen Eintrag kopiert.", + "Any unsaved changes will not be duplicated into the new entry.": "Alle ungesicherten Änderungen werden nicht in den neuen Eintrag kopiert.", "Any unsaved changes will not be reflected in this action's behavior.": "Alle ungesicherten Änderungen werden bei dieser Aktion nicht berücksichtigt.", "Appearance": "Erscheinungsbild", "Appearance & Behavior": "Erscheinungsbild & Verhalten", @@ -111,6 +115,7 @@ "Are you sure you want to rename this asset?|Are you sure you want to rename these :count assets?": "Willst du diese Datei wirklich umbenennen?|Willst du diese :count Dateien wirklich umbenennen?", "Are you sure you want to rename this folder?|Are you sure you want to rename these :count folders?": "Willst du diesen Ordner wirklich umbenennen?|Willst du diese :count Ordner wirklich umbenennen?", "Are you sure you want to reset nav customizations?": "Willst du die Navigationsanpassungen wirklich zurücksetzen?", + "Are you sure you want to reset this item?": "Willst du dieses Element wirklich zurücksetzen?", "Are you sure you want to restore this revision?": "Willst du diese Überarbeitung wirklich wiederherstellen?", "Are you sure you want to run this action?|Are you sure you want to run this action on :count items?": "Willst du diese Aktion wirklich ausführen?|Willst du diese Aktion wirklich bei :count Einträgen ausführen?", "Are you sure you want to unpublish this entry?|Are you sure you want to unpublish these :count entries?": "Willst du diesen Eintrag wirklich nicht mehr veröffentlichen?|Willst du diese :count Einträge wirklich nicht mehr veröffentlichen?", @@ -154,10 +159,12 @@ "Blueprint": "Blueprint", "Blueprint created": "Blueprint erstellt", "Blueprint deleted": "Blueprint gelöscht", + "Blueprint reset": "Blueprint zurückgesetzt", "Blueprint saved": "Blueprint gespeichert", "Blueprints": "Blueprints", "Blueprints successfully reordered": "Blueprints erfolgreich umsortiert", "Bold": "Fett", + "Border": "Umrandung", "Boundaries": "Einschränkungen", "Browse": "Durchsuchen", "Build time": "Erstellungsdauer", @@ -238,6 +245,7 @@ "Copy": "Kopieren", "Copy password reset email for this user?": "E-Mail zum Zurücksetzen des Passworts für diese Benutzer:in kopieren?", "Copy Password Reset Link": "Passwort Zurücksetzen Link kopieren", + "Copy to clipboard": "in die Zwischenablage kopieren", "Copy URL": "URL kopieren", "Core": "Core", "Couldn't publish entry": "Eintrag konnte nicht veröffentlicht werden", @@ -325,6 +333,7 @@ "Description of the image": "Beschreibung des Bildes", "Deselect option": "Auswahl aufheben", "Detach": "Loslösen", + "Dictionary": "Wörterbuch", "Directory": "Verzeichnis", "Directory already exists.": "Verzeichnis existiert bereits.", "Disabled": "Deaktiviert", @@ -384,6 +393,7 @@ "Email Address": "E-Mail-Adresse", "Email Content": "E-Mail-Inhalt", "Email Subject": "E-Mail-Betreff", + "Emojis": "Emojis", "Enable Input Rules": "Eingaberegeln aktivieren", "Enable Line Wrapping": "Zeilenumbrüche aktivieren", "Enable Paste Rules": "Einfügeregeln aktivieren", @@ -394,21 +404,31 @@ "Enter any internal or external URL.": "Beliebige interne oder externe URL eingeben.", "Enter URL": "URL eingeben", "Entries": "Einträge", + "Entries could not be deleted": "Einträge konnten nicht gelöscht werden", + "Entries could not be published": "Einträge konnten nicht veröffentlicht werden", + "Entries could not be unpublished": "Einträge konnten nicht unveröffentlicht werden", "Entries successfully reordered": "Einträge erfolgreich umsortiert", "Entry": "Eintrag", + "Entry could not be deleted": "Eintrag konnte nicht gelöscht werden", + "Entry could not be published": "Eintrag konnte nicht veröffentlicht werden", + "Entry could not be unpublished": "Eintrag konnte nicht unveröffentlicht werden", "Entry created": "Eintrag erstellt", "Entry deleted": "Eintrag gelöscht", + "Entry deleted|Entries deleted": "Eintrag gelöscht|Einträge gelöscht", "Entry has a published version": "Eintrag hat eine veröffentlichte Version", "Entry has not been published": "Eintrag wurde nicht veröffentlicht", "Entry has unpublished changes": "Eintrag hat unveröffentlichte Änderungen", "Entry link": "Link zum Eintrag", + "Entry published|Entries published": "Eintrag veröffentlicht|Einträge veröffentlicht", "Entry saved": "Eintrag gespeichert", + "Entry unpublished|Entries unpublished": "Eintrag nicht veröffentlicht|Einträge nicht veröffentlicht", "equals": "ist gleich", "Equals": "Gleich", "Escape Markup": "Markup escapen", "Everything is up to date.": "Alles ist auf dem neusten Stand.", "Example": "Beispiel", "Exit Fullscreen Mode": "Vollbildmodus beenden", + "Expand": "Ausklappen", "Expand All": "Alle ausklappen", "Expand Set": "Set ausklappen", "Expand Sets": "Sets ausklappen", @@ -429,6 +449,7 @@ "Fieldset": "Fieldset", "Fieldset created": "Fieldset erstellt", "Fieldset deleted": "Fieldset gelöscht", + "Fieldset reset": "Fieldset zurückgesetzt", "Fieldset saved": "Fieldset gespeichert", "Fieldsets": "Fieldsets", "Fieldtypes": "Feldtypen", @@ -440,6 +461,7 @@ "Filter preset saved": "Filtervoreinstellung gespeichert", "Filter preset updated": "Filtervoreinstellung aktualisiert", "Finish": "Übernehmen", + "First child": "Erster Untereintrag", "First Child": "Erster Untereintrag", "Fixed": "Fixiert", "Floating": "Schwebend", @@ -526,7 +548,6 @@ "Insert Asset": "Datei einfügen", "Insert Image": "Bild einfügen", "Insert Link": "Link einfügen", - "Install": "Installieren", "Installed": "Installiert", "Instructions": "Beschreibung", "Instructions Position": "Position Beschreibung", @@ -538,6 +559,9 @@ "Is": "ist", "Isn't": "ist nicht", "Italic": "Kursiv", + "Item could not be deleted": "Element konnte nicht gelöscht werden", + "Item deleted|Items deleted": "Element gelöscht|Elemente gelöscht", + "Items could not be deleted": "Elemente konnten nicht gelöscht werden", "Key": "Schlüssel", "Key Mappings": "Tastaturbefehle", "Keyboard Shortcuts": "Tastaturkurzbefehle", @@ -733,6 +757,7 @@ "Regards": "Viele Grüße", "Regenerate": "Regenerieren", "Regenerate from: :field": "Regenerieren von: :field", + "Region": "Region", "Registration successful.": "Registrierung erfolgreich.", "Relationship": "Beziehung", "Released on :date": "Veröffentlicht am :date", @@ -762,6 +787,7 @@ "Require Slugs": "Erfordert Slugs", "Required": "Erforderlich", "Reset": "Zurücksetzen", + "Reset :resource": ":resource zurücksetzen", "Reset Nav Customizations": "Navigationsanpassungen zurücksetzen", "Reset Password": "Passwort zurücksetzen", "Responsive": "Responsiv", @@ -978,7 +1004,6 @@ "Unauthorized": "Keine Berechtigungen", "Uncheck All": "Alles abwählen", "Underline": "Unterstrichen", - "Uninstall": "Deinstallieren", "Unlink": "Verknüpfung aufheben", "Unlisted Addons": "Nicht aufgelistete Addons", "Unordered List": "Ungeordnete Liste", diff --git a/resources/lang/de/fieldtypes.php b/resources/lang/de/fieldtypes.php index 01619bff40..b81f543495 100644 --- a/resources/lang/de/fieldtypes.php +++ b/resources/lang/de/fieldtypes.php @@ -4,6 +4,7 @@ 'any.config.antlers' => 'Antlers-Parsing für den Inhalt dieses Feldes aktivieren.', 'any.config.cast_booleans' => 'Optionen mit den Werten *true* und *false* als Booleans speichern.', 'any.config.mode' => 'Bevorzugten UI-Stil wählen.', + 'array.config.expand' => 'Soll das Array im erweiterten Format gespeichert werden? Verwende diese Option, wenn du numerische Werte verwenden willst.', 'array.config.keys' => 'Arrayschlüssel (Variablen) und optionale Beschriftungen festlegen.', 'array.config.mode' => 'Der **dynamische** Modus gibt der Benutzer:in freie Kontrolle über die Daten, während der **schlüsselgebundene-** und der **Einzel**-Modus strenge Schlüssel vorschreiben.', 'array.title' => 'Array', @@ -72,6 +73,10 @@ 'date.config.time_enabled' => 'Aktiviert den Timepicker.', 'date.config.time_seconds_enabled' => 'Sekunden im Timepicker anzeigen.', 'date.title' => 'Datum', + 'dictionary.config.dictionary' => 'Das Wörterbuch, aus dem du Optionen abrufen möchtest.', + 'dictionary.file.config.filename' => 'Der Dateiname, der deine Optionen enthält, relativ zum `resources/dictionaries` Verzeichnis.', + 'dictionary.file.config.label' => 'Der Schlüssel, der die Bezeichnungen der Optionen enthält. Standardmäßig ist das `label`. Alternativ kannst du auch Antlers benutzen.', + 'dictionary.file.config.value' => 'Der Schlüssel, der die Werte der Optionen enthält. Standardmäßig ist dies `value`.', 'entries.config.collections' => 'Aus diesen Sammlungen kann die Benutzer:in auswählen.', 'entries.config.create' => 'Das Erstellen neuer Einträge zulassen.', 'entries.config.query_scopes' => 'Auswählen, welche Abfragebereiche beim Abrufen der auswählbaren Einträge angewendet werden sollen.', @@ -83,6 +88,7 @@ 'form.config.query_scopes' => 'Auswählen, welche Abfragebereiche beim Abrufen der auswählbaren Formulare angewendet werden sollen.', 'form.title' => 'Formular', 'grid.config.add_row' => 'Beschriftung des Buttons «Zeile hinzufügen» anpassen.', + 'grid.config.border' => 'Zeigt um die Felder in dieser Gruppe einen Rahmen und Padding an.', 'grid.config.fields' => 'Im Tabellenmodus wird jedes Feld zu einer Spalte.', 'grid.config.fullscreen' => 'Umschaltfunktion für den Vollbildmodus aktivieren.', 'grid.config.max_rows' => 'Maximale Anzahl möglicher Zeilen festlegen.', diff --git a/resources/lang/de/messages.php b/resources/lang/de/messages.php index 5c737e69c5..01fd3023bb 100644 --- a/resources/lang/de/messages.php +++ b/resources/lang/de/messages.php @@ -69,6 +69,8 @@ 'collections_route_instructions' => 'Die Route steuert das URL-Schema der Einträge. Weitere Infos dazu findest du in der [Dokumentation](https://statamic.dev/collections#routing).', 'collections_sort_direction_instructions' => 'Die voreingestellte Sortierrichtung.', 'collections_taxonomies_instructions' => 'Einträge in dieser Sammlung mit Taxonomien verknüpfen. Die Felder werden automatisch den Eingabemasken hinzugefügt.', + 'dictionaries_countries_emojis_instructions' => 'Flaggen-Emojis in die Beschriftung aufnehmen oder nicht.', + 'dictionaries_countries_region_instructions' => 'Optional die Länder nach Regionen filtern.', 'duplicate_action_localizations_confirmation' => 'Bist du sicher, dass du diese Aktion ausführen willst? Die Lokalisierungen werden ebenfalls dupliziert.', 'duplicate_action_warning_localization' => 'Dieser Eintrag ist eine Lokalisierung. Der Ursprungseintrag wird dupliziert.', 'duplicate_action_warning_localizations' => 'Ein oder mehrere ausgewählte Einträge sind Lokalisierungen. Es wird stattdessen der Ursprungseintrag dupliziert.', diff --git a/resources/lang/de/moment.php b/resources/lang/de/moment.php index c1c37433ca..396aa13a30 100644 --- a/resources/lang/de/moment.php +++ b/resources/lang/de/moment.php @@ -1,18 +1,18 @@ 'einem Monat', - 'relativeTime.MM' => '%d Monaten', - 'relativeTime.d' => 'einem Tag', - 'relativeTime.dd' => '%d Tagen', 'relativeTime.future' => 'in %s', - 'relativeTime.h' => 'einer Stunde', - 'relativeTime.hh' => '%d Stunden', - 'relativeTime.m' => 'einer Minute', - 'relativeTime.mm' => '%d Minuten', 'relativeTime.past' => 'vor %s', 'relativeTime.s' => 'ein paar Sekunden', 'relativeTime.ss' => '%d Sekunden', + 'relativeTime.m' => 'einer Minute', + 'relativeTime.mm' => '%d Minuten', + 'relativeTime.h' => 'einer Stunde', + 'relativeTime.hh' => '%d Stunden', + 'relativeTime.d' => 'einem Tag', + 'relativeTime.dd' => '%d Tagen', + 'relativeTime.M' => 'einem Monat', + 'relativeTime.MM' => '%d Monaten', 'relativeTime.y' => 'einem Jahr', 'relativeTime.yy' => '%d Jahren', ]; diff --git a/resources/lang/de/validation.php b/resources/lang/de/validation.php index 58775d5d63..13178b5252 100644 --- a/resources/lang/de/validation.php +++ b/resources/lang/de/validation.php @@ -130,7 +130,7 @@ 'date_fieldtype_time_required' => 'Eine Zeitangabe ist erforderlich.', 'duplicate_field_handle' => 'Ein Feld mit einem Handle :handle gibt es bereits.', 'duplicate_uri' => 'Doppelte URI :value', - 'email_available' => 'Eine Benutzer:in mit dieser E-Mailadresse existiert bereits.', + 'email_available' => 'Eine Benutzer:in mit dieser E-Mail-Adresse existiert bereits.', 'fieldset_imported_recursively' => 'Fieldset :handle wird rekursiv importiert.', 'one_site_without_origin' => 'Mindestens eine Website darf keine Quelle enthalten.', 'options_require_keys' => 'Alle Optionen müssen Schlüssel haben.', diff --git a/resources/lang/de_CH.json b/resources/lang/de_CH.json index 6b00b1b5b8..43ea1fa270 100644 --- a/resources/lang/de_CH.json +++ b/resources/lang/de_CH.json @@ -10,6 +10,10 @@ ":count\/:total characters": ":count\/:total Zeichen", ":file uploaded": ":file hochgeladen", ":start-:end of :total": ":start–:end von :total", + ":success\/:total entries were deleted": ":success\/:total Einträge wurden gelöscht", + ":success\/:total entries were published": ":success\/:total Einträge wurden veröffentlicht", + ":success\/:total entries were unpublished": ":success\/:total Einträge wurden nicht veröffentlicht", + ":success\/:total items were deleted": ":success\/:total Einträge wurden gelöscht", ":title Field": ":title Feld", "A blueprint with that name already exists.": "Ein Blueprint mit dieser Bezeichnung gibt es bereits.", "A fieldset with that name already exists.": "Ein Fieldset mit dieser Bezeichnung gibt es bereits.", @@ -111,6 +115,7 @@ "Are you sure you want to rename this asset?|Are you sure you want to rename these :count assets?": "Willst du diese Datei wirklich umbenennen?|Willst du diese :count Dateien wirklich umbenennen?", "Are you sure you want to rename this folder?|Are you sure you want to rename these :count folders?": "Willst du diesen Ordner wirklich umbenennen?|Willst du diese :count Ordner wirklich umbenennen?", "Are you sure you want to reset nav customizations?": "Willst du die Navigationsanpassungen wirklich zurücksetzen?", + "Are you sure you want to reset this item?": "Willst du dieses Element wirklich zurücksetzen?", "Are you sure you want to restore this revision?": "Willst du diese Überarbeitung wirklich wiederherstellen?", "Are you sure you want to run this action?|Are you sure you want to run this action on :count items?": "Willst du diese Aktion wirklich ausführen?|Willst du diese Aktion wirklich bei :count Einträgen ausführen?", "Are you sure you want to unpublish this entry?|Are you sure you want to unpublish these :count entries?": "Willst du diesen Eintrag wirklich nicht mehr veröffentlichen?|Willst du diese :count Einträge wirklich nicht mehr veröffentlichen?", @@ -154,10 +159,12 @@ "Blueprint": "Blueprint", "Blueprint created": "Blueprint erstellt", "Blueprint deleted": "Blueprint gelöscht", + "Blueprint reset": "Blueprint zurückgesetzt", "Blueprint saved": "Blueprint gespeichert", "Blueprints": "Blueprints", "Blueprints successfully reordered": "Blueprints erfolgreich umsortiert", "Bold": "Fett", + "Border": "Umrandung", "Boundaries": "Einschränkungen", "Browse": "Durchsuchen", "Build time": "Erstellungsdauer", @@ -238,6 +245,7 @@ "Copy": "Kopieren", "Copy password reset email for this user?": "E-Mail zum Zurücksetzen des Passworts für diese Benutzer:in kopieren?", "Copy Password Reset Link": "Passwort Zurücksetzen Link kopieren", + "Copy to clipboard": "in die Zwischenablage kopieren", "Copy URL": "URL kopieren", "Core": "Core", "Couldn't publish entry": "Eintrag konnte nicht veröffentlicht werden", @@ -325,6 +333,7 @@ "Description of the image": "Beschreibung des Bildes", "Deselect option": "Auswahl aufheben", "Detach": "Loslösen", + "Dictionary": "Wörterbuch", "Directory": "Verzeichnis", "Directory already exists.": "Verzeichnis existiert bereits.", "Disabled": "Deaktiviert", @@ -384,6 +393,7 @@ "Email Address": "E-Mail-Adresse", "Email Content": "E-Mail-Inhalt", "Email Subject": "E-Mail-Betreff", + "Emojis": "Emojis", "Enable Input Rules": "Eingaberegeln aktivieren", "Enable Line Wrapping": "Zeilenumbrüche aktivieren", "Enable Paste Rules": "Einfügeregeln aktivieren", @@ -394,21 +404,31 @@ "Enter any internal or external URL.": "Beliebige interne oder externe URL eingeben.", "Enter URL": "URL eingeben", "Entries": "Einträge", + "Entries could not be deleted": "Einträge konnten nicht gelöscht werden", + "Entries could not be published": "Einträge konnten nicht veröffentlicht werden", + "Entries could not be unpublished": "Einträge konnten nicht unveröffentlicht werden", "Entries successfully reordered": "Einträge erfolgreich umsortiert", "Entry": "Eintrag", + "Entry could not be deleted": "Eintrag konnte nicht gelöscht werden", + "Entry could not be published": "Eintrag konnte nicht veröffentlicht werden", + "Entry could not be unpublished": "Eintrag konnte nicht unveröffentlicht werden", "Entry created": "Eintrag erstellt", "Entry deleted": "Eintrag gelöscht", + "Entry deleted|Entries deleted": "Eintrag gelöscht|Einträge gelöscht", "Entry has a published version": "Eintrag hat eine veröffentlichte Version", "Entry has not been published": "Eintrag wurde nicht veröffentlicht", "Entry has unpublished changes": "Eintrag hat unveröffentlichte Änderungen", "Entry link": "Link zum Eintrag", + "Entry published|Entries published": "Eintrag veröffentlicht|Einträge veröffentlicht", "Entry saved": "Eintrag gespeichert", + "Entry unpublished|Entries unpublished": "Eintrag nicht veröffentlicht|Einträge nicht veröffentlicht", "equals": "ist gleich", "Equals": "Gleich", "Escape Markup": "Markup escapen", "Everything is up to date.": "Alles ist auf dem neusten Stand.", "Example": "Beispiel", "Exit Fullscreen Mode": "Vollbildmodus beenden", + "Expand": "Ausklappen", "Expand All": "Alle ausklappen", "Expand Set": "Set ausklappen", "Expand Sets": "Sets ausklappen", @@ -429,6 +449,7 @@ "Fieldset": "Fieldset", "Fieldset created": "Fieldset erstellt", "Fieldset deleted": "Fieldset gelöscht", + "Fieldset reset": "Fieldset zurückgesetzt", "Fieldset saved": "Fieldset gespeichert", "Fieldsets": "Fieldsets", "Fieldtypes": "Feldtypen", @@ -440,6 +461,7 @@ "Filter preset saved": "Filtervoreinstellung gespeichert", "Filter preset updated": "Filtervoreinstellung aktualisiert", "Finish": "Übernehmen", + "First child": "Erster Untereintrag", "First Child": "Erster Untereintrag", "Fixed": "Fixiert", "Floating": "Schwebend", @@ -526,7 +548,6 @@ "Insert Asset": "Datei einfügen", "Insert Image": "Bild einfügen", "Insert Link": "Link einfügen", - "Install": "Installieren", "Installed": "Installiert", "Instructions": "Beschreibung", "Instructions Position": "Position Beschreibung", @@ -538,6 +559,9 @@ "Is": "ist", "Isn't": "ist nicht", "Italic": "Kursiv", + "Item could not be deleted": "Element konnte nicht gelöscht werden", + "Item deleted|Items deleted": "Element gelöscht|Elemente gelöscht", + "Items could not be deleted": "Elemente konnten nicht gelöscht werden", "Key": "Schlüssel", "Key Mappings": "Tastaturbefehle", "Keyboard Shortcuts": "Tastaturkurzbefehle", @@ -733,6 +757,7 @@ "Regards": "Viele Grüsse", "Regenerate": "Regenerieren", "Regenerate from: :field": "Regenerieren von: :field", + "Region": "Region", "Registration successful.": "Registrierung erfolgreich.", "Relationship": "Beziehung", "Released on :date": "Veröffentlicht am :date", @@ -762,6 +787,7 @@ "Require Slugs": "Erfordert Slugs", "Required": "Erforderlich", "Reset": "Zurücksetzen", + "Reset :resource": ":resource zurücksetzen", "Reset Nav Customizations": "Navigationsanpassungen zurücksetzen", "Reset Password": "Passwort zurücksetzen", "Responsive": "Responsiv", @@ -978,7 +1004,6 @@ "Unauthorized": "Keine Berechtigungen", "Uncheck All": "Alles abwählen", "Underline": "Unterstrichen", - "Uninstall": "Deinstallieren", "Unlink": "Verknüpfung aufheben", "Unlisted Addons": "Nicht aufgelistete Addons", "Unordered List": "Ungeordnete Liste", diff --git a/resources/lang/de_CH/fieldtypes.php b/resources/lang/de_CH/fieldtypes.php index 11caa162c3..9b9f4eafa0 100644 --- a/resources/lang/de_CH/fieldtypes.php +++ b/resources/lang/de_CH/fieldtypes.php @@ -4,6 +4,7 @@ 'any.config.antlers' => 'Antlers-Parsing für den Inhalt dieses Feldes aktivieren.', 'any.config.cast_booleans' => 'Optionen mit den Werten *true* und *false* als Booleans speichern.', 'any.config.mode' => 'Bevorzugten UI-Stil wählen.', + 'array.config.expand' => 'Soll das Array im erweiterten Format gespeichert werden? Verwende diese Option, wenn du numerische Werte verwenden willst.', 'array.config.keys' => 'Arrayschlüssel (Variablen) und optionale Beschriftungen festlegen.', 'array.config.mode' => 'Der **dynamische** Modus gibt der Benutzer:in freie Kontrolle über die Daten, während der **schlüsselgebundene-** und der **Einzel**-Modus strenge Schlüssel vorschreiben.', 'array.title' => 'Array', @@ -72,6 +73,10 @@ 'date.config.time_enabled' => 'Aktiviert den Timepicker.', 'date.config.time_seconds_enabled' => 'Sekunden im Timepicker anzeigen.', 'date.title' => 'Datum', + 'dictionary.config.dictionary' => 'Das Wörterbuch, aus dem du Optionen abrufen möchtest.', + 'dictionary.file.config.filename' => 'Der Dateiname, der deine Optionen enthält, relativ zum `resources/dictionaries` Verzeichnis.', + 'dictionary.file.config.label' => 'Der Schlüssel, der die Bezeichnungen der Optionen enthält. Standardmässig ist das `label`. Alternativ kannst du auch Antlers benutzen.', + 'dictionary.file.config.value' => 'Der Schlüssel, der die Werte der Optionen enthält. Standardmässig ist dies `value`.', 'entries.config.collections' => 'Aus diesen Sammlungen kann die Benutzer:in auswählen.', 'entries.config.create' => 'Das Erstellen neuer Einträge zulassen.', 'entries.config.query_scopes' => 'Auswählen, welche Abfragebereiche beim Abrufen der auswählbaren Einträge angewendet werden sollen.', @@ -83,6 +88,7 @@ 'form.config.query_scopes' => 'Auswählen, welche Abfragebereiche beim Abrufen der auswählbaren Formulare angewendet werden sollen.', 'form.title' => 'Formular', 'grid.config.add_row' => 'Beschriftung des Buttons «Zeile hinzufügen» anpassen.', + 'grid.config.border' => 'Zeigt um die Felder in dieser Gruppe einen Rahmen und Padding an.', 'grid.config.fields' => 'Im Tabellenmodus wird jedes Feld zu einer Spalte.', 'grid.config.fullscreen' => 'Umschaltfunktion für den Vollbildmodus aktivieren.', 'grid.config.max_rows' => 'Maximale Anzahl möglicher Zeilen festlegen.', diff --git a/resources/lang/de_CH/messages.php b/resources/lang/de_CH/messages.php index 8acf0be978..9a8358ea44 100644 --- a/resources/lang/de_CH/messages.php +++ b/resources/lang/de_CH/messages.php @@ -69,6 +69,8 @@ 'collections_route_instructions' => 'Die Route steuert das URL-Schema der Einträge. Weitere Infos dazu findest du in der [Dokumentation](https://statamic.dev/collections#routing).', 'collections_sort_direction_instructions' => 'Die voreingestellte Sortierrichtung.', 'collections_taxonomies_instructions' => 'Einträge in dieser Sammlung mit Taxonomien verknüpfen. Die Felder werden automatisch den Eingabemasken hinzugefügt.', + 'dictionaries_countries_emojis_instructions' => 'Flaggen-Emojis in die Beschriftung aufnehmen oder nicht.', + 'dictionaries_countries_region_instructions' => 'Optional die Länder nach Regionen filtern.', 'duplicate_action_localizations_confirmation' => 'Bist du sicher, dass du diese Aktion ausführen willst? Die Lokalisierungen werden ebenfalls dupliziert.', 'duplicate_action_warning_localization' => 'Dieser Eintrag ist eine Lokalisierung. Der Ursprungseintrag wird dupliziert.', 'duplicate_action_warning_localizations' => 'Ein oder mehrere ausgewählte Einträge sind Lokalisierungen. Es wird stattdessen der Ursprungseintrag dupliziert.', diff --git a/resources/lang/de_CH/moment.php b/resources/lang/de_CH/moment.php index c1c37433ca..396aa13a30 100644 --- a/resources/lang/de_CH/moment.php +++ b/resources/lang/de_CH/moment.php @@ -1,18 +1,18 @@ 'einem Monat', - 'relativeTime.MM' => '%d Monaten', - 'relativeTime.d' => 'einem Tag', - 'relativeTime.dd' => '%d Tagen', 'relativeTime.future' => 'in %s', - 'relativeTime.h' => 'einer Stunde', - 'relativeTime.hh' => '%d Stunden', - 'relativeTime.m' => 'einer Minute', - 'relativeTime.mm' => '%d Minuten', 'relativeTime.past' => 'vor %s', 'relativeTime.s' => 'ein paar Sekunden', 'relativeTime.ss' => '%d Sekunden', + 'relativeTime.m' => 'einer Minute', + 'relativeTime.mm' => '%d Minuten', + 'relativeTime.h' => 'einer Stunde', + 'relativeTime.hh' => '%d Stunden', + 'relativeTime.d' => 'einem Tag', + 'relativeTime.dd' => '%d Tagen', + 'relativeTime.M' => 'einem Monat', + 'relativeTime.MM' => '%d Monaten', 'relativeTime.y' => 'einem Jahr', 'relativeTime.yy' => '%d Jahren', ]; diff --git a/resources/lang/de_CH/validation.php b/resources/lang/de_CH/validation.php index ec593f9cf5..162e2cf750 100644 --- a/resources/lang/de_CH/validation.php +++ b/resources/lang/de_CH/validation.php @@ -130,7 +130,7 @@ 'date_fieldtype_time_required' => 'Eine Zeitangabe ist erforderlich.', 'duplicate_field_handle' => 'Ein Feld mit einem Handle :handle gibt es bereits.', 'duplicate_uri' => 'Doppelte URI :value', - 'email_available' => 'Eine Benutzer:in mit dieser E-Mailadresse existiert bereits.', + 'email_available' => 'Eine Benutzer:in mit dieser E-Mail-Adresse existiert bereits.', 'fieldset_imported_recursively' => 'Fieldset :handle wird rekursiv importiert.', 'one_site_without_origin' => 'Mindestens eine Website darf keine Quelle enthalten.', 'options_require_keys' => 'Alle Optionen müssen Schlüssel haben.', From aff217e973ea345367d8d5f17bbf0c94aef5f136 Mon Sep 17 00:00:00 2001 From: Michael Aerni Date: Thu, 26 Sep 2024 14:40:26 -0400 Subject: [PATCH 27/85] [5.x] Fix textarea UI bug (#10850) --- resources/css/elements/forms.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/css/elements/forms.css b/resources/css/elements/forms.css index 649e26c284..49780b6b6f 100644 --- a/resources/css/elements/forms.css +++ b/resources/css/elements/forms.css @@ -24,7 +24,7 @@ select { ========================================================================== */ .input-text { - @apply appearance-none bg-gray-100 text-gray-800 border max-w-full w-full p-2 rounded shadow-inner placeholder:text-gray-600 text-md @lg:text-base; + @apply block appearance-none bg-gray-100 text-gray-800 border max-w-full w-full p-2 rounded shadow-inner placeholder:text-gray-600 text-md @lg:text-base; @apply dark:bg-dark-650 dark:text-dark-150 dark:border-dark-900 dark:placeholder:text-dark-175 dark:shadow-inner-dark; transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out; From e58f90b5df8033f32f0529ef15369cbdcdcc1f0d Mon Sep 17 00:00:00 2001 From: Sam Harvey <73826654+samharvey44@users.noreply.github.com> Date: Thu, 26 Sep 2024 21:02:20 +0100 Subject: [PATCH 28/85] [5.x] Fix User Accessor in Password Reset (#10848) Co-authored-by: Sam Harvey --- src/Http/Controllers/CP/Users/PasswordController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Http/Controllers/CP/Users/PasswordController.php b/src/Http/Controllers/CP/Users/PasswordController.php index d707891968..03abbda110 100644 --- a/src/Http/Controllers/CP/Users/PasswordController.php +++ b/src/Http/Controllers/CP/Users/PasswordController.php @@ -17,7 +17,7 @@ public function update(Request $request, $user) { throw_unless($user = User::find($user), new NotFoundHttpException); - $updatingOwnPassword = $user->id() == $request->user()->id(); + $updatingOwnPassword = $user->id() == User::fromUser($request->user())->id(); $this->authorize('editPassword', $user); From d475bf3cbdd4659c9b84cbed67748694cf826105 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Sep 2024 16:08:17 -0400 Subject: [PATCH 29/85] [5.x] Bump rollup from 3.29.4 to 3.29.5 (#10851) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index bf0f35503e..92be78620a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9032,9 +9032,9 @@ } }, "node_modules/rollup": { - "version": "3.29.4", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", - "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", + "version": "3.29.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz", + "integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==", "dev": true, "bin": { "rollup": "dist/bin/rollup" From dea9d7f2b50af65f5925c8d8e8eae6d4865b3e9e Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Thu, 26 Sep 2024 16:09:15 -0400 Subject: [PATCH 30/85] [5.x] Dynamic asset folders (#10808) Co-authored-by: Jack McDade --- .../css/components/fieldtypes/assets.css | 2 +- .../fieldtypes/assets/AssetsFieldtype.vue | 123 +++++++++++++++++- resources/lang/en/fieldtypes.php | 2 + src/Actions/RenameAssetFolder.php | 2 +- src/Fieldtypes/Assets/Assets.php | 85 +++++++++++- 5 files changed, 204 insertions(+), 10 deletions(-) diff --git a/resources/css/components/fieldtypes/assets.css b/resources/css/components/fieldtypes/assets.css index cc7ef23e83..3ba77ac311 100644 --- a/resources/css/components/fieldtypes/assets.css +++ b/resources/css/components/fieldtypes/assets.css @@ -7,7 +7,7 @@ } .assets-fieldtype .assets-fieldtype-picker { - @apply flex flex-col @sm:flex-row items-start @sm:items-center px-4 py-2 bg-gray-200 dark:bg-dark-650 border dark:border-dark-900 rounded; + @apply flex items-center px-4 py-2 bg-gray-200 dark:bg-dark-650 border dark:border-dark-900 rounded; &.is-expanded { @apply border-b-0 rounded-b-none; diff --git a/resources/js/components/fieldtypes/assets/AssetsFieldtype.vue b/resources/js/components/fieldtypes/assets/AssetsFieldtype.vue index 1d59e98826..b84d4bf0a7 100644 --- a/resources/js/components/fieldtypes/assets/AssetsFieldtype.vue +++ b/resources/js/components/fieldtypes/assets/AssetsFieldtype.vue @@ -1,6 +1,12 @@ diff --git a/resources/lang/ar.json b/resources/lang/ar.json index 37fb2a0316..01d5151115 100644 --- a/resources/lang/ar.json +++ b/resources/lang/ar.json @@ -302,7 +302,6 @@ "Delete": "حذف", "Delete :resource": "حذف :resource", "Delete child entry|Delete :count child entries": "حذف الإدخال الفرعي|حذف :count الإدخالات الفرعية", - "Delete Collection": "حذف المجموعة", "Delete Column": "حذف العمود", "Delete Container": "حذف الحاوية", "Delete Entry": "حذف الإدخال", diff --git a/resources/lang/az.json b/resources/lang/az.json index c33f32cf8d..bb0b3e1e70 100644 --- a/resources/lang/az.json +++ b/resources/lang/az.json @@ -318,7 +318,6 @@ "Delete": "Sil", "Delete :resource": "Bunu sil: :resource", "Delete child entry|Delete :count child entries": "Uşaq girişi sil|:count uşaq girişi sil", - "Delete Collection": "Kolleksiyanı Sil", "Delete Column": "Sütunu Sil", "Delete Container": "Konteyneri Sil", "Delete Entry": "Girişi Sil", diff --git a/resources/lang/cs.json b/resources/lang/cs.json index f1c23ee5e9..d73514ca14 100644 --- a/resources/lang/cs.json +++ b/resources/lang/cs.json @@ -227,7 +227,6 @@ "Delete": "Smazat", "Delete :resource": "Smazat :resource", "Delete child entry|Delete :count child entries": "Smazat podzáznam|Smazat :count podzáznamů", - "Delete Collection": "Smazat kolekci", "Delete Column": "Smazat sloupec", "Delete Container": "Smazat kontejner", "Delete Entry": "Smazat záznam", diff --git a/resources/lang/da.json b/resources/lang/da.json index cdd4711a9e..4ac8d38df2 100644 --- a/resources/lang/da.json +++ b/resources/lang/da.json @@ -211,7 +211,6 @@ "Delete": "Slet", "Delete :resource": "Slet :resource", "Delete child entry|Delete :count child entries": "Slet underordnet post | Slet :count underordnede poster", - "Delete Collection": "Slet samling", "Delete Column": "Slet kolonne", "Delete Container": "Slet beholder", "Delete Entry": "Slet post", diff --git a/resources/lang/de.json b/resources/lang/de.json index 4113c5ef78..30c229a406 100644 --- a/resources/lang/de.json +++ b/resources/lang/de.json @@ -313,7 +313,6 @@ "Delete": "Löschen", "Delete :resource": ":resource löschen", "Delete child entry|Delete :count child entries": "Untergeordneten Eintrag löschen|:count untergeordnete Einträge löschen", - "Delete Collection": "Sammlung löschen", "Delete Column": "Spalte löschen", "Delete Container": "Container löschen", "Delete Entry": "Eintrag löschen", diff --git a/resources/lang/de_CH.json b/resources/lang/de_CH.json index 43ea1fa270..e449b90ac7 100644 --- a/resources/lang/de_CH.json +++ b/resources/lang/de_CH.json @@ -313,7 +313,6 @@ "Delete": "Löschen", "Delete :resource": ":resource löschen", "Delete child entry|Delete :count child entries": "Untergeordneten Eintrag löschen|:count untergeordnete Einträge löschen", - "Delete Collection": "Sammlung löschen", "Delete Column": "Spalte löschen", "Delete Container": "Container löschen", "Delete Entry": "Eintrag löschen", diff --git a/resources/lang/es.json b/resources/lang/es.json index 17bdd4880b..7504854f4a 100644 --- a/resources/lang/es.json +++ b/resources/lang/es.json @@ -211,7 +211,6 @@ "Delete": "Eliminar", "Delete :resource": "Eliminar :resource", "Delete child entry|Delete :count child entries": "Eliminar entrada interna|Eliminar :count entradas internas", - "Delete Collection": "Eliminar colección", "Delete Column": "Eliminar columna", "Delete Container": "Eliminar contenedor", "Delete Entry": "Eliminar entrada", diff --git a/resources/lang/fa.json b/resources/lang/fa.json index 40275f32fc..9f561d5e19 100644 --- a/resources/lang/fa.json +++ b/resources/lang/fa.json @@ -329,7 +329,6 @@ "Delete": "حذف", "Delete :resource": "حذف :resource", "Delete child entry|Delete :count child entries": "حذف مطلب فرزند|حذف :count مطلب فرزند", - "Delete Collection": "حذف کالکشن", "Delete Column": "حذف ستون", "Delete Container": "حذف کانتینر", "Delete Entry": "حذف مطلب", diff --git a/resources/lang/fr.json b/resources/lang/fr.json index 25cc31dccd..3a69ed0f27 100644 --- a/resources/lang/fr.json +++ b/resources/lang/fr.json @@ -313,7 +313,6 @@ "Delete": "Supprimer", "Delete :resource": "Supprimer :resource", "Delete child entry|Delete :count child entries": "Supprimer l'entrée enfant|Supprimer :count entrées enfants", - "Delete Collection": "Supprimer la collection", "Delete Column": "Supprimer la colonne", "Delete Container": "Supprimer le conteneur", "Delete Entry": "Supprimer l'entrée", diff --git a/resources/lang/hu.json b/resources/lang/hu.json index b60cabfbab..b14f33a651 100644 --- a/resources/lang/hu.json +++ b/resources/lang/hu.json @@ -217,7 +217,6 @@ "Delete": "Töröl", "Delete :resource": ":resource törlése", "Delete child entry|Delete :count child entries": "Gyermek bejegyzés törlése|:count gyermek bejegyzés törlése", - "Delete Collection": "Gyűjtemény törlése", "Delete Column": "Oszlop törlése", "Delete Container": "Tároló törlése", "Delete Entry": "Bejegyzés törlése", diff --git a/resources/lang/id.json b/resources/lang/id.json index 90949b5d5b..dc1fb002e1 100644 --- a/resources/lang/id.json +++ b/resources/lang/id.json @@ -211,7 +211,6 @@ "Delete": "Hapus", "Delete :resource": "Hapus :resource", "Delete child entry|Delete :count child entries": "Hapus entri anak|Hapus :count entru anak", - "Delete Collection": "Hapus Koleksi", "Delete Column": "Hapus Kolom", "Delete Container": "Hapus Penampung", "Delete Entry": "Hapus Entri", diff --git a/resources/lang/it.json b/resources/lang/it.json index 76c22f3a4e..efbca93243 100644 --- a/resources/lang/it.json +++ b/resources/lang/it.json @@ -211,7 +211,6 @@ "Delete": "Elimina", "Delete :resource": "Elimina :resource", "Delete child entry|Delete :count child entries": "Elimina voce figlio|Elimina :count voci figlio", - "Delete Collection": "Elimina raccolta", "Delete Column": "Elimina colonna", "Delete Container": "Elimina contenitore", "Delete Entry": "Elimina voce", diff --git a/resources/lang/ja.json b/resources/lang/ja.json index d04901a8a1..cb2beb1f94 100644 --- a/resources/lang/ja.json +++ b/resources/lang/ja.json @@ -295,7 +295,6 @@ "Delete": "消去", "Delete :resource": "削除:resource", "Delete child entry|Delete :count child entries": "子エントリの削除| :countの子エントリを削除", - "Delete Collection": "コレクションの削除", "Delete Column": "列の削除", "Delete Container": "コンテナの削除", "Delete Entry": "記入を消す", diff --git a/resources/lang/ms.json b/resources/lang/ms.json index b5cd29eee5..d6b9f757f6 100644 --- a/resources/lang/ms.json +++ b/resources/lang/ms.json @@ -211,7 +211,6 @@ "Delete": "Hapus", "Delete :resource": "Hapus :resource", "Delete child entry|Delete :count child entries": "Hapus entri anak|Hapus :count entri anak", - "Delete Collection": "Hapus Koleksi", "Delete Column": "Hapus Lajur", "Delete Container": "Hapus Penampung", "Delete Entry": "Hapus Entri", diff --git a/resources/lang/nb.json b/resources/lang/nb.json index 612b44abdf..6baced9b84 100644 --- a/resources/lang/nb.json +++ b/resources/lang/nb.json @@ -302,7 +302,6 @@ "Delete": "Slett", "Delete :resource": "Slett :resource", "Delete child entry|Delete :count child entries": "Slett underordnet oppføring|Slett :count underordnede oppføringer", - "Delete Collection": "Slett samling", "Delete Column": "Slett kolonne", "Delete Container": "Slett beholder", "Delete Entry": "Slett oppføring", diff --git a/resources/lang/nl.json b/resources/lang/nl.json index c2e65ce364..8303f9fd63 100644 --- a/resources/lang/nl.json +++ b/resources/lang/nl.json @@ -302,7 +302,6 @@ "Delete": "Verwijderen", "Delete :resource": "Verwijder :resource", "Delete child entry|Delete :count child entries": "Verwijder child entry|Verwijder :count child entries", - "Delete Collection": "Verwijderd collectie", "Delete Column": "Verwijderd kolom", "Delete Container": "Verwijder container", "Delete Entry": "Verwijder entry", diff --git a/resources/lang/pl.json b/resources/lang/pl.json index 941bdd0ec6..fd4b11b4f9 100644 --- a/resources/lang/pl.json +++ b/resources/lang/pl.json @@ -229,7 +229,6 @@ "Delete": "Usuń", "Delete :resource": "Usuń :resource", "Delete child entry|Delete :count child entries": "Usuń podrzędnyrekord|Usuń :count rekordy/ów podrzędne", - "Delete Collection": "Usuń kolekcję", "Delete Column": "Usuń kolumnę", "Delete Container": "Usuń kontener", "Delete Entry": "Usuń rekord", diff --git a/resources/lang/pt.json b/resources/lang/pt.json index fc27d34556..d1c06c4902 100644 --- a/resources/lang/pt.json +++ b/resources/lang/pt.json @@ -211,7 +211,6 @@ "Delete": "Eliminar", "Delete :resource": "Eliminar :resource", "Delete child entry|Delete :count child entries": "Eliminar entrada-filho:Eliminar :count entrada(s)-filho(s)", - "Delete Collection": "Eliminar Coleção", "Delete Column": "Eliminar Coluna", "Delete Container": "Eliminar Biblioteca", "Delete Entry": "Eliminar Entrada", diff --git a/resources/lang/pt_BR.json b/resources/lang/pt_BR.json index a8829c24cd..56192a8d7c 100644 --- a/resources/lang/pt_BR.json +++ b/resources/lang/pt_BR.json @@ -237,7 +237,6 @@ "Delete": "Excluir", "Delete :resource": "Excluir :resource", "Delete child entry|Delete :count child entries": "Excluir entrada-filha|Excluir :count entradas-filhas", - "Delete Collection": "Excluir Coleção", "Delete Column": "Exlucir Coluna", "Delete Container": "Excluir Contêiner", "Delete Entry": "Excluir Entrada", diff --git a/resources/lang/ru.json b/resources/lang/ru.json index 6c8fdbc151..fcb3a6a09f 100644 --- a/resources/lang/ru.json +++ b/resources/lang/ru.json @@ -296,7 +296,6 @@ "Delete": "Удалить", "Delete :resource": "Удалить :resource", "Delete child entry|Delete :count child entries": "Удалить дочернюю запись|Удалить :count дочерние записи|Удалить :count дочерних записей", - "Delete Collection": "Удалить коллекцию", "Delete Column": "Удалить столбец", "Delete Container": "Удалить контейнер", "Delete Entry": "Удалить запись", diff --git a/resources/lang/sl.json b/resources/lang/sl.json index 6b82885b25..1a3c44e682 100644 --- a/resources/lang/sl.json +++ b/resources/lang/sl.json @@ -211,7 +211,6 @@ "Delete": "Izbriši", "Delete :resource": "Izbriši :resource", "Delete child entry|Delete :count child entries": "Izbriši podrejeni vnos | Izbriši :count podrejene vnose", - "Delete Collection": "Izbriši zbirko", "Delete Column": "Izbriši stolpec", "Delete Container": "Izbriši vsebnik", "Delete Entry": "Izbriši vnos", diff --git a/resources/lang/sv.json b/resources/lang/sv.json index 091f13c1d8..ba9474b068 100644 --- a/resources/lang/sv.json +++ b/resources/lang/sv.json @@ -229,7 +229,6 @@ "Delete": "Radera", "Delete :resource": "Radera :resource", "Delete child entry|Delete :count child entries": "Radera underordnat inlägg|Radera :count underordnade inlägg", - "Delete Collection": "Radera samling", "Delete Column": "Radera kolumn", "Delete Container": "Radera behållare", "Delete Entry": "Radera inlägg", diff --git a/resources/lang/tr.json b/resources/lang/tr.json index 5a0bb71881..83107a8b69 100644 --- a/resources/lang/tr.json +++ b/resources/lang/tr.json @@ -334,7 +334,6 @@ "Delete": "Sil", "Delete :resource": ":resource sil", "Delete child entry|Delete :count child entries": "Alt girdiyi sil|:count alt girdiyi sil", - "Delete Collection": "Koleksiyonu Sil", "Delete Column": "Sütunu Sil", "Delete Container": "Konteyneri Sil", "Delete Entry": "Girişi Sil", diff --git a/resources/lang/uk.json b/resources/lang/uk.json index c44acd9121..c9304347d1 100644 --- a/resources/lang/uk.json +++ b/resources/lang/uk.json @@ -295,7 +295,6 @@ "Delete": "Видалити", "Delete :resource": "Видалити :resource", "Delete child entry|Delete :count child entries": "Видалити дочірній запис|Видалити :count дочірніх записів", - "Delete Collection": "Видалити колекцію", "Delete Column": "Видалити стовпець", "Delete Container": "Видалити контейнер", "Delete Entry": "Видалити запис", diff --git a/resources/lang/zh_CN.json b/resources/lang/zh_CN.json index 0ee862b9a3..799f64b77f 100644 --- a/resources/lang/zh_CN.json +++ b/resources/lang/zh_CN.json @@ -236,7 +236,6 @@ "Delete": "删除", "Delete :resource": "删除 :resource", "Delete child entry|Delete :count child entries": "删除子条目|删除 :count 子条目", - "Delete Collection": "删除条目集合", "Delete Column": "删除列", "Delete Container": "删除容器", "Delete Entry": "删除条目", diff --git a/resources/lang/zh_TW.json b/resources/lang/zh_TW.json index af17f6caa9..c8d9be1409 100644 --- a/resources/lang/zh_TW.json +++ b/resources/lang/zh_TW.json @@ -236,7 +236,6 @@ "Delete": "刪除", "Delete :resource": "刪除 :resource", "Delete child entry|Delete :count child entries": "刪除子條目|刪除 :count 個子條目", - "Delete Collection": "刪除條目集", "Delete Column": "刪除欄位", "Delete Container": "刪除容器", "Delete Entry": "刪除條目", diff --git a/resources/views/collections/show.blade.php b/resources/views/collections/show.blade.php index 81d122a540..07a0916e25 100644 --- a/resources/views/collections/show.blade.php +++ b/resources/views/collections/show.blade.php @@ -53,17 +53,6 @@ :actions="{{ $actions }}" @completed="actionCompleted" > - @can('delete', $collection) - - - - @endcan - @endif diff --git a/src/Actions/Delete.php b/src/Actions/Delete.php index 4c6da3259f..446d8a2174 100644 --- a/src/Actions/Delete.php +++ b/src/Actions/Delete.php @@ -19,6 +19,7 @@ public function visibleTo($item) case $item instanceof Contracts\Entries\Entry && $item->collection()->sites()->count() === 1: return ! $item->page()?->isRoot(); break; + case $item instanceof Contracts\Entries\Collection: case $item instanceof Contracts\Taxonomies\Term: case $item instanceof Contracts\Assets\Asset: case $item instanceof Contracts\Assets\AssetFolder: @@ -52,6 +53,14 @@ public function confirmationText() return 'Are you sure you want to delete this?|Are you sure you want to delete these :count items?'; } + public function warningText() + { + if ($this->items->first() instanceof Contracts\Entries\Collection) { + /** @translation */ + return 'This will delete the collection and all of its entries.|This will delete the collections and all of their entries.'; + } + } + public function bypassesDirtyWarning(): bool { return true; @@ -85,6 +94,8 @@ public function redirect($items, $values) $item = $items->first(); switch (true) { + case $item instanceof Contracts\Entries\Collection: + return cp_route('collections.index'); case $item instanceof Contracts\Entries\Entry: return cp_route('collections.show', $item->collection()->handle()); case $item instanceof Contracts\Taxonomies\Term: diff --git a/src/Http/Controllers/CP/Collections/CollectionsController.php b/src/Http/Controllers/CP/Collections/CollectionsController.php index a435f4b2db..e482e809ba 100644 --- a/src/Http/Controllers/CP/Collections/CollectionsController.php +++ b/src/Http/Controllers/CP/Collections/CollectionsController.php @@ -121,7 +121,7 @@ public function show(Request $request, $collection) ->all(), 'canCreate' => User::current()->can('create', [EntryContract::class, $collection]) && $collection->hasVisibleEntryBlueprint(), 'canChangeLocalizationDeleteBehavior' => count($authorizedSites) > 1 && (count($authorizedSites) == $collection->sites()->count()), - 'actions' => Action::for($collection), + 'actions' => Action::for($collection, ['view' => 'form']), ]; if ($collection->queryEntries()->count() === 0) { diff --git a/tests/Feature/Collections/ViewCollectionListingTest.php b/tests/Feature/Collections/ViewCollectionListingTest.php index d1afb2bc82..7be7e695f6 100644 --- a/tests/Feature/Collections/ViewCollectionListingTest.php +++ b/tests/Feature/Collections/ViewCollectionListingTest.php @@ -44,7 +44,7 @@ public function it_shows_a_list_of_collections() 'editable' => true, 'blueprint_editable' => true, 'available_in_selected_site' => true, - 'actions' => collect(), + 'actions' => Facades\Action::for($collectionA, ['view' => 'list']), 'actions_url' => 'http://localhost/cp/collections/foo/actions', ], [ @@ -61,7 +61,7 @@ public function it_shows_a_list_of_collections() 'editable' => true, 'blueprint_editable' => true, 'available_in_selected_site' => true, - 'actions' => collect(), + 'actions' => Facades\Action::for($collectionB, ['view' => 'list']), 'actions_url' => 'http://localhost/cp/collections/bar/actions', ], ])) From c7e140eb46990f074e409483816d11c853795630 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Fri, 11 Oct 2024 12:54:43 -0400 Subject: [PATCH 75/85] [5.x] Improve UX of rename asset action (#10941) --- src/Actions/RenameAsset.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Actions/RenameAsset.php b/src/Actions/RenameAsset.php index 761b236b7d..e642a5f2ed 100644 --- a/src/Actions/RenameAsset.php +++ b/src/Actions/RenameAsset.php @@ -51,8 +51,10 @@ protected function fieldItems() 'validate' => 'required', // TODO: Better filename validation 'classes' => 'mousetrap', 'focus' => true, - 'placeholder' => $this->items->containsOneItem() ? $this->items->first()->filename() : null, + 'default' => $value = $this->items->containsOneItem() ? $this->items->first()->filename() : null, + 'placeholder' => $value, 'debounce' => false, + 'autoselect' => true, ], ]; } From ef4f7d73aead508b521280924d785d30da4acafd Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Fri, 11 Oct 2024 15:33:55 -0400 Subject: [PATCH 76/85] [5.x] Fix errors in upload queue (#10944) --- resources/js/components/assets/Uploader.vue | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/resources/js/components/assets/Uploader.vue b/resources/js/components/assets/Uploader.vue index 4c16383972..6f1f7d5001 100644 --- a/resources/js/components/assets/Uploader.vue +++ b/resources/js/components/assets/Uploader.vue @@ -170,9 +170,11 @@ export default { }, processUploadQueue() { - if (this.uploads.length === 0) return; + const uploads = this.uploads.filter(u => !u.errorMessage); - const upload = this.uploads[0]; + if (uploads.length === 0) return; + + const upload = uploads[0]; const id = upload.id; upload.instance.upload().then(response => { From 813d2cb95c3095710c0746aaef8bc68b2400fb5a Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Mon, 14 Oct 2024 15:25:20 +0100 Subject: [PATCH 77/85] [5.x] Prompt for license when installing starter kit (#10951) --- src/Console/Commands/StarterKitInstall.php | 2 +- src/StarterKits/LicenseManager.php | 22 +++++++++++++++++----- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/Console/Commands/StarterKitInstall.php b/src/Console/Commands/StarterKitInstall.php index d7db695ef2..cf3864745a 100644 --- a/src/Console/Commands/StarterKitInstall.php +++ b/src/Console/Commands/StarterKitInstall.php @@ -52,7 +52,7 @@ public function handle() return; } - $licenseManager = StarterKitLicenseManager::validate($package, $this->option('license'), $this); + $licenseManager = StarterKitLicenseManager::validate($package, $this->option('license'), $this, $this->input->isInteractive()); if (! $licenseManager->isValid()) { return; diff --git a/src/StarterKits/LicenseManager.php b/src/StarterKits/LicenseManager.php index f5d090af1b..9e1fa4bc04 100644 --- a/src/StarterKits/LicenseManager.php +++ b/src/StarterKits/LicenseManager.php @@ -6,6 +6,8 @@ use Illuminate\Support\Facades\Http; use Statamic\Console\NullConsole; +use function Laravel\Prompts\text; + final class LicenseManager { const OUTPOST_ENDPOINT = 'https://outpost.statamic.com/v3/starter-kits/'; @@ -13,25 +15,27 @@ final class LicenseManager private $package; private $licenseKey; private $console; + private $isInteractive; private $details; private $valid = false; /** * Instantiate starter kit license manager. */ - public function __construct(string $package, ?string $licenseKey = null, ?Command $console = null) + public function __construct(string $package, ?string $licenseKey = null, ?Command $console = null, bool $isInteractive = false) { $this->package = $package; $this->licenseKey = $licenseKey ?? config('statamic.system.license_key'); $this->console = $console ?? new NullConsole; + $this->isInteractive = $isInteractive; } /** * Instantiate starter kit license manager. */ - public static function validate(string $package, ?string $licenceKey = null, ?Command $console = null): self + public static function validate(string $package, ?string $licenceKey = null, ?Command $console = null, bool $isInteractive = false): self { - return (new self($package, $licenceKey, $console))->performValidation(); + return (new self($package, $licenceKey, $console, $isInteractive))->performValidation(); } /** @@ -74,10 +78,18 @@ private function performValidation(): self $marketplaceUrl = "https://statamic.com/starter-kits/{$sellerSlug}/{$kitSlug}"; if (! $this->licenseKey) { - return $this - ->error("License required for [{$this->package}]!") + if (! $this->isInteractive) { + return $this + ->error("License required for [{$this->package}]!") + ->comment('This is a paid starter kit. If you haven\'t already, you may purchase a license at:') + ->comment($marketplaceUrl); + } + + $this ->comment('This is a paid starter kit. If you haven\'t already, you may purchase a license at:') ->comment($marketplaceUrl); + + $this->licenseKey = text('Please enter your license key', required: true); } if ($this->outpostValidatesLicense()) { From 1348a922fe5c0ff552837f37cb7ac9ece243ff14 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Mon, 14 Oct 2024 15:25:45 +0100 Subject: [PATCH 78/85] [5.x] Tweak `make:` command descriptions (#10952) --- src/Console/Commands/MakeDictionary.php | 2 +- src/Console/Commands/MakeFieldtype.php | 2 +- src/Console/Commands/MakeFilter.php | 2 +- src/Console/Commands/MakeModifier.php | 2 +- src/Console/Commands/MakeScope.php | 2 +- src/Console/Commands/MakeTag.php | 2 +- src/Console/Commands/MakeWidget.php | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Console/Commands/MakeDictionary.php b/src/Console/Commands/MakeDictionary.php index f1a8618496..118c603bfc 100644 --- a/src/Console/Commands/MakeDictionary.php +++ b/src/Console/Commands/MakeDictionary.php @@ -22,7 +22,7 @@ class MakeDictionary extends GeneratorCommand * * @var string */ - protected $description = 'Create a new dictionary addon'; + protected $description = 'Create a new dictionary'; /** * The type of class being generated. diff --git a/src/Console/Commands/MakeFieldtype.php b/src/Console/Commands/MakeFieldtype.php index 48a588ff75..e245ce03f6 100644 --- a/src/Console/Commands/MakeFieldtype.php +++ b/src/Console/Commands/MakeFieldtype.php @@ -24,7 +24,7 @@ class MakeFieldtype extends GeneratorCommand * * @var string */ - protected $description = 'Create a new fieldtype addon'; + protected $description = 'Create a new fieldtype'; /** * The type of class being generated. diff --git a/src/Console/Commands/MakeFilter.php b/src/Console/Commands/MakeFilter.php index b753fa1b19..caed50c5e3 100644 --- a/src/Console/Commands/MakeFilter.php +++ b/src/Console/Commands/MakeFilter.php @@ -22,7 +22,7 @@ class MakeFilter extends GeneratorCommand * * @var string */ - protected $description = 'Create a new filter addon'; + protected $description = 'Create a new filter'; /** * The type of class being generated. diff --git a/src/Console/Commands/MakeModifier.php b/src/Console/Commands/MakeModifier.php index 96ef3e8a88..075d44bb97 100644 --- a/src/Console/Commands/MakeModifier.php +++ b/src/Console/Commands/MakeModifier.php @@ -22,7 +22,7 @@ class MakeModifier extends GeneratorCommand * * @var string */ - protected $description = 'Create a new modifier addon'; + protected $description = 'Create a new modifier'; /** * The type of class being generated. diff --git a/src/Console/Commands/MakeScope.php b/src/Console/Commands/MakeScope.php index 3fc770ad59..2dbc029474 100644 --- a/src/Console/Commands/MakeScope.php +++ b/src/Console/Commands/MakeScope.php @@ -22,7 +22,7 @@ class MakeScope extends GeneratorCommand * * @var string */ - protected $description = 'Create a new query scope addon'; + protected $description = 'Create a new query scope'; /** * The type of class being generated. diff --git a/src/Console/Commands/MakeTag.php b/src/Console/Commands/MakeTag.php index 92bb893816..086bcb689a 100644 --- a/src/Console/Commands/MakeTag.php +++ b/src/Console/Commands/MakeTag.php @@ -23,7 +23,7 @@ class MakeTag extends GeneratorCommand * * @var string */ - protected $description = 'Create a new tag addon'; + protected $description = 'Create a new tag'; /** * The type of class being generated. diff --git a/src/Console/Commands/MakeWidget.php b/src/Console/Commands/MakeWidget.php index 65ef362760..59f1695a62 100644 --- a/src/Console/Commands/MakeWidget.php +++ b/src/Console/Commands/MakeWidget.php @@ -23,7 +23,7 @@ class MakeWidget extends GeneratorCommand * * @var string */ - protected $description = 'Create a new widget addon'; + protected $description = 'Create a new widget'; /** * The type of class being generated. From 6be88fbf6368b82d2cd78de1056e3a822abebdff Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Mon, 14 Oct 2024 16:10:03 +0100 Subject: [PATCH 79/85] [5.x] Improve UX of rename asset folder action (#10950) Co-authored-by: Jason Varga --- src/Actions/RenameAssetFolder.php | 3 +++ src/Http/Controllers/CP/Assets/BrowserController.php | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Actions/RenameAssetFolder.php b/src/Actions/RenameAssetFolder.php index 2652d1f9bd..0b7c61c767 100644 --- a/src/Actions/RenameAssetFolder.php +++ b/src/Actions/RenameAssetFolder.php @@ -47,7 +47,10 @@ protected function fieldItems() 'validate' => ['required', 'string', new AlphaDashSpace], 'classes' => 'mousetrap', 'focus' => true, + 'default' => $value = $this->items->containsOneItem() ? $this->items->first()->basename() : null, + 'placeholder' => $value, 'debounce' => false, + 'autoselect' => true, ], ]; } diff --git a/src/Http/Controllers/CP/Assets/BrowserController.php b/src/Http/Controllers/CP/Assets/BrowserController.php index a7f6fbfb06..9d5493fcb8 100644 --- a/src/Http/Controllers/CP/Assets/BrowserController.php +++ b/src/Http/Controllers/CP/Assets/BrowserController.php @@ -112,7 +112,7 @@ public function folder(Request $request, $container, $path = '/') 'data' => [ 'assets' => FolderAsset::collection($assets ?? collect())->resolve(), 'folder' => array_merge((new Folder($folder))->resolve(), [ - 'folders' => $folders->values(), + 'folders' => Folder::collection($folders->values()), ]), ], 'links' => [ From ffe4cc197fa476adc4ca9a3d7cd8834d3ba25928 Mon Sep 17 00:00:00 2001 From: Philipp Daun Date: Mon, 14 Oct 2024 17:31:38 +0200 Subject: [PATCH 80/85] [5.x] Fix asset browser history navigation (#10948) --- resources/js/components/assets/Browser/Browser.vue | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/resources/js/components/assets/Browser/Browser.vue b/resources/js/components/assets/Browser/Browser.vue index c35b75c704..619c2ee1bf 100644 --- a/resources/js/components/assets/Browser/Browser.vue +++ b/resources/js/components/assets/Browser/Browser.vue @@ -453,6 +453,10 @@ export default { this.loadAssets(); }, + selectedPath(selectedPath) { + this.path = selectedPath; + }, + parameters(after, before) { if (this.initializing || JSON.stringify(before) === JSON.stringify(after)) return; this.loadAssets(); From 311fce1552a6b7c58aef4fc8e990125a061f6b10 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Mon, 14 Oct 2024 18:24:23 +0100 Subject: [PATCH 81/85] [5.x] Prevent timeout during `install:eloquent-driver` command (#10955) --- src/Console/Commands/InstallEloquentDriver.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Console/Commands/InstallEloquentDriver.php b/src/Console/Commands/InstallEloquentDriver.php index 3f41b2450c..31c4f85fc2 100644 --- a/src/Console/Commands/InstallEloquentDriver.php +++ b/src/Console/Commands/InstallEloquentDriver.php @@ -652,7 +652,7 @@ private function runArtisanCommand(string $command, bool $writeOutput = false): explode(' ', $command) ); - $result = Process::run($components, function ($type, $line) use ($writeOutput) { + $result = Process::forever()->run($components, function ($type, $line) use ($writeOutput) { if ($writeOutput) { $this->output->write($line); } From 9a966ea4b2cb4ee3646c420db63b793c7e8f193b Mon Sep 17 00:00:00 2001 From: Michael Aerni Date: Mon, 14 Oct 2024 13:44:49 -0400 Subject: [PATCH 82/85] [5.x] Fix issue if submitted password is null (#10945) --- src/Auth/Protect/Protectors/Password/Controller.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Auth/Protect/Protectors/Password/Controller.php b/src/Auth/Protect/Protectors/Password/Controller.php index dcc0d5fdf0..eac1f4b4fd 100644 --- a/src/Auth/Protect/Protectors/Password/Controller.php +++ b/src/Auth/Protect/Protectors/Password/Controller.php @@ -33,7 +33,7 @@ public function store() return back()->withErrors(['token' => __('statamic::messages.password_protect_token_invalid')], 'passwordProtect'); } - if (! $this->driver()->isValidPassword($this->password)) { + if (is_null($this->password) || ! $this->driver()->isValidPassword($this->password)) { return back()->withErrors(['password' => __('statamic::messages.password_protect_incorrect_password')], 'passwordProtect'); } From fa1a6a9ea3ae9487bfded0a9c269ab5fc96b25fe Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Mon, 14 Oct 2024 18:57:28 +0100 Subject: [PATCH 83/85] [5.x] `make:` commands dont need to register addon components (#10942) --- src/Console/Commands/MakeAction.php | 36 ------------------------- src/Console/Commands/MakeDictionary.php | 24 ----------------- src/Console/Commands/MakeFieldtype.php | 8 +----- src/Console/Commands/MakeFilter.php | 36 ------------------------- src/Console/Commands/MakeModifier.php | 36 ------------------------- src/Console/Commands/MakeScope.php | 36 ------------------------- src/Console/Commands/MakeTag.php | 36 ------------------------- src/Console/Commands/MakeWidget.php | 24 ----------------- 8 files changed, 1 insertion(+), 235 deletions(-) diff --git a/src/Console/Commands/MakeAction.php b/src/Console/Commands/MakeAction.php index 1e83c98ff7..03d8892fb7 100644 --- a/src/Console/Commands/MakeAction.php +++ b/src/Console/Commands/MakeAction.php @@ -2,8 +2,6 @@ namespace Statamic\Console\Commands; -use Archetype\Facades\PHPFile; -use PhpParser\BuilderFactory; use Statamic\Console\RunsInPlease; class MakeAction extends GeneratorCommand @@ -37,38 +35,4 @@ class MakeAction extends GeneratorCommand * @var string */ protected $stub = 'action.php.stub'; - - /** - * Execute the console command. - * - * @return bool|null - */ - public function handle() - { - if (parent::handle() === false) { - return false; - } - - if ($this->argument('addon')) { - $this->updateServiceProvider(); - } - } - - /** - * Update the Service Provider to register the Action component. - */ - protected function updateServiceProvider() - { - $factory = new BuilderFactory(); - - $actionClassValue = $factory->classConstFetch('Actions\\'.$this->getNameInput(), 'class'); - - try { - PHPFile::load("addons/{$this->package}/src/ServiceProvider.php") - ->add()->protected()->property('actions', $actionClassValue) - ->save(); - } catch (\Exception $e) { - $this->comment("Don't forget to register the Action class in your addon's service provider."); - } - } } diff --git a/src/Console/Commands/MakeDictionary.php b/src/Console/Commands/MakeDictionary.php index 118c603bfc..1e834da207 100644 --- a/src/Console/Commands/MakeDictionary.php +++ b/src/Console/Commands/MakeDictionary.php @@ -2,8 +2,6 @@ namespace Statamic\Console\Commands; -use Archetype\Facades\PHPFile; -use PhpParser\BuilderFactory; use Statamic\Console\RunsInPlease; class MakeDictionary extends GeneratorCommand @@ -48,27 +46,5 @@ public function handle() if (parent::handle() === false) { return false; } - - if ($this->argument('addon')) { - $this->updateServiceProvider(); - } - } - - /** - * Update the Service Provider to register dictionary components. - */ - protected function updateServiceProvider() - { - $factory = new BuilderFactory(); - - $dictionaryClassValue = $factory->classConstFetch('Dictionaries\\'.$this->getNameInput(), 'class'); - - try { - PHPFile::load("addons/{$this->package}/src/ServiceProvider.php") - ->add()->protected()->property('dictionaries', $dictionaryClassValue) - ->save(); - } catch (\Exception $e) { - $this->comment("Don't forget to register the Dictionary class in your addon's service provider."); - } } } diff --git a/src/Console/Commands/MakeFieldtype.php b/src/Console/Commands/MakeFieldtype.php index e245ce03f6..1ceacb2b0d 100644 --- a/src/Console/Commands/MakeFieldtype.php +++ b/src/Console/Commands/MakeFieldtype.php @@ -3,7 +3,6 @@ namespace Statamic\Console\Commands; use Archetype\Facades\PHPFile; -use PhpParser\BuilderFactory; use Statamic\Console\RunsInPlease; use Statamic\Support\Str; use Symfony\Component\Console\Input\InputOption; @@ -148,20 +147,15 @@ protected function wireUpAddonJs($addon) */ protected function updateServiceProvider() { - $factory = new BuilderFactory(); - - $fieldtypeClassValue = $factory->classConstFetch('Fieldtypes\\'.$this->getNameInput(), 'class'); - try { PHPFile::load("addons/{$this->package}/src/ServiceProvider.php") ->add()->protected()->property('vite', [ 'input' => ['resources/js/addon.js'], 'publicDirectory' => 'resources/dist', ]) - ->add()->protected()->property('fieldtypes', $fieldtypeClassValue) ->save(); } catch (\Exception $e) { - $this->comment("Don't forget to register the Fieldtype class and scripts in your addon's service provider."); + $this->comment("Don't forget to configure Vite in your addon's service provider."); } } diff --git a/src/Console/Commands/MakeFilter.php b/src/Console/Commands/MakeFilter.php index caed50c5e3..e9494be0af 100644 --- a/src/Console/Commands/MakeFilter.php +++ b/src/Console/Commands/MakeFilter.php @@ -2,8 +2,6 @@ namespace Statamic\Console\Commands; -use Archetype\Facades\PHPFile; -use PhpParser\BuilderFactory; use Statamic\Console\RunsInPlease; class MakeFilter extends GeneratorCommand @@ -37,38 +35,4 @@ class MakeFilter extends GeneratorCommand * @var string */ protected $stub = 'filter.php.stub'; - - /** - * Execute the console command. - * - * @return bool|null - */ - public function handle() - { - if (parent::handle() === false) { - return false; - } - - if ($this->argument('addon')) { - $this->updateServiceProvider(); - } - } - - /** - * Update the Service Provider to register the Filter component. - */ - protected function updateServiceProvider() - { - $factory = new BuilderFactory(); - - $filterClassValue = $factory->classConstFetch('Filters\\'.$this->getNameInput(), 'class'); - - try { - PHPFile::load("addons/{$this->package}/src/ServiceProvider.php") - ->add()->protected()->property('filters', $filterClassValue) - ->save(); - } catch (\Exception $e) { - $this->comment("Don't forget to register the Filter class in your addon's service provider."); - } - } } diff --git a/src/Console/Commands/MakeModifier.php b/src/Console/Commands/MakeModifier.php index 075d44bb97..8fb9011831 100644 --- a/src/Console/Commands/MakeModifier.php +++ b/src/Console/Commands/MakeModifier.php @@ -2,8 +2,6 @@ namespace Statamic\Console\Commands; -use Archetype\Facades\PHPFile; -use PhpParser\BuilderFactory; use Statamic\Console\RunsInPlease; class MakeModifier extends GeneratorCommand @@ -37,38 +35,4 @@ class MakeModifier extends GeneratorCommand * @var string */ protected $stub = 'modifier.php.stub'; - - /** - * Execute the console command. - * - * @return bool|null - */ - public function handle() - { - if (parent::handle() === false) { - return false; - } - - if ($this->argument('addon')) { - $this->updateServiceProvider(); - } - } - - /** - * Update the Service Provider to register fieldtype components. - */ - protected function updateServiceProvider() - { - $factory = new BuilderFactory(); - - $modifierClassValue = $factory->classConstFetch('Modifiers\\'.$this->getNameInput(), 'class'); - - try { - PHPFile::load("addons/{$this->package}/src/ServiceProvider.php") - ->add()->protected()->property('modifiers', $modifierClassValue) - ->save(); - } catch (\Exception $e) { - $this->comment("Don't forget to register the Modifier class in your addon's service provider."); - } - } } diff --git a/src/Console/Commands/MakeScope.php b/src/Console/Commands/MakeScope.php index 2dbc029474..a4268a8679 100644 --- a/src/Console/Commands/MakeScope.php +++ b/src/Console/Commands/MakeScope.php @@ -2,8 +2,6 @@ namespace Statamic\Console\Commands; -use Archetype\Facades\PHPFile; -use PhpParser\BuilderFactory; use Statamic\Console\RunsInPlease; class MakeScope extends GeneratorCommand @@ -37,38 +35,4 @@ class MakeScope extends GeneratorCommand * @var string */ protected $stub = 'scope.php.stub'; - - /** - * Execute the console command. - * - * @return bool|null - */ - public function handle() - { - if (parent::handle() === false) { - return false; - } - - if ($this->argument('addon')) { - $this->updateServiceProvider(); - } - } - - /** - * Update the Service Provider to register the scope component. - */ - protected function updateServiceProvider() - { - $factory = new BuilderFactory(); - - $scopeClassValue = $factory->classConstFetch('Scopes\\'.$this->getNameInput(), 'class'); - - try { - PHPFile::load("addons/{$this->package}/src/ServiceProvider.php") - ->add()->protected()->property('scopes', $scopeClassValue) - ->save(); - } catch (\Exception $e) { - $this->comment("Don't forget to register the Scope class in your addon's service provider."); - } - } } diff --git a/src/Console/Commands/MakeTag.php b/src/Console/Commands/MakeTag.php index 086bcb689a..01942692e5 100644 --- a/src/Console/Commands/MakeTag.php +++ b/src/Console/Commands/MakeTag.php @@ -2,8 +2,6 @@ namespace Statamic\Console\Commands; -use Archetype\Facades\PHPFile; -use PhpParser\BuilderFactory; use Statamic\Console\RunsInPlease; use Statamic\Support\Str; @@ -39,40 +37,6 @@ class MakeTag extends GeneratorCommand */ protected $stub = 'tag.php.stub'; - /** - * Execute the console command. - * - * @return bool|null - */ - public function handle() - { - if (parent::handle() === false) { - return false; - } - - if ($this->argument('addon')) { - $this->updateServiceProvider(); - } - } - - /** - * Update the Service Provider to register the Tag component. - */ - protected function updateServiceProvider() - { - $factory = new BuilderFactory(); - - $tagsClassValue = $factory->classConstFetch('Tags\\'.$this->getNameInput(), 'class'); - - try { - PHPFile::load("addons/{$this->package}/src/ServiceProvider.php") - ->add()->protected()->property('tags', $tagsClassValue) - ->save(); - } catch (\Exception $e) { - $this->comment("Don't forget to register the Tag class in your addon's service provider."); - } - } - /** * Build the class with the given name. * diff --git a/src/Console/Commands/MakeWidget.php b/src/Console/Commands/MakeWidget.php index 59f1695a62..d26da59f59 100644 --- a/src/Console/Commands/MakeWidget.php +++ b/src/Console/Commands/MakeWidget.php @@ -2,8 +2,6 @@ namespace Statamic\Console\Commands; -use Archetype\Facades\PHPFile; -use PhpParser\BuilderFactory; use Statamic\Console\RunsInPlease; use Statamic\Support\Str; @@ -51,10 +49,6 @@ public function handle() } $this->generateWidgetView(); - - if ($this->argument('addon')) { - $this->updateServiceProvider(); - } } /** @@ -80,24 +74,6 @@ protected function generateWidgetView() ); } - /** - * Update the Service Provider to register the widget component. - */ - protected function updateServiceProvider() - { - $factory = new BuilderFactory(); - - $widgetClassValue = $factory->classConstFetch('Widgets\\'.$this->getNameInput(), 'class'); - - try { - PHPFile::load("addons/{$this->package}/src/ServiceProvider.php") - ->add()->protected()->property('widgets', $widgetClassValue) - ->save(); - } catch (\Exception $e) { - $this->info("Don't forget to register the Widget class in your addon's service provider."); - } - } - /** * Build the class with the given name. * From ca2468bb1154794cb60b649a9a406c5ea8d8c337 Mon Sep 17 00:00:00 2001 From: Michael Aerni Date: Mon, 14 Oct 2024 14:12:52 -0400 Subject: [PATCH 84/85] [5.x] Make data of password-protected available in the view (#10946) --- src/Auth/Protect/Protectors/Password/Controller.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Auth/Protect/Protectors/Password/Controller.php b/src/Auth/Protect/Protectors/Password/Controller.php index eac1f4b4fd..afd3a7f9d0 100644 --- a/src/Auth/Protect/Protectors/Password/Controller.php +++ b/src/Auth/Protect/Protectors/Password/Controller.php @@ -17,11 +17,12 @@ public function show() { if ($this->tokenData = session('statamic:protect:password.tokens.'.request('token'))) { $site = Site::findByUrl($this->getUrl()); + $data = Data::find($this->tokenData['reference']); app()->setLocale($site->lang()); } - return View::make('statamic::auth.protect.password'); + return View::make('statamic::auth.protect.password')->cascadeContent($data ?? null); } public function store() From 741db252614f8b68a86eb408fb07d03e9666c351 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Mon, 14 Oct 2024 14:45:38 -0400 Subject: [PATCH 85/85] changelog --- CHANGELOG.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e6d1cfd7af..0689a23ed9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,34 @@ # Release Notes +## 5.31.0 (2024-10-14) + +### What's new +- Dictionary tag [#10885](https://github.com/statamic/cms/issues/10885) by @ryanmitchell +- Make data of password-protected available in the view [#10946](https://github.com/statamic/cms/issues/10946) by @aerni +- Prompt for license when installing starter kit [#10951](https://github.com/statamic/cms/issues/10951) by @duncanmcclean +- Add `taxonomy:count` tag [#10923](https://github.com/statamic/cms/issues/10923) by @aerni + +### What's fixed +- Improve UX of rename asset action [#10941](https://github.com/statamic/cms/issues/10941) by @jasonvarga +- Improve UX of rename asset folder action [#10950](https://github.com/statamic/cms/issues/10950) by @duncanmcclean +- Addon `make` commands no longer add to service providers since they are autoloaded [#10942](https://github.com/statamic/cms/issues/10942) by @duncanmcclean +- Tweak `make` command descriptions [#10952](https://github.com/statamic/cms/issues/10952) by @duncanmcclean +- Fix error if submitted password is null [#10945](https://github.com/statamic/cms/issues/10945) by @aerni +- Prevent timeout during `install:eloquent-driver` command [#10955](https://github.com/statamic/cms/issues/10955) by @duncanmcclean +- Fix asset browser history navigation [#10948](https://github.com/statamic/cms/issues/10948) by @daun +- Fix errors in upload queue [#10944](https://github.com/statamic/cms/issues/10944) by @jasonvarga +- Fix error when deleting collections [#10908](https://github.com/statamic/cms/issues/10908) by @duncanmcclean +- Fix ordering search results by date [#10939](https://github.com/statamic/cms/issues/10939) by @duncanmcclean +- Only show the sync/de-synced state for syncable nav fields [#10933](https://github.com/statamic/cms/issues/10933) by @duncanmcclean +- Ensure default values for globals are available in templates [#10909](https://github.com/statamic/cms/issues/10909) by @duncanmcclean +- Handle empty nodes in `bard_text` modifier [#10913](https://github.com/statamic/cms/issues/10913) by @ryanmitchell +- Use directory paths from stache config instead of static paths [#10914](https://github.com/statamic/cms/issues/10914) by @Alpenverein +- Improvements to the `install:eloquent-driver` command [#10910](https://github.com/statamic/cms/issues/10910) by @duncanmcclean +- Check site requested when using global route binding on api routes [#10894](https://github.com/statamic/cms/issues/10894) by @ryanmitchell +- Update "Bug Report" issue template [#10918](https://github.com/statamic/cms/issues/10918) by @duncanmcclean + + + ## 5.30.0 (2024-10-03) ### What's new