diff --git a/resources/js/components/entries/PublishForm.vue b/resources/js/components/entries/PublishForm.vue index 65af14be295..bafa4470137 100644 --- a/resources/js/components/entries/PublishForm.vue +++ b/resources/js/components/entries/PublishForm.vue @@ -476,7 +476,7 @@ export default { }, showLivePreviewButton() { - return !this.isPreviewing && !this.readOnly && !this.isCreating && this.isBase && this.livePreviewUrl; + return !this.isPreviewing && !this.readOnly && this.isBase && this.livePreviewUrl; }, showVisitUrlButton() { diff --git a/src/Entries/Entry.php b/src/Entries/Entry.php index bdb436155e9..471e2fed7cc 100644 --- a/src/Entries/Entry.php +++ b/src/Entries/Entry.php @@ -1044,6 +1044,10 @@ public function routeData() public function uri() { + if (! $this->id() && $uri = $this->getSupplement('live_preview_uri')) { + return $uri; + } + if ($this->id() && Blink::store('entry-uris')->has($this->id())) { return Blink::store('entry-uris')->get($this->id()); } diff --git a/src/Http/Controllers/CP/Collections/EntriesController.php b/src/Http/Controllers/CP/Collections/EntriesController.php index d1a7a7fd4b7..50bad1780f4 100644 --- a/src/Http/Controllers/CP/Collections/EntriesController.php +++ b/src/Http/Controllers/CP/Collections/EntriesController.php @@ -11,7 +11,6 @@ use Statamic\Facades\Blink; use Statamic\Facades\Entry; use Statamic\Facades\Site; -use Statamic\Facades\Stache; use Statamic\Facades\User; use Statamic\Hooks\CP\EntriesIndexQuery; use Statamic\Http\Controllers\CP\CpController; @@ -31,7 +30,8 @@ class EntriesController extends CpController { use ExtractsFromEntryFields, Hookable, - QueriesFilters; + QueriesFilters, + ResolvesEntryUri; public function index(FilteredRequest $request, $collection) { @@ -346,7 +346,7 @@ public function create(Request $request, $collection, $site) 'blueprint' => $blueprint->toPublishArray(), 'published' => $collection->defaultPublishState(), 'locale' => $site->handle(), - 'localizations' => $this->getAuthorizedSitesForCollection($collection)->map(function ($handle) use ($collection, $site, $blueprint) { + 'localizations' => $this->getAuthorizedSitesForCollection($collection)->map(function ($handle) use ($collection, $site, $blueprint, $request) { return [ 'handle' => $handle, 'name' => Site::get($handle)->name(), @@ -354,7 +354,7 @@ public function create(Request $request, $collection, $site) 'exists' => false, 'published' => false, 'url' => cp_route('collections.entries.create', [$collection->handle(), $handle, 'blueprint' => $blueprint->handle()]), - 'livePreviewUrl' => $collection->route($handle) ? cp_route('collections.entries.preview.create', [$collection->handle(), $handle]) : null, + 'livePreviewUrl' => $collection->route($handle) ? cp_route('collections.entries.preview.create', array_filter([$collection->handle(), $handle, 'parent' => $collection->hasStructure() ? $request->parent : null])) : null, ]; })->values()->all(), 'revisionsEnabled' => $collection->revisionsEnabled(), @@ -486,38 +486,6 @@ private function validateUniqueUri($entry, $tree, $parent) throw ValidationException::withMessages(['slug' => __('statamic::validation.unique_uri')]); } - private function entryUri($entry, $tree, $parent) - { - if (! $entry->route()) { - return null; - } - - if (! $tree) { - return app(\Statamic\Contracts\Routing\UrlBuilder::class) - ->content($entry) - ->merge([ - 'id' => $entry->id() ?? Stache::generateId(), - ]) - ->build($entry->route()); - } - - $parent = $parent ? $tree->find($parent) : null; - - if ($parent && $parent->isRoot()) { - $parent = null; - } - - return app(\Statamic\Contracts\Routing\UrlBuilder::class) - ->content($entry) - ->merge([ - 'parent_uri' => $parent ? $parent->uri() : null, - 'slug' => $entry->slug(), - 'depth' => $parent ? $parent->depth() + 1 : 1, - 'is_root' => false, - ]) - ->build($entry->route()); - } - protected function getAuthorizedSitesForCollection($collection) { return $collection diff --git a/src/Http/Controllers/CP/Collections/EntryPreviewController.php b/src/Http/Controllers/CP/Collections/EntryPreviewController.php index 8d56767a09f..d9d8e1d5da4 100644 --- a/src/Http/Controllers/CP/Collections/EntryPreviewController.php +++ b/src/Http/Controllers/CP/Collections/EntryPreviewController.php @@ -6,29 +6,40 @@ use Statamic\Contracts\Entries\Entry as EntryContract; use Statamic\Facades\Entry; use Statamic\Http\Controllers\CP\PreviewController; -use Statamic\Support\Arr; class EntryPreviewController extends PreviewController { + use ResolvesEntryUri; + public function create(Request $request, $collection, $site) { $this->authorize('create', [EntryContract::class, $collection]); - $fields = $collection->entryBlueprint($request->blueprint) + $blueprint = $collection->entryBlueprint($request->blueprint); + + $values = $blueprint ->fields() ->addValues($preview = $request->preview) - ->process(); - - $values = Arr::except($fields->values()->all(), ['slug']); + ->process() + ->values() + ->except(['slug']); $entry = Entry::make() ->slug($preview['slug'] ?? 'slug') ->collection($collection) - ->locale($site->handle()) - ->data($values); + ->locale($site->handle()); if ($collection->dated()) { - $entry->date($preview['date'] ?? now()->format('Y-m-d-His')); + $date = $values->pull('date'); + $entry->date(($date ? $blueprint->field('date')?->fieldtype()->augment($date) : null) ?? now()); + } + + $entry->data($values->all()); + + // An entry that hasn't been saved isn't in the tree, so it has no URI + // of its own. Give it the one it will have under its parent page. + if ($structure = $collection->structure()) { + $entry->setSupplement('live_preview_uri', $this->entryUri($entry, $structure->in($site->handle()), $request->parent)); } return $this->tokenizeAndReturn($request, $entry); diff --git a/src/Http/Controllers/CP/Collections/ResolvesEntryUri.php b/src/Http/Controllers/CP/Collections/ResolvesEntryUri.php new file mode 100644 index 00000000000..3d4ba6618a5 --- /dev/null +++ b/src/Http/Controllers/CP/Collections/ResolvesEntryUri.php @@ -0,0 +1,45 @@ +route()) { + return null; + } + + if (! $tree) { + return app(UrlBuilder::class) + ->content($entry) + ->merge([ + 'id' => $entry->id() ?? Stache::generateId(), + ]) + ->build($entry->route()); + } + + $parent = $parent ? $tree->find($parent) : null; + + if ($parent && $parent->isRoot()) { + $parent = null; + } + + return app(UrlBuilder::class) + ->content($entry) + ->merge([ + 'parent_uri' => $parent ? $parent->uri() : null, + 'slug' => $entry->slug(), + 'depth' => $parent ? $parent->depth() + 1 : 1, + 'is_root' => false, + ]) + ->build($entry->route()); + } +} diff --git a/src/Stache/Repositories/EntryRepository.php b/src/Stache/Repositories/EntryRepository.php index 37c896b2a81..9c4e06b1690 100644 --- a/src/Stache/Repositories/EntryRepository.php +++ b/src/Stache/Repositories/EntryRepository.php @@ -188,8 +188,13 @@ public static function bindings(): array public function substitute($item) { - Blink::store('entry-uris')->forget($item->id()); - $this->substitutionsById[$item->id()] = $item; + // An entry being previewed before its first save has no id yet, + // so it can only be found by its URI. + if ($id = $item->id()) { + Blink::store('entry-uris')->forget($id); + $this->substitutionsById[$id] = $item; + } + $this->substitutionsByUri[$item->locale().'@'.$item->uri()] = $item; } diff --git a/tests/Feature/Entries/CreateEntryTest.php b/tests/Feature/Entries/CreateEntryTest.php index 9f1bdde5177..b1b43e0158f 100644 --- a/tests/Feature/Entries/CreateEntryTest.php +++ b/tests/Feature/Entries/CreateEntryTest.php @@ -52,6 +52,38 @@ public function it_populates_the_form_from_query_string() ); } + #[Test] + public function the_live_preview_url_keeps_the_parent_of_a_structured_entry() + { + $this->setTestRoles(['test' => ['access cp', 'create test entries']]); + $user = tap(User::make()->assignRole('test'))->save(); + Collection::make('test')->routes('{parent_uri}/{slug}')->structureContents(['max_depth' => 10])->save(); + + $this + ->actingAs($user) + ->get(cp_route('collections.entries.create', ['test', 'en']).'?parent=the-parent') + ->assertOk() + ->assertInertia(fn ($page) => $page + ->where('localizations.0.livePreviewUrl', cp_route('collections.entries.preview.create', ['test', 'en', 'parent' => 'the-parent'])) + ); + } + + #[Test] + public function the_live_preview_url_has_no_parent_for_a_collection_without_a_structure() + { + $this->setTestRoles(['test' => ['access cp', 'create test entries']]); + $user = tap(User::make()->assignRole('test'))->save(); + Collection::make('test')->routes('/test/{slug}')->save(); + + $this + ->actingAs($user) + ->get(cp_route('collections.entries.create', ['test', 'en']).'?parent=the-parent') + ->assertOk() + ->assertInertia(fn ($page) => $page + ->where('localizations.0.livePreviewUrl', cp_route('collections.entries.preview.create', ['test', 'en'])) + ); + } + #[Test] public function the_publish_state_cannot_be_managed_without_permission() { diff --git a/tests/Feature/Entries/LivePreviewNewEntryTest.php b/tests/Feature/Entries/LivePreviewNewEntryTest.php new file mode 100644 index 00000000000..57bbac1bfec --- /dev/null +++ b/tests/Feature/Entries/LivePreviewNewEntryTest.php @@ -0,0 +1,114 @@ +withFakeViews(); + $this->viewShouldReturnRaw('layout', '{{ template_content }}'); + $this->viewShouldReturnRaw('entry', '