From 9d92554987623e64a4b41fb653228a029133856d Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Fri, 7 May 2021 15:31:56 -0400 Subject: [PATCH 01/23] when saving an entry, append the id the filename if there's a duplicate --- src/Data/ExistsAsFile.php | 9 ++- src/Entries/Entry.php | 5 ++ src/Stache/Stores/CollectionEntriesStore.php | 18 +++++ tests/Stache/Stores/EntriesStoreTest.php | 71 ++++++++++++++++++++ 4 files changed, 101 insertions(+), 2 deletions(-) diff --git a/src/Data/ExistsAsFile.php b/src/Data/ExistsAsFile.php index 9ede14e8eaa..db18fb1c7f1 100644 --- a/src/Data/ExistsAsFile.php +++ b/src/Data/ExistsAsFile.php @@ -13,6 +13,11 @@ trait ExistsAsFile abstract public function path(); + protected function buildPath() + { + return $this->path(); + } + public function initialPath($path = null) { if (func_num_args() === 0) { @@ -77,9 +82,9 @@ public function fileExtension() return 'yaml'; } - public function writeFile() + public function writeFile($path = null) { - $path = $this->path(); + $path = $path ?? $this->buildPath(); $initial = $this->initialPath(); if ($initial && $path !== $initial) { diff --git a/src/Entries/Entry.php b/src/Entries/Entry.php index d30bd6af83a..e0f14cedb60 100644 --- a/src/Entries/Entry.php +++ b/src/Entries/Entry.php @@ -335,6 +335,11 @@ public function taxonomize() } public function path() + { + return $this->initialPath ?? $this->buildPath(); + } + + protected function buildPath() { $prefix = ''; diff --git a/src/Stache/Stores/CollectionEntriesStore.php b/src/Stache/Stores/CollectionEntriesStore.php index 58c6800baee..4ea15f842b4 100644 --- a/src/Stache/Stores/CollectionEntriesStore.php +++ b/src/Stache/Stores/CollectionEntriesStore.php @@ -5,6 +5,7 @@ use Statamic\Entries\GetDateFromPath; use Statamic\Facades\Collection; use Statamic\Facades\Entry; +use Statamic\Facades\File; use Statamic\Facades\Path; use Statamic\Facades\Site; use Statamic\Facades\YAML; @@ -165,4 +166,21 @@ protected function storeIndexes() $collection->taxonomies()->map->handle() )->all(); } + + protected function writeItemToDisk($item) + { + if (! $contents = File::get($path = $item->path())) { + return $item->writeFile(); + } + + $itemFromDisk = $this->makeItemFromFile($path, $contents); + + if ($item->id() !== $itemFromDisk->id()) { + $ext = '.'.$item->fileExtension(); + $filename = Str::before($path, $ext); + $path = "{$filename}.{$item->id()}{$ext}"; + } + + $item->writeFile($path); + } } diff --git a/tests/Stache/Stores/EntriesStoreTest.php b/tests/Stache/Stores/EntriesStoreTest.php index 8e3f3722f59..1e4aa7b748b 100644 --- a/tests/Stache/Stores/EntriesStoreTest.php +++ b/tests/Stache/Stores/EntriesStoreTest.php @@ -130,6 +130,77 @@ public function it_saves_to_disk() $this->assertEquals($path, $this->parent->store('blog')->paths()->get('123')); } + /** @test */ + public function it_appends_the_id_to_the_filename_if_one_already_exists() + { + $existingPath = $this->directory.'/blog/2017-07-04.test.md'; + file_put_contents($existingPath, $existingContents = "---\nid: existing-id\n---"); + + $entry = Facades\Entry::make() + ->id('new-id') + ->slug('test') + ->date('2017-07-04') + ->collection('blog'); + + $this->parent->store('blog')->save($entry); + + $newPath = $this->directory.'/blog/2017-07-04.test.new-id.md'; + $this->assertFileEqualsString($existingPath, $existingContents); + $this->assertFileEqualsString($newPath, $entry->fileContents()); + @unlink($newPath); + @unlink($existingPath); + $this->assertFileNotExists($newPath); + $this->assertFileNotExists($existingPath); + + $this->assertEquals($newPath, $this->parent->store('blog')->paths()->get('new-id')); + } + + /** @test */ + public function it_doesnt_append_the_id_to_the_filename_if_it_is_itself() + { + $existingPath = $this->directory.'/blog/2017-07-04.test.md'; + file_put_contents($existingPath, "---\nid: the-id\n---"); + + $entry = Facades\Entry::make() + ->id('the-id') + ->slug('test') + ->date('2017-07-04') + ->collection('blog'); + + $this->parent->store('blog')->save($entry); + + $pathWithIdSuffix = $this->directory.'/blog/2017-07-04.test.the-id.md'; + $this->assertFileEqualsString($existingPath, $entry->fileContents()); + @unlink($existingPath); + $this->assertFileNotExists($pathWithIdSuffix); + $this->assertFileNotExists($existingPath); + + $this->assertEquals($existingPath, $this->parent->store('blog')->paths()->get('the-id')); + } + + /** @test */ + public function it_removes_the_id_suffix_if_the_suffixless_path_is_available() + { + $existingPath = $this->directory.'/blog/2017-07-04.test.123.md'; + $expectedPath = $this->directory.'/blog/2017-07-04.test.md'; + + $entry = Facades\Entry::make() + ->id('123') + ->slug('test') + ->date('2017-07-04') + ->collection('blog') + ->initialPath($existingPath); + + $this->parent->store('blog')->save($entry); + + $this->assertFileNotExists($existingPath); + $this->assertFileEqualsString($expectedPath, $entry->fileContents()); + @unlink($expectedPath); + $this->assertFileNotExists($expectedPath); + + $this->assertEquals($expectedPath, $this->parent->store('blog')->paths()->get('123')); + } + /** @test */ public function it_ignores_entries_in_a_site_subdirectory_where_the_collection_doesnt_have_that_site_enabled() { From bbf13df5fac8dbc967f0838c0826eda589f6e66d Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Fri, 7 May 2021 16:35:49 -0400 Subject: [PATCH 02/23] Handle id suffixes when getting date from path --- src/Entries/GetDateFromPath.php | 15 ++++++++++--- tests/Data/Entries/GetDateFromPathTest.php | 26 +++++++++++++++++----- 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/src/Entries/GetDateFromPath.php b/src/Entries/GetDateFromPath.php index 47db536846b..a743983ea2b 100644 --- a/src/Entries/GetDateFromPath.php +++ b/src/Entries/GetDateFromPath.php @@ -8,8 +8,17 @@ public function __invoke($path) { $filename = pathinfo($path, PATHINFO_FILENAME); - return strpos($filename, '.') === false - ? null - : explode('.', pathinfo($path, PATHINFO_FILENAME))[0]; + if (strpos($filename, '.') === false) { + return null; + } + + $firstSegment = explode('.', pathinfo($path, PATHINFO_FILENAME), 2)[0]; + + return $this->isDate($firstSegment) ? $firstSegment : null; + } + + private function isDate($str) + { + return preg_match('/^\d{4}-\d{2}-\d{2}(-\d{4})?$/', $str); } } diff --git a/tests/Data/Entries/GetDateFromPathTest.php b/tests/Data/Entries/GetDateFromPathTest.php index 8a3c70f97ac..23cf4163b7c 100644 --- a/tests/Data/Entries/GetDateFromPathTest.php +++ b/tests/Data/Entries/GetDateFromPathTest.php @@ -7,11 +7,27 @@ class GetDateFromPathTest extends TestCase { - /** @test */ - public function it_gets_the_date_from_a_path() + /** + * @test + * @dataProvider paths + **/ + public function it_gets_the_date_from_a_path($expected, $path) { - $this->assertEquals('2015-01-01', (new GetDateFromPath)('path/to/2015-01-01.post.md')); - $this->assertEquals('2015-01-01-1300', (new GetDateFromPath)('path/to/2015-01-01-1300.post.md')); - $this->assertNull((new GetDateFromPath)('path/to/post.md')); + $this->assertEquals($expected, (new GetDateFromPath)($path)); + } + + public function paths() + { + return [ + 'date' => ['2015-01-01', 'path/to/2015-01-01.post.md'], + 'time' => ['2015-01-01-1300', 'path/to/2015-01-01-1300.post.md'], + 'no date' => [null, 'path/to/post.md'], + 'no date but slug with number' => [null, 'path/to/2nd-post.md'], + + 'date with id suffix' => ['2015-01-01', 'path/to/2015-01-01.post.id-suffix.md'], + 'time with id suffix' => ['2015-01-01-1300', 'path/to/2015-01-01-1300.post.id-suffix.md'], + 'no date with id suffix' => [null, 'path/to/post.id-suffix.md'], + 'no date but slug with number with id suffix' => [null, 'path/to/2nd-post.md'], + ]; } } From 430a5b22afac1704d6607da43114d2f2284eebab Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Fri, 7 May 2021 16:36:24 -0400 Subject: [PATCH 03/23] Class for getting slug from path, and handle suffixes --- src/Entries/GetSlugFromPath.php | 28 ++++++++++++++++ src/Stache/Stores/CollectionEntriesStore.php | 3 +- src/Stache/Stores/TaxonomyTermsStore.php | 3 +- tests/Data/Entries/GetSlugFromPathTest.php | 35 ++++++++++++++++++++ 4 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 src/Entries/GetSlugFromPath.php create mode 100644 tests/Data/Entries/GetSlugFromPathTest.php diff --git a/src/Entries/GetSlugFromPath.php b/src/Entries/GetSlugFromPath.php new file mode 100644 index 00000000000..219035ca732 --- /dev/null +++ b/src/Entries/GetSlugFromPath.php @@ -0,0 +1,28 @@ +isDate($segments[0])) { + return $segments[1]; + } + + return $segments[0]; + } + + private function isDate($str) + { + return preg_match('/^\d{4}-\d{2}-\d{2}(-\d{4})?$/', $str); + } +} diff --git a/src/Stache/Stores/CollectionEntriesStore.php b/src/Stache/Stores/CollectionEntriesStore.php index 4ea15f842b4..e96f67916f8 100644 --- a/src/Stache/Stores/CollectionEntriesStore.php +++ b/src/Stache/Stores/CollectionEntriesStore.php @@ -3,6 +3,7 @@ namespace Statamic\Stache\Stores; use Statamic\Entries\GetDateFromPath; +use Statamic\Entries\GetSlugFromPath; use Statamic\Facades\Collection; use Statamic\Facades\Entry; use Statamic\Facades\File; @@ -63,7 +64,7 @@ public function makeItemFromFile($path, $contents) ->id($id) ->collection($collection); - $slug = pathinfo(Path::clean($path), PATHINFO_FILENAME); + $slug = (new GetSlugFromPath)($path); if ($origin = array_pull($data, 'origin')) { $entry->origin($origin); diff --git a/src/Stache/Stores/TaxonomyTermsStore.php b/src/Stache/Stores/TaxonomyTermsStore.php index 01de86a667e..07690ee9bda 100644 --- a/src/Stache/Stores/TaxonomyTermsStore.php +++ b/src/Stache/Stores/TaxonomyTermsStore.php @@ -4,6 +4,7 @@ use Facades\Statamic\Stache\Traverser; use Illuminate\Support\Facades\Cache; +use Statamic\Entries\GetSlugFromPath; use Statamic\Facades\File; use Statamic\Facades\Path; use Statamic\Facades\Stache; @@ -51,7 +52,7 @@ public function makeItemFromFile($path, $contents) $term = Term::make() ->taxonomy($taxonomy) - ->slug(pathinfo(Path::clean($path), PATHINFO_FILENAME)) + ->slug((new GetSlugFromPath)($path)) ->initialPath($path) ->blueprint($data['blueprint'] ?? null); diff --git a/tests/Data/Entries/GetSlugFromPathTest.php b/tests/Data/Entries/GetSlugFromPathTest.php new file mode 100644 index 00000000000..ece6b5e3d6c --- /dev/null +++ b/tests/Data/Entries/GetSlugFromPathTest.php @@ -0,0 +1,35 @@ +assertEquals($expected, (new GetSlugFromPath)('path/to/'.$path)); + } + + public function paths() + { + return [ + 'date' => ['post', '2015-01-01.post.md'], + 'time' => ['post', '2015-01-01-1300.post.md'], + 'no date' => ['post', 'post.md'], + 'no date but slug thats a number' => ['404', '404.md'], + 'no date but slug with number' => ['2nd-post', '2nd-post.md'], + + 'date with id suffix' => ['post', '2015-01-01.post.id-suffix.md'], + 'time with id suffix' => ['post', '2015-01-01-1300.post.id-suffix.md'], + 'no date with id suffix' => ['post', 'post.id-suffix.md'], + 'no date but slug thats a number' => ['404', '404.md'], + 'no date but slug with number with id suffix' => ['2nd-post', '2nd-post.md'], + ]; + } +} From 0c855caa0c53f3b2ef04f70e8ae82a8526ec23eb Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Fri, 7 May 2021 16:40:29 -0400 Subject: [PATCH 04/23] Adjust test so it actually does the thing it says --- src/Data/ExistsAsFile.php | 2 +- src/Entries/Entry.php | 2 +- src/Stache/Stores/CollectionEntriesStore.php | 2 +- tests/Stache/Stores/EntriesStoreTest.php | 8 ++------ 4 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/Data/ExistsAsFile.php b/src/Data/ExistsAsFile.php index db18fb1c7f1..7a549641c01 100644 --- a/src/Data/ExistsAsFile.php +++ b/src/Data/ExistsAsFile.php @@ -13,7 +13,7 @@ trait ExistsAsFile abstract public function path(); - protected function buildPath() + public function buildPath() { return $this->path(); } diff --git a/src/Entries/Entry.php b/src/Entries/Entry.php index e0f14cedb60..365aa374688 100644 --- a/src/Entries/Entry.php +++ b/src/Entries/Entry.php @@ -339,7 +339,7 @@ public function path() return $this->initialPath ?? $this->buildPath(); } - protected function buildPath() + public function buildPath() { $prefix = ''; diff --git a/src/Stache/Stores/CollectionEntriesStore.php b/src/Stache/Stores/CollectionEntriesStore.php index e96f67916f8..dc99fc1a8d2 100644 --- a/src/Stache/Stores/CollectionEntriesStore.php +++ b/src/Stache/Stores/CollectionEntriesStore.php @@ -170,7 +170,7 @@ protected function storeIndexes() protected function writeItemToDisk($item) { - if (! $contents = File::get($path = $item->path())) { + if (! $contents = File::get($path = $item->buildPath())) { return $item->writeFile(); } diff --git a/tests/Stache/Stores/EntriesStoreTest.php b/tests/Stache/Stores/EntriesStoreTest.php index 1e4aa7b748b..13a295585ce 100644 --- a/tests/Stache/Stores/EntriesStoreTest.php +++ b/tests/Stache/Stores/EntriesStoreTest.php @@ -184,12 +184,8 @@ public function it_removes_the_id_suffix_if_the_suffixless_path_is_available() $existingPath = $this->directory.'/blog/2017-07-04.test.123.md'; $expectedPath = $this->directory.'/blog/2017-07-04.test.md'; - $entry = Facades\Entry::make() - ->id('123') - ->slug('test') - ->date('2017-07-04') - ->collection('blog') - ->initialPath($existingPath); + file_put_contents($existingPath, 'id: 123'); + $entry = $this->parent->store('blog')->makeItemFromFile($existingPath, file_get_contents($existingPath)); $this->parent->store('blog')->save($entry); From faa4062e164e09cb1ff9ba9cdba68b9a9d98bc25 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Fri, 7 May 2021 16:41:10 -0400 Subject: [PATCH 05/23] Remove number prefixes which haven't been a thing for a while now, but will now actually break tests. --- tests/Stache/Stores/EntriesStoreTest.php | 6 +++--- .../content/collections/numeric/{1.one.md => one.md} | 0 .../content/collections/numeric/{3.three.md => three.md} | 0 .../content/collections/numeric/{2.two.md => two.md} | 0 4 files changed, 3 insertions(+), 3 deletions(-) rename tests/Stache/__fixtures__/content/collections/numeric/{1.one.md => one.md} (100%) rename tests/Stache/__fixtures__/content/collections/numeric/{3.three.md => three.md} (100%) rename tests/Stache/__fixtures__/content/collections/numeric/{2.two.md => two.md} (100%) diff --git a/tests/Stache/Stores/EntriesStoreTest.php b/tests/Stache/Stores/EntriesStoreTest.php index 13a295585ce..099db93e5fb 100644 --- a/tests/Stache/Stores/EntriesStoreTest.php +++ b/tests/Stache/Stores/EntriesStoreTest.php @@ -58,9 +58,9 @@ public function it_gets_nested_files() $files = Traverser::filter([$store, 'getItemFilter'])->traverse($store); $this->assertEquals(collect([ - $dir.'/numeric/1.one.md', - $dir.'/numeric/2.two.md', - $dir.'/numeric/3.three.md', + $dir.'/numeric/one.md', + $dir.'/numeric/two.md', + $dir.'/numeric/three.md', ])->sort()->values()->all(), $files->keys()->sort()->values()->all()); }); diff --git a/tests/Stache/__fixtures__/content/collections/numeric/1.one.md b/tests/Stache/__fixtures__/content/collections/numeric/one.md similarity index 100% rename from tests/Stache/__fixtures__/content/collections/numeric/1.one.md rename to tests/Stache/__fixtures__/content/collections/numeric/one.md diff --git a/tests/Stache/__fixtures__/content/collections/numeric/3.three.md b/tests/Stache/__fixtures__/content/collections/numeric/three.md similarity index 100% rename from tests/Stache/__fixtures__/content/collections/numeric/3.three.md rename to tests/Stache/__fixtures__/content/collections/numeric/three.md diff --git a/tests/Stache/__fixtures__/content/collections/numeric/2.two.md b/tests/Stache/__fixtures__/content/collections/numeric/two.md similarity index 100% rename from tests/Stache/__fixtures__/content/collections/numeric/2.two.md rename to tests/Stache/__fixtures__/content/collections/numeric/two.md From 12c7e799a77e14d6efc7a6b6adc7d7bbb8788fae Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Fri, 7 May 2021 16:43:21 -0400 Subject: [PATCH 06/23] Remove Path::clean() and URL::buildFromPath() which are methods that were really only used in v2 --- src/Facades/Endpoint/Path.php | 30 ------------------------------ src/Facades/Endpoint/URL.php | 21 --------------------- src/Facades/URL.php | 1 - tests/Facades/UrlTest.php | 30 ------------------------------ tests/PathsTest.php | 15 --------------- 5 files changed, 97 deletions(-) diff --git a/src/Facades/Endpoint/Path.php b/src/Facades/Endpoint/Path.php index d4e60f19caa..bcd25442859 100644 --- a/src/Facades/Endpoint/Path.php +++ b/src/Facades/Endpoint/Path.php @@ -89,36 +89,6 @@ public function resolve($path) return $leadingSlash ? Str::ensureLeft($path, '/') : $path; } - /** - * Cleans up a given $path, removing any flags and order keys (date-based or number-based). - * - * Assumes the path will always end with an extension. - * - * @param string $path Path to clean - * @return string - */ - public function clean($path) - { - // Remove draft and hidden flags - $path = preg_replace('/\/_[_]?/', '/', $path); - - // Strip the order keys - $segments = explode('/', $path); - $total_segments = count($segments); - foreach ($segments as $i => &$segment) { - // Skip the final segment (the basename) if it doesn't contain two periods. - // This stops filenames like 404.md from being interpreted with 404 as - // the order key, resulting in a borked filename. - if ($i + 1 === $total_segments && substr_count($segment, '.') < 2) { - continue; - } - - $segment = preg_replace(Pattern::orderKey(), '', $segment); - } - - return implode('/', $segments); - } - /** * Assembles a URL from an ordered list of segments. * diff --git a/src/Facades/Endpoint/URL.php b/src/Facades/Endpoint/URL.php index 26d1fde733b..406d2114833 100644 --- a/src/Facades/Endpoint/URL.php +++ b/src/Facades/Endpoint/URL.php @@ -249,27 +249,6 @@ public function getSiteUrl() return $protocol.$domain_name; } - /** - * Build a page URL from a path. - * - * @param string $path - * @return string - */ - public function buildFromPath($path) - { - $path = Path::makeRelative($path); - - $ext = pathinfo($path)['extension']; - - $path = Path::clean($path); - - $path = preg_replace('/^pages/', '', $path); - - $path = preg_replace('#\/(?:[a-z]+\.)?index\.'.$ext.'$#', '', $path); - - return Str::ensureLeft($path, '/'); - } - /** * Encode a URL. * diff --git a/src/Facades/URL.php b/src/Facades/URL.php index 07d0957b944..a32c15e4464 100644 --- a/src/Facades/URL.php +++ b/src/Facades/URL.php @@ -20,7 +20,6 @@ * @method static string format($url) * @method static bool isExternal($url) * @method static string getSiteUrl() - * @method static string buildFromPath($path) * @method static string encode($url) * @method static mixed getDefaultUri($locale, $uri) * @method static string gravatar($email, $size = null) diff --git a/tests/Facades/UrlTest.php b/tests/Facades/UrlTest.php index aeb63038ec9..22147526b60 100644 --- a/tests/Facades/UrlTest.php +++ b/tests/Facades/UrlTest.php @@ -15,36 +15,6 @@ protected function resolveApplicationConfiguration($app) $app['config']->set('app.url', 'http://absolute-url-resolved-from-request.com'); } - public function testBuildsUrl() - { - $url = URL::buildFromPath('pages/about/index.md'); - $this->assertEquals('/about', $url); - } - - public function testBuildsHomepage() - { - $url = URL::buildFromPath('pages/index.md'); - $this->assertEquals('/', $url); - } - - public function testBuildsUrlFromFullPath() - { - $url = URL::buildFromPath(base_path().'/pages/index.md'); - $this->assertEquals('/', $url); - } - - public function testBuildsLocalizedUrl() - { - $url = URL::buildFromPath('pages/about/fr.index.md'); - $this->assertEquals('/about', $url); - } - - public function testBuildsLocalizedHomepage() - { - $url = URL::buildFromPath('pages/fr.index.md'); - $this->assertEquals('/', $url); - } - public function testPrependsSiteUrl() { Site::setConfig('sites.en.url', 'http://site.com/'); diff --git a/tests/PathsTest.php b/tests/PathsTest.php index 916b36f05e8..e6fe450b050 100644 --- a/tests/PathsTest.php +++ b/tests/PathsTest.php @@ -6,21 +6,6 @@ class PathsTest extends TestCase { - public function testPathCleaning() - { - $path = Path::clean('/blog/2015-01-12.post.md'); - $this->assertEquals('/blog/post.md', $path); - - $path = Path::clean('pages/1.about/index.md'); - $this->assertEquals('pages/about/index.md', $path); - } - - public function testPathCleaningWithNumericSlug() - { - $path = Path::clean('/blog/404.md'); - $this->assertEquals('/blog/404.md', $path); - } - public function testRelativePath() { $path = Path::makeRelative(base_path().'/content/foo/bar.md'); From 01a564b5fdf78457d25bd4517d7429882683eba7 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Fri, 7 May 2021 16:44:10 -0400 Subject: [PATCH 07/23] Apply fixes from StyleCI (#3670) Co-authored-by: Jason Varga --- src/Stache/Stores/TaxonomyTermsStore.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Stache/Stores/TaxonomyTermsStore.php b/src/Stache/Stores/TaxonomyTermsStore.php index 07690ee9bda..c530c0a38ee 100644 --- a/src/Stache/Stores/TaxonomyTermsStore.php +++ b/src/Stache/Stores/TaxonomyTermsStore.php @@ -6,7 +6,6 @@ use Illuminate\Support\Facades\Cache; use Statamic\Entries\GetSlugFromPath; use Statamic\Facades\File; -use Statamic\Facades\Path; use Statamic\Facades\Stache; use Statamic\Facades\Taxonomy; use Statamic\Facades\Term; From 9505b5c20ec50a4a11bd0b572bf82907a0f38bc5 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Mon, 10 May 2021 19:23:49 -0400 Subject: [PATCH 08/23] use incrementing numbers rather than ids --- src/Stache/Stores/CollectionEntriesStore.php | 25 ++++++-- tests/Stache/Stores/EntriesStoreTest.php | 63 ++++++++++++++------ 2 files changed, 65 insertions(+), 23 deletions(-) diff --git a/src/Stache/Stores/CollectionEntriesStore.php b/src/Stache/Stores/CollectionEntriesStore.php index dc99fc1a8d2..6ed297b0b0b 100644 --- a/src/Stache/Stores/CollectionEntriesStore.php +++ b/src/Stache/Stores/CollectionEntriesStore.php @@ -170,16 +170,29 @@ protected function storeIndexes() protected function writeItemToDisk($item) { - if (! $contents = File::get($path = $item->buildPath())) { - return $item->writeFile(); + if (! File::exists($basePath = $item->buildPath())) { + return $item->writeFile($basePath); } - $itemFromDisk = $this->makeItemFromFile($path, $contents); + $num = 0; - if ($item->id() !== $itemFromDisk->id()) { + while (true) { $ext = '.'.$item->fileExtension(); - $filename = Str::before($path, $ext); - $path = "{$filename}.{$item->id()}{$ext}"; + $filename = Str::before($basePath, $ext); + $suffix = $num ? ".$num" : ''; + $path = "{$filename}{$suffix}{$ext}"; + + if (! $contents = File::get($path)) { + break; + } + + $itemFromDisk = $this->makeItemFromFile($path, $contents); + + if ($item->id() == $itemFromDisk->id()) { + break; + } + + $num++; } $item->writeFile($path); diff --git a/tests/Stache/Stores/EntriesStoreTest.php b/tests/Stache/Stores/EntriesStoreTest.php index 099db93e5fb..4a9f2a3d273 100644 --- a/tests/Stache/Stores/EntriesStoreTest.php +++ b/tests/Stache/Stores/EntriesStoreTest.php @@ -131,32 +131,36 @@ public function it_saves_to_disk() } /** @test */ - public function it_appends_the_id_to_the_filename_if_one_already_exists() + public function it_appends_suffix_to_the_filename_if_one_already_exists() { $existingPath = $this->directory.'/blog/2017-07-04.test.md'; file_put_contents($existingPath, $existingContents = "---\nid: existing-id\n---"); - $entry = Facades\Entry::make() - ->id('new-id') - ->slug('test') - ->date('2017-07-04') - ->collection('blog'); - + $entry = Facades\Entry::make()->id('new-id')->slug('test')->date('2017-07-04')->collection('blog'); $this->parent->store('blog')->save($entry); - - $newPath = $this->directory.'/blog/2017-07-04.test.new-id.md'; + $newPath = $this->directory.'/blog/2017-07-04.test.1.md'; $this->assertFileEqualsString($existingPath, $existingContents); $this->assertFileEqualsString($newPath, $entry->fileContents()); + + $anotherEntry = Facades\Entry::make()->id('another-new-id')->slug('test')->date('2017-07-04')->collection('blog'); + $this->parent->store('blog')->save($anotherEntry); + $anotherNewPath = $this->directory.'/blog/2017-07-04.test.2.md'; + $this->assertFileEqualsString($existingPath, $existingContents); + $this->assertFileEqualsString($anotherNewPath, $anotherEntry->fileContents()); + + $this->assertEquals($newPath, $this->parent->store('blog')->paths()->get('new-id')); + $this->assertEquals($anotherNewPath, $this->parent->store('blog')->paths()->get('another-new-id')); + @unlink($newPath); + @unlink($anotherNewPath); @unlink($existingPath); $this->assertFileNotExists($newPath); + $this->assertFileNotExists($anotherNewPath); $this->assertFileNotExists($existingPath); - - $this->assertEquals($newPath, $this->parent->store('blog')->paths()->get('new-id')); } /** @test */ - public function it_doesnt_append_the_id_to_the_filename_if_it_is_itself() + public function it_doesnt_append_the_suffix_to_the_filename_if_it_is_itself() { $existingPath = $this->directory.'/blog/2017-07-04.test.md'; file_put_contents($existingPath, "---\nid: the-id\n---"); @@ -169,19 +173,44 @@ public function it_doesnt_append_the_id_to_the_filename_if_it_is_itself() $this->parent->store('blog')->save($entry); - $pathWithIdSuffix = $this->directory.'/blog/2017-07-04.test.the-id.md'; + $pathWithSuffix = $this->directory.'/blog/2017-07-04.test.1.md'; $this->assertFileEqualsString($existingPath, $entry->fileContents()); + $this->assertEquals($existingPath, $this->parent->store('blog')->paths()->get('the-id')); + @unlink($existingPath); - $this->assertFileNotExists($pathWithIdSuffix); + $this->assertFileNotExists($pathWithSuffix); $this->assertFileNotExists($existingPath); + } - $this->assertEquals($existingPath, $this->parent->store('blog')->paths()->get('the-id')); + /** @test */ + public function it_doesnt_append_the_suffix_to_an_already_suffixed_filename_if_it_is_itself() + { + $suffixlessExistingPath = $this->directory.'/blog/2017-07-04.test.md'; + file_put_contents($suffixlessExistingPath, "---\nid: the-id\n---"); + $suffixedExistingPath = $this->directory.'/blog/2017-07-04.test.md'; + file_put_contents($suffixedExistingPath, "---\nid: another-id\n---"); + + $entry = Facades\Entry::make() + ->id('another-id') + ->slug('test') + ->date('2017-07-04') + ->collection('blog'); + + $this->parent->store('blog')->save($entry); + + $pathWithIncrementedSuffix = $this->directory.'/blog/2017-07-04.test.2.md'; + $this->assertFileEqualsString($suffixedExistingPath, $entry->fileContents()); + @unlink($suffixedExistingPath); + $this->assertFileNotExists($pathWithIncrementedSuffix); + $this->assertFileNotExists($suffixedExistingPath); + + $this->assertEquals($suffixedExistingPath, $this->parent->store('blog')->paths()->get('another-id')); } /** @test */ - public function it_removes_the_id_suffix_if_the_suffixless_path_is_available() + public function it_removes_the_suffix_if_the_suffixless_path_is_available() { - $existingPath = $this->directory.'/blog/2017-07-04.test.123.md'; + $existingPath = $this->directory.'/blog/2017-07-04.test.1.md'; $expectedPath = $this->directory.'/blog/2017-07-04.test.md'; file_put_contents($existingPath, 'id: 123'); From 4f1c932ab78cc03d6f891ff25d79a0de759d9fec Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Mon, 10 May 2021 19:42:43 -0400 Subject: [PATCH 09/23] Keep the suffix. It felt weird changing it, in practice. --- src/Stache/Stores/CollectionEntriesStore.php | 6 ++++-- tests/Stache/Stores/EntriesStoreTest.php | 15 ++++++++------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/Stache/Stores/CollectionEntriesStore.php b/src/Stache/Stores/CollectionEntriesStore.php index 6ed297b0b0b..77972efe945 100644 --- a/src/Stache/Stores/CollectionEntriesStore.php +++ b/src/Stache/Stores/CollectionEntriesStore.php @@ -170,8 +170,10 @@ protected function storeIndexes() protected function writeItemToDisk($item) { - if (! File::exists($basePath = $item->buildPath())) { - return $item->writeFile($basePath); + $basePath = $item->buildPath(); + + if ($basePath !== $item->path()) { + return $item->writeFile($item->path()); } $num = 0; diff --git a/tests/Stache/Stores/EntriesStoreTest.php b/tests/Stache/Stores/EntriesStoreTest.php index 4a9f2a3d273..27088c1e7ef 100644 --- a/tests/Stache/Stores/EntriesStoreTest.php +++ b/tests/Stache/Stores/EntriesStoreTest.php @@ -208,22 +208,23 @@ public function it_doesnt_append_the_suffix_to_an_already_suffixed_filename_if_i } /** @test */ - public function it_removes_the_suffix_if_the_suffixless_path_is_available() + public function it_keeps_the_suffix_even_if_the_suffixless_path_is_available() { $existingPath = $this->directory.'/blog/2017-07-04.test.1.md'; - $expectedPath = $this->directory.'/blog/2017-07-04.test.md'; + $suffixlessPath = $this->directory.'/blog/2017-07-04.test.md'; file_put_contents($existingPath, 'id: 123'); $entry = $this->parent->store('blog')->makeItemFromFile($existingPath, file_get_contents($existingPath)); $this->parent->store('blog')->save($entry); - $this->assertFileNotExists($existingPath); - $this->assertFileEqualsString($expectedPath, $entry->fileContents()); - @unlink($expectedPath); - $this->assertFileNotExists($expectedPath); + $this->assertFileEqualsString($existingPath, $entry->fileContents()); + $this->assertFileNotExists($suffixlessPath); + + $this->assertEquals($existingPath, $this->parent->store('blog')->paths()->get('123')); - $this->assertEquals($expectedPath, $this->parent->store('blog')->paths()->get('123')); + @unlink($existingPath); + $this->assertFileNotExists($existingPath); } /** @test */ From e7fb87391223fd70c8539f76a1305808e2e9bac3 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Tue, 11 May 2021 12:08:46 -0400 Subject: [PATCH 10/23] Replace unique slug validation with unique uri validation --- resources/lang/en/validation.php | 1 + .../CP/Collections/EntriesController.php | 38 ++++++++++++++++++- src/Stache/Repositories/EntryRepository.php | 4 +- 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/resources/lang/en/validation.php b/resources/lang/en/validation.php index 4cd5ae33921..779c7905b2f 100644 --- a/resources/lang/en/validation.php +++ b/resources/lang/en/validation.php @@ -128,6 +128,7 @@ 'duplicate_field_handle' => 'Field with a handle of :handle cannot be used more than once.', 'one_site_without_origin' => 'At least one site must not have an origin.', 'origin_cannot_be_disabled' => 'Cannot select a disabled origin.', + 'unique_uri' => 'This URI has already been taken.', /* |-------------------------------------------------------------------------- diff --git a/src/Http/Controllers/CP/Collections/EntriesController.php b/src/Http/Controllers/CP/Collections/EntriesController.php index 7974b10f8d3..13fc8aa4061 100644 --- a/src/Http/Controllers/CP/Collections/EntriesController.php +++ b/src/Http/Controllers/CP/Collections/EntriesController.php @@ -3,6 +3,7 @@ namespace Statamic\Http\Controllers\CP\Collections; use Illuminate\Http\Request; +use Illuminate\Validation\ValidationException; use Statamic\Contracts\Entries\Entry as EntryContract; use Statamic\CP\Breadcrumbs; use Statamic\Facades\Asset; @@ -189,9 +190,9 @@ public function update(Request $request, $collection, $entry) } if ($collection->structure() && ! $collection->orderable()) { - $entry->afterSave(function ($entry) use ($parent) { - $tree = $entry->structure()->in($entry->locale()); + $tree = $entry->structure()->in($entry->locale()); + $entry->afterSave(function ($entry) use ($parent, $tree) { if ($parent && optional($tree->page($parent))->isRoot()) { $parent = null; } @@ -202,6 +203,8 @@ public function update(Request $request, $collection, $entry) }); } + $this->validateUniqueUri($entry, $tree ?? null, $parent ?? null); + if ($entry->revisionsEnabled() && $entry->published()) { $entry ->makeWorkingCopy() @@ -326,6 +329,8 @@ public function store(Request $request, $collection, $site) }); } + $this->validateUniqueUri($entry, $tree ?? null, $parent ?? null); + if ($entry->revisionsEnabled()) { $entry->store([ 'message' => $request->message, @@ -418,6 +423,35 @@ protected function formatDateForSaving($date) return $date; } + private function validateUniqueUri($entry, $tree, $parent) + { + $uri = $this->entryUri($entry, $tree, $parent); + + $existing = Entry::findByUri($uri); + + if (! $existing || $existing->id() === $entry->id()) { + return; + } + + throw ValidationException::withMessages(['slug' => __('statamic::validation.unique_uri')]); + } + + private function entryUri($entry, $tree, $parent) + { + if (! $tree) { + return $entry->uri(); + } + + $parent = $parent ? $tree->page($parent) : null; + + return app(\Statamic\Contracts\Routing\UrlBuilder::class) + ->content($entry) + ->merge([ + 'parent_uri' => $parent ? $parent->uri() : null, + ]) + ->build($entry->route()); + } + protected function breadcrumbs($collection) { return new Breadcrumbs([ diff --git a/src/Stache/Repositories/EntryRepository.php b/src/Stache/Repositories/EntryRepository.php index 02bbbe80760..b1d0d3a39e6 100644 --- a/src/Stache/Repositories/EntryRepository.php +++ b/src/Stache/Repositories/EntryRepository.php @@ -103,7 +103,7 @@ public function createRules($collection, $site) { return [ 'title' => 'required', - 'slug' => 'required|unique_entry_value:'.$collection->handle().',null,'.$site->handle(), + 'slug' => 'required', ]; } @@ -111,7 +111,7 @@ public function updateRules($collection, $entry) { return [ 'title' => 'required', - 'slug' => 'required|alpha_dash|unique_entry_value:'.$collection->handle().','.$entry->id().','.$entry->locale(), + 'slug' => 'required|alpha_dash', ]; } From 859e79d048036309495dd934e4d41a2709d75c88 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Tue, 11 May 2021 13:10:57 -0400 Subject: [PATCH 11/23] Prevent error when validating entry in a collection without a route --- src/Http/Controllers/CP/Collections/EntriesController.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Http/Controllers/CP/Collections/EntriesController.php b/src/Http/Controllers/CP/Collections/EntriesController.php index 13fc8aa4061..c9c99222fc4 100644 --- a/src/Http/Controllers/CP/Collections/EntriesController.php +++ b/src/Http/Controllers/CP/Collections/EntriesController.php @@ -425,7 +425,9 @@ protected function formatDateForSaving($date) private function validateUniqueUri($entry, $tree, $parent) { - $uri = $this->entryUri($entry, $tree, $parent); + if (! $uri = $this->entryUri($entry, $tree, $parent)) { + return; + } $existing = Entry::findByUri($uri); From 4c9570442a6f6d23f72143a7b27373bd4fa197b8 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Tue, 11 May 2021 13:27:52 -0400 Subject: [PATCH 12/23] Deprecate findBySlug --- src/Contracts/Entries/EntryRepository.php | 1 + src/Contracts/Taxonomies/TermRepository.php | 1 + src/Facades/Entry.php | 1 - src/Facades/Term.php | 1 - src/Stache/Repositories/EntryRepository.php | 1 + src/Stache/Repositories/TermRepository.php | 1 + tests/Stache/FeatureTest.php | 5 ++++- tests/Stache/Repositories/EntryRepositoryTest.php | 5 ++++- 8 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/Contracts/Entries/EntryRepository.php b/src/Contracts/Entries/EntryRepository.php index 4b1b4746d3b..fbaf755aa69 100644 --- a/src/Contracts/Entries/EntryRepository.php +++ b/src/Contracts/Entries/EntryRepository.php @@ -14,6 +14,7 @@ public function find($id); public function findByUri(string $uri); + /** @deprecated */ public function findBySlug(string $slug, string $collection); public function make(); diff --git a/src/Contracts/Taxonomies/TermRepository.php b/src/Contracts/Taxonomies/TermRepository.php index 8220df3e608..cd454b7b62b 100644 --- a/src/Contracts/Taxonomies/TermRepository.php +++ b/src/Contracts/Taxonomies/TermRepository.php @@ -14,6 +14,7 @@ public function find($id); public function findByUri(string $uri); + /** @deprecated */ public function findBySlug(string $slug, string $collection); public function make(string $slug = null); diff --git a/src/Facades/Entry.php b/src/Facades/Entry.php index 73df8e89126..186f4f28f4d 100644 --- a/src/Facades/Entry.php +++ b/src/Facades/Entry.php @@ -11,7 +11,6 @@ * @method static \Statamic\Entries\EntryCollection whereInCollection(array $handles) * @method static null|\Statamic\Contracts\Entries\Entry find($id) * @method static null|\Statamic\Contracts\Entries\Entry findByUri(string $uri) - * @method static null|\Statamic\Contracts\Entries\Entry findBySlug(string $slug, string $collection) * @method static \Statamic\Contracts\Entries\Entry make() * @method static \Statamic\Contracts\Entries\QueryBuilder query() * @method static void save($entry) diff --git a/src/Facades/Term.php b/src/Facades/Term.php index 5b9b35e0bb9..b7c343ef87a 100644 --- a/src/Facades/Term.php +++ b/src/Facades/Term.php @@ -14,7 +14,6 @@ * @method static TermCollection whereInTaxonomy(array $handles) * @method static TermContract find($id) * @method static TermContract findByUri(string $uri, string $site = null) - * @method static TermContract findBySlug(string $slug, string $taxonomy) * @method static save($term) * @method static delete($term) * @method static TermQueryBuilder query() diff --git a/src/Stache/Repositories/EntryRepository.php b/src/Stache/Repositories/EntryRepository.php index b1d0d3a39e6..4677239d40a 100644 --- a/src/Stache/Repositories/EntryRepository.php +++ b/src/Stache/Repositories/EntryRepository.php @@ -40,6 +40,7 @@ public function find($id): ?Entry return $this->query()->where('id', $id)->first(); } + /** @deprecated */ public function findBySlug(string $slug, string $collection): ?Entry { return $this->query() diff --git a/src/Stache/Repositories/TermRepository.php b/src/Stache/Repositories/TermRepository.php index 364374b1cc3..fdb659db32e 100644 --- a/src/Stache/Repositories/TermRepository.php +++ b/src/Stache/Repositories/TermRepository.php @@ -82,6 +82,7 @@ public function findByUri(string $uri, string $site = null): ?Term return $term->collection($collection); } + /** @deprecated */ public function findBySlug(string $slug, string $taxonomy): ?Term { return $this->query() diff --git a/tests/Stache/FeatureTest.php b/tests/Stache/FeatureTest.php index 4c763e7df91..823388d0752 100644 --- a/tests/Stache/FeatureTest.php +++ b/tests/Stache/FeatureTest.php @@ -65,7 +65,10 @@ public function it_gets_entry() $this->assertNull(Entry::find('users-john')); } - /** @test */ + /** + * @test + * @deprecated + **/ public function it_gets_entry_by_slug() { $this->assertEquals('Christmas', Entry::findBySlug('christmas', 'blog', 'christmas')->get('title')); diff --git a/tests/Stache/Repositories/EntryRepositoryTest.php b/tests/Stache/Repositories/EntryRepositoryTest.php index f3a9eae24ef..70b50943818 100644 --- a/tests/Stache/Repositories/EntryRepositoryTest.php +++ b/tests/Stache/Repositories/EntryRepositoryTest.php @@ -132,7 +132,10 @@ public function it_gets_entry_by_id() $this->assertNull($this->repo->find('unknown')); } - /** @test */ + /** + * @test + * @deprecated + **/ public function it_gets_entry_by_slug() { $entry = $this->repo->findBySlug('bravo', 'alphabetical'); From a57d1e7b7a53be1736b4be2ec2334dbc6fd3dcbd Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Tue, 11 May 2021 16:42:30 -0400 Subject: [PATCH 13/23] Ability to have placeholders and replacements in validation rules --- src/Fields/Validator.php | 28 ++++++++++++++++++++++++++-- tests/Fields/ValidatorTest.php | 28 ++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/src/Fields/Validator.php b/src/Fields/Validator.php index e0d194255c6..59b5a4c5562 100644 --- a/src/Fields/Validator.php +++ b/src/Fields/Validator.php @@ -4,11 +4,13 @@ use Illuminate\Support\Facades\Lang; use Illuminate\Support\Facades\Validator as LaravelValidator; +use Statamic\Support\Arr; +use Statamic\Support\Str; class Validator { protected $fields; - protected $data = []; + protected $replacements = []; protected $extraRules = []; public function make() @@ -34,7 +36,11 @@ public function rules() { return $this ->merge($this->fieldRules(), $this->extraRules) - ->all(); + ->map(function ($rules) { + return collect($rules)->map(function ($rule) { + return $this->parse($rule); + })->all(); + })->all(); } private function fieldRules() @@ -65,6 +71,13 @@ public function merge($original, $overrides) return collect($original); } + public function withReplacements($replacements) + { + $this->replacements = $replacements; + + return $this; + } + public function validate() { return LaravelValidator::validate( @@ -84,6 +97,17 @@ private function fieldAttributes() })->all(); } + private function parse($rule) + { + if (! Str::contains($rule, '{')) { + return $rule; + } + + return preg_replace_callback('/{\s*([a-zA-Z0-9_\-]+)\s*}/', function ($match) { + return Arr::get($this->replacements, $match[1], 'NULL'); + }, $rule); + } + public static function explodeRules($rules) { if (! $rules) { diff --git a/tests/Fields/ValidatorTest.php b/tests/Fields/ValidatorTest.php index a600ef03185..3dc8447500b 100644 --- a/tests/Fields/ValidatorTest.php +++ b/tests/Fields/ValidatorTest.php @@ -116,4 +116,32 @@ public function it_merges_additional_rules_into_field_rules() 'additional' => ['required'], ], $validation->rules()); } + + /** @test */ + public function it_makes_replacements() + { + $field = Mockery::mock(Field::class); + $field->shouldReceive('rules')->andReturn([ + 'one' => ['required', 'test:{foo}'], + ]); + + $fields = Mockery::mock(Fields::class); + $fields->shouldReceive('all')->andReturn(collect([$field])); + $fields->shouldReceive('preProcessValidatables')->andReturnSelf(); + + $validation = (new Validator)->fields($fields)->withRules([ + 'one' => 'test:{bar}', + 'two' => 'another:{baz},{qux},{quux}', + ])->withReplacements([ + 'foo' => 'FOO', + 'bar' => 'BAR', + 'baz' => 'BAZ', + 'quux' => 'QUUX', + ]); + + $this->assertEquals([ + 'one' => ['required', 'test:FOO', 'test:BAR'], + 'two' => ['another:BAZ,NULL,QUUX'], + ], $validation->rules()); + } } From dd9850a46f744a6820465d45a614d0398b665c67 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Tue, 11 May 2021 16:42:51 -0400 Subject: [PATCH 14/23] Pass along replacements in entries --- .../CP/Collections/EntriesController.php | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/Http/Controllers/CP/Collections/EntriesController.php b/src/Http/Controllers/CP/Collections/EntriesController.php index 7974b10f8d3..118a2b0aca9 100644 --- a/src/Http/Controllers/CP/Collections/EntriesController.php +++ b/src/Http/Controllers/CP/Collections/EntriesController.php @@ -164,7 +164,14 @@ public function update(Request $request, $collection, $entry) $fields = $entry->blueprint()->fields()->addValues($data); - $fields->validate(Entry::updateRules($collection, $entry)); + $fields + ->validator() + ->withRules(Entry::updateRules($collection, $entry)) + ->withReplacements([ + 'id' => $entry->id(), + 'collection' => $collection->handle(), + 'site' => $entry->locale(), + ])->validate(); $values = $fields->process()->values(); @@ -302,7 +309,13 @@ public function store(Request $request, $collection, $site) $fields = $blueprint->fields()->addValues($data); - $fields->validate(Entry::createRules($collection, $site)); + $fields + ->validator() + ->withRules(Entry::createRules($collection, $site)) + ->withReplacements([ + 'collection' => $collection->handle(), + 'site' => $site->handle(), + ])->validate(); $values = $fields->process()->values()->except(['slug', 'date', 'blueprint']); From fe4c5af078fb8b652670a3de5a1fd22bf56df230 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Tue, 11 May 2021 16:50:23 -0400 Subject: [PATCH 15/23] add unique entry value rule to suggestions, and handle inserting a rule with a colon but isn't expecting params to be typed --- resources/js/components/field-validation/Builder.vue | 6 +++++- resources/js/components/field-validation/Rules.js | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/resources/js/components/field-validation/Builder.vue b/resources/js/components/field-validation/Builder.vue index 5da152c8084..beaf4635351 100644 --- a/resources/js/components/field-validation/Builder.vue +++ b/resources/js/components/field-validation/Builder.vue @@ -47,7 +47,7 @@ />