From 6d7cde0b6fd8e0e163e3c8a31aa7cb1198ed5f4a Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Tue, 30 Jan 2024 14:39:33 +0000 Subject: [PATCH 01/44] wip --- resources/js/components/Slugify.vue | 17 ++++++- resources/js/plugins/slugify.js | 49 ++++++++++++------- routes/cp.php | 2 + .../CP/Fieldtypes/SlugFieldtypeController.php | 25 ++++++++++ 4 files changed, 74 insertions(+), 19 deletions(-) create mode 100644 src/Http/Controllers/CP/Fieldtypes/SlugFieldtypeController.php diff --git a/resources/js/components/Slugify.vue b/resources/js/components/Slugify.vue index e0ce91e703a..18d6901e248 100644 --- a/resources/js/components/Slugify.vue +++ b/resources/js/components/Slugify.vue @@ -28,10 +28,15 @@ export default { computed: { - slug() { + async slug() { if (!this.shouldSlugify) return this.to; if (!this.from) return ''; - return this.$slugify(this.from, this.separator, this.language); + + // use generateSlug method and get the response.data.slug from it + // return this.generateSlug().data.slug; + + + // return await this.$slugify(this.from, this.separator, this.language); } }, @@ -60,6 +65,14 @@ export default { methods: { + // async generateSlug() { + // return await this.$axios.post(cp_url('fieldtypes/slug'), { + // from: this.from, + // separator: this.separator, + // language: this.language + // }); + // }, + reset() { if (this.enabled) this.shouldSlugify = true; } diff --git a/resources/js/plugins/slugify.js b/resources/js/plugins/slugify.js index 59ce9a715d0..b46a1adde88 100644 --- a/resources/js/plugins/slugify.js +++ b/resources/js/plugins/slugify.js @@ -3,27 +3,42 @@ import getSlug from 'speakingurl'; export default { install(Vue, options) { Vue.prototype.$slugify = function(text, glue, lang) { - const selectedSite = Statamic.$config.get('selectedSite'); - const sites = Statamic.$config.get('sites'); - const site = sites.find(site => site.handle === selectedSite); - lang = lang ?? site?.lang ?? Statamic.$config.get('lang'); - const custom = Statamic.$config.get(`charmap.${lang}`) ?? {}; + // this.$axios.post(cp_url('fieldtypes/slug'), { + // from: this.from, + // separator: this.separator, + // language: this.language + // }); - // Remove apostrophes in all languages - custom["'"] = ""; + // Return the slug from the API + return this.$axios.post(cp_url('fieldtypes/slug'), { + from: text, + separator: glue || '-', + language: lang + }).then(response => { + return response.data.slug; + }); - // Remove smart single quotes - custom["’"] = ""; + // const selectedSite = Statamic.$config.get('selectedSite'); + // const sites = Statamic.$config.get('sites'); + // const site = sites.find(site => site.handle === selectedSite); + // lang = lang ?? site?.lang ?? Statamic.$config.get('lang'); + // const custom = Statamic.$config.get(`charmap.${lang}`) ?? {}; - // Prevent `Block - Hero` turning into `block_-_hero` - custom[" - "] = " "; + // // Remove apostrophes in all languages + // custom["'"] = ""; - return getSlug(text, { - separator: glue || '-', - lang, - custom, - symbols: Statamic.$config.get('asciiReplaceExtraSymbols') - }); + // // Remove smart single quotes + // custom["’"] = ""; + + // // Prevent `Block - Hero` turning into `block_-_hero` + // custom[" - "] = " "; + + // return getSlug(text, { + // separator: glue || '-', + // lang, + // custom, + // symbols: Statamic.$config.get('asciiReplaceExtraSymbols') + // }); }; } }; diff --git a/routes/cp.php b/routes/cp.php index c3dd3ef5e97..d05009f87fd 100644 --- a/routes/cp.php +++ b/routes/cp.php @@ -48,6 +48,7 @@ use Statamic\Http\Controllers\CP\Fieldtypes\FilesFieldtypeController; use Statamic\Http\Controllers\CP\Fieldtypes\MarkdownFieldtypeController; use Statamic\Http\Controllers\CP\Fieldtypes\RelationshipFieldtypeController; +use Statamic\Http\Controllers\CP\Fieldtypes\SlugFieldtypeController; use Statamic\Http\Controllers\CP\Forms\ActionController as FormActionController; use Statamic\Http\Controllers\CP\Forms\FormBlueprintController; use Statamic\Http\Controllers\CP\Forms\FormExportController; @@ -297,6 +298,7 @@ Route::get('relationship/filters', [RelationshipFieldtypeController::class, 'filters'])->name('relationship.filters'); Route::post('markdown', [MarkdownFieldtypeController::class, 'preview'])->name('markdown.preview'); Route::post('files/upload', [FilesFieldtypeController::class, 'upload'])->name('files.upload'); + Route::post('slug', [SlugFieldtypeController::class, 'generate'])->name('slug.generate'); }); Route::group(['prefix' => 'api', 'as' => 'api.'], function () { diff --git a/src/Http/Controllers/CP/Fieldtypes/SlugFieldtypeController.php b/src/Http/Controllers/CP/Fieldtypes/SlugFieldtypeController.php new file mode 100644 index 00000000000..93209d089c0 --- /dev/null +++ b/src/Http/Controllers/CP/Fieldtypes/SlugFieldtypeController.php @@ -0,0 +1,25 @@ +validate([ + 'from' => ['required'], + 'separator' => ['required'], + 'language' => ['required'], + ]); + + $slug = Str::slug($validated['from'], $validated['separator'], $validated['language']); + + return [ + 'slug' => $slug, + ]; + } +} From 00d734a72385e77a4fff33033f352b19b1491fcd Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Wed, 31 Jan 2024 11:43:14 +0000 Subject: [PATCH 02/44] Cover slug generation in tests I've added test cases for some of the slug issues we've fixed in the past, including: * #8429 * #8749 * #8844 * #5823 --- tests/Feature/Fieldtypes/SlugTest.php | 43 +++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 tests/Feature/Fieldtypes/SlugTest.php diff --git a/tests/Feature/Fieldtypes/SlugTest.php b/tests/Feature/Fieldtypes/SlugTest.php new file mode 100644 index 00000000000..8f6c8c9dbd8 --- /dev/null +++ b/tests/Feature/Fieldtypes/SlugTest.php @@ -0,0 +1,43 @@ +actingAs(tap(User::make()->makeSuper())->save()) + ->post('/cp/fieldtypes/slug', [ + 'from' => $from, + 'separator' => $separator, + 'language' => $language, + ]) + ->assertOk() + ->assertJson([ + 'slug' => $expected, + ]); + } + + public function slugProvider() + { + return [ + 'single_word' => ['one', '-', 'en', 'one'], + 'one-two-three' => ['one two three', '-', 'en', 'one-two-three'], + 'apples' => ["Apple's", '-', 'en', 'apples'], + 'single_smart_quotes' => ['GTA Online’s latest news: “huge map”', '-', 'en', 'gta-onlines-latest-news-huge-map'], + 'highens_separated_by_spaces' => ['Block - Hero', '-', 'en', 'block-hero'], + ]; + } +} From afc0b645a894a9fc5e96fb1d5b64e2ee4ac579a3 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Wed, 31 Jan 2024 11:45:14 +0000 Subject: [PATCH 03/44] Reverse yesterday's JS changes --- resources/js/components/Slugify.vue | 17 ++-------- resources/js/plugins/slugify.js | 49 ++++++++++------------------- 2 files changed, 19 insertions(+), 47 deletions(-) diff --git a/resources/js/components/Slugify.vue b/resources/js/components/Slugify.vue index 18d6901e248..e0ce91e703a 100644 --- a/resources/js/components/Slugify.vue +++ b/resources/js/components/Slugify.vue @@ -28,15 +28,10 @@ export default { computed: { - async slug() { + slug() { if (!this.shouldSlugify) return this.to; if (!this.from) return ''; - - // use generateSlug method and get the response.data.slug from it - // return this.generateSlug().data.slug; - - - // return await this.$slugify(this.from, this.separator, this.language); + return this.$slugify(this.from, this.separator, this.language); } }, @@ -65,14 +60,6 @@ export default { methods: { - // async generateSlug() { - // return await this.$axios.post(cp_url('fieldtypes/slug'), { - // from: this.from, - // separator: this.separator, - // language: this.language - // }); - // }, - reset() { if (this.enabled) this.shouldSlugify = true; } diff --git a/resources/js/plugins/slugify.js b/resources/js/plugins/slugify.js index b46a1adde88..59ce9a715d0 100644 --- a/resources/js/plugins/slugify.js +++ b/resources/js/plugins/slugify.js @@ -3,42 +3,27 @@ import getSlug from 'speakingurl'; export default { install(Vue, options) { Vue.prototype.$slugify = function(text, glue, lang) { - // this.$axios.post(cp_url('fieldtypes/slug'), { - // from: this.from, - // separator: this.separator, - // language: this.language - // }); + const selectedSite = Statamic.$config.get('selectedSite'); + const sites = Statamic.$config.get('sites'); + const site = sites.find(site => site.handle === selectedSite); + lang = lang ?? site?.lang ?? Statamic.$config.get('lang'); + const custom = Statamic.$config.get(`charmap.${lang}`) ?? {}; - // Return the slug from the API - return this.$axios.post(cp_url('fieldtypes/slug'), { - from: text, - separator: glue || '-', - language: lang - }).then(response => { - return response.data.slug; - }); - - // const selectedSite = Statamic.$config.get('selectedSite'); - // const sites = Statamic.$config.get('sites'); - // const site = sites.find(site => site.handle === selectedSite); - // lang = lang ?? site?.lang ?? Statamic.$config.get('lang'); - // const custom = Statamic.$config.get(`charmap.${lang}`) ?? {}; + // Remove apostrophes in all languages + custom["'"] = ""; - // // Remove apostrophes in all languages - // custom["'"] = ""; + // Remove smart single quotes + custom["’"] = ""; - // // Remove smart single quotes - // custom["’"] = ""; + // Prevent `Block - Hero` turning into `block_-_hero` + custom[" - "] = " "; - // // Prevent `Block - Hero` turning into `block_-_hero` - // custom[" - "] = " "; - - // return getSlug(text, { - // separator: glue || '-', - // lang, - // custom, - // symbols: Statamic.$config.get('asciiReplaceExtraSymbols') - // }); + return getSlug(text, { + separator: glue || '-', + lang, + custom, + symbols: Statamic.$config.get('asciiReplaceExtraSymbols') + }); }; } }; From 7909882e605054df8ab8082eb20ae8187f254463 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Wed, 31 Jan 2024 11:50:50 +0000 Subject: [PATCH 04/44] Add test cases for #2817 & #6985 --- tests/Feature/Fieldtypes/SlugTest.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/Feature/Fieldtypes/SlugTest.php b/tests/Feature/Fieldtypes/SlugTest.php index 8f6c8c9dbd8..7f0c0f2f08b 100644 --- a/tests/Feature/Fieldtypes/SlugTest.php +++ b/tests/Feature/Fieldtypes/SlugTest.php @@ -36,8 +36,10 @@ public function slugProvider() 'single_word' => ['one', '-', 'en', 'one'], 'one-two-three' => ['one two three', '-', 'en', 'one-two-three'], 'apples' => ["Apple's", '-', 'en', 'apples'], - 'single_smart_quotes' => ['GTA Online’s latest news: “huge map”', '-', 'en', 'gta-onlines-latest-news-huge-map'], + 'smart_quotes' => ['Statamic’s latest feature: “Duplicator”', '-', 'en', 'statamics-latest-feature-duplicator'], 'highens_separated_by_spaces' => ['Block - Hero', '-', 'en', 'block-hero'], + 'chinese_characters' => ['你好,世界', '-', 'ch', 'ni-hao-shi-jie'], + 'german_characters' => ['Björn Müller', '-', 'de', 'bjoern-mueller'], ]; } } From a492019c6cfc69c0bd7c3ca43ef0e7382e60e76f Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Wed, 31 Jan 2024 11:51:21 +0000 Subject: [PATCH 05/44] wip --- tests/Feature/Fieldtypes/SlugTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Feature/Fieldtypes/SlugTest.php b/tests/Feature/Fieldtypes/SlugTest.php index 7f0c0f2f08b..556d38fc80e 100644 --- a/tests/Feature/Fieldtypes/SlugTest.php +++ b/tests/Feature/Fieldtypes/SlugTest.php @@ -34,7 +34,7 @@ public function slugProvider() { return [ 'single_word' => ['one', '-', 'en', 'one'], - 'one-two-three' => ['one two three', '-', 'en', 'one-two-three'], + 'multiple_words' => ['one two three', '-', 'en', 'one-two-three'], 'apples' => ["Apple's", '-', 'en', 'apples'], 'smart_quotes' => ['Statamic’s latest feature: “Duplicator”', '-', 'en', 'statamics-latest-feature-duplicator'], 'highens_separated_by_spaces' => ['Block - Hero', '-', 'en', 'block-hero'], From 0ea36f42f26d5531fbbafc90d52fd0ff7e101af7 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Wed, 31 Jan 2024 12:09:38 +0000 Subject: [PATCH 06/44] Change parameter names --- .../Controllers/CP/Fieldtypes/SlugFieldtypeController.php | 6 +++--- tests/Feature/Fieldtypes/SlugTest.php | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Http/Controllers/CP/Fieldtypes/SlugFieldtypeController.php b/src/Http/Controllers/CP/Fieldtypes/SlugFieldtypeController.php index 93209d089c0..fc839f016fd 100644 --- a/src/Http/Controllers/CP/Fieldtypes/SlugFieldtypeController.php +++ b/src/Http/Controllers/CP/Fieldtypes/SlugFieldtypeController.php @@ -11,12 +11,12 @@ class SlugFieldtypeController extends CpController public function generate(Request $request) { $validated = $request->validate([ - 'from' => ['required'], - 'separator' => ['required'], + 'text' => ['required'], + 'glue' => ['required'], 'language' => ['required'], ]); - $slug = Str::slug($validated['from'], $validated['separator'], $validated['language']); + $slug = Str::slug($validated['text'], $validated['glue'], $validated['language']); return [ 'slug' => $slug, diff --git a/tests/Feature/Fieldtypes/SlugTest.php b/tests/Feature/Fieldtypes/SlugTest.php index 556d38fc80e..538049f96a6 100644 --- a/tests/Feature/Fieldtypes/SlugTest.php +++ b/tests/Feature/Fieldtypes/SlugTest.php @@ -15,13 +15,13 @@ class SlugTest extends TestCase * * @dataProvider slugProvider */ - public function it_generates_a_slug($from, $separator, $language, $expected) + public function it_generates_a_slug($text, $glue, $language, $expected) { $this ->actingAs(tap(User::make()->makeSuper())->save()) ->post('/cp/fieldtypes/slug', [ - 'from' => $from, - 'separator' => $separator, + 'text' => $text, + 'glue' => $glue, 'language' => $language, ]) ->assertOk() From 0eb166c79787095935882bc9a47a082cf468125d Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Wed, 31 Jan 2024 12:11:21 +0000 Subject: [PATCH 07/44] Refactor slugify helper Because we're now returning a Promise from this, I'll need to do some tweaking in all the places we use the $slugify helper. --- resources/js/plugins/slugify.js | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/resources/js/plugins/slugify.js b/resources/js/plugins/slugify.js index 59ce9a715d0..5613c4b8ec8 100644 --- a/resources/js/plugins/slugify.js +++ b/resources/js/plugins/slugify.js @@ -1,4 +1,4 @@ -import getSlug from 'speakingurl'; +import axios from 'axios'; export default { install(Vue, options) { @@ -7,22 +7,11 @@ export default { const sites = Statamic.$config.get('sites'); const site = sites.find(site => site.handle === selectedSite); lang = lang ?? site?.lang ?? Statamic.$config.get('lang'); - const custom = Statamic.$config.get(`charmap.${lang}`) ?? {}; - // Remove apostrophes in all languages - custom["'"] = ""; - - // Remove smart single quotes - custom["’"] = ""; - - // Prevent `Block - Hero` turning into `block_-_hero` - custom[" - "] = " "; - - return getSlug(text, { - separator: glue || '-', - lang, - custom, - symbols: Statamic.$config.get('asciiReplaceExtraSymbols') + return new Promise((resolve, reject) => { + axios.post(cp_url('fieldtypes/slug'), { text, glue, language: lang }) + .then(response => resolve(response.data.slug)) + .catch(error => reject(error)); }); }; } From e7b9d3af9c6f0aa29bff0e3a83ef26b0d5a7daa6 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Wed, 31 Jan 2024 12:38:19 +0000 Subject: [PATCH 08/44] Make the Slugify component work --- resources/js/components/Slugify.vue | 30 ++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/resources/js/components/Slugify.vue b/resources/js/components/Slugify.vue index e0ce91e703a..481be9f2fba 100644 --- a/resources/js/components/Slugify.vue +++ b/resources/js/components/Slugify.vue @@ -26,26 +26,15 @@ export default { } }, - computed: { + watch: { - slug() { + from(from) { if (!this.shouldSlugify) return this.to; - if (!this.from) return ''; - return this.$slugify(this.from, this.separator, this.language); - } - - }, - - watch: { + if (!from) return this.$emit('slugified', ''); - to(to) { - if (to !== this.slug) this.shouldSlugify = false; + this.slugify(); }, - slug(slug) { - this.$emit('slugified', slug); - } - }, created() { @@ -62,6 +51,17 @@ export default { reset() { if (this.enabled) this.shouldSlugify = true; + }, + + slugify() { + return new Promise((resolve, reject) => { + this.$slugify(this.from, this.separator, this.language).then((slug) => { + this.$emit('slugified', slug); + resolve(slug); + }).catch((error) => { + reject(error); + }); + }); } } From 5222823cca0cae72908e2e83761cad1ba73f5d75 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Wed, 31 Jan 2024 16:50:43 +0000 Subject: [PATCH 09/44] Change how handles are generated on create forms & add debounce --- resources/js/components/collections/CreateForm.vue | 6 +++--- resources/js/components/fieldsets/CreateForm.vue | 6 +++--- resources/js/components/forms/CreateForm.vue | 6 +++--- resources/js/components/globals/Create.vue | 6 +++--- resources/js/components/navigation/CreateForm.vue | 7 +++---- resources/js/components/roles/PublishForm.vue | 6 +++--- resources/js/components/taxonomies/CreateForm.vue | 6 +++--- 7 files changed, 21 insertions(+), 22 deletions(-) diff --git a/resources/js/components/collections/CreateForm.vue b/resources/js/components/collections/CreateForm.vue index c73e1923773..18d13d2e104 100644 --- a/resources/js/components/collections/CreateForm.vue +++ b/resources/js/components/collections/CreateForm.vue @@ -48,9 +48,9 @@ export default { }, watch: { - 'title': function(val) { - this.handle = this.$slugify(val, '_'); - } + title: _.debounce(function(value) { + this.$slugify(value, '_').then(handle => this.handle = handle); + }, 500) }, computed: { diff --git a/resources/js/components/fieldsets/CreateForm.vue b/resources/js/components/fieldsets/CreateForm.vue index 45291680c93..8825bd37bbb 100644 --- a/resources/js/components/fieldsets/CreateForm.vue +++ b/resources/js/components/fieldsets/CreateForm.vue @@ -48,9 +48,9 @@ export default { }, watch: { - 'title': function(val) { - this.handle = this.$slugify(val, '_'); - } + title: _.debounce(function(value) { + this.$slugify(value, '_').then(handle => this.handle = handle); + }, 500) }, computed: { diff --git a/resources/js/components/forms/CreateForm.vue b/resources/js/components/forms/CreateForm.vue index 86a67b0d13d..cbcdf7d4c8c 100644 --- a/resources/js/components/forms/CreateForm.vue +++ b/resources/js/components/forms/CreateForm.vue @@ -48,9 +48,9 @@ export default { }, watch: { - 'title': function(val) { - this.handle = this.$slugify(val, '_'); - } + title: _.debounce(function(value) { + this.$slugify(value, '_').then(handle => this.handle = handle); + }, 500) }, computed: { diff --git a/resources/js/components/globals/Create.vue b/resources/js/components/globals/Create.vue index ac42165c1c4..5c401b63e74 100644 --- a/resources/js/components/globals/Create.vue +++ b/resources/js/components/globals/Create.vue @@ -47,9 +47,9 @@ export default { }, watch: { - 'title': function(val) { - this.handle = this.$slugify(val, '_'); - } + title: _.debounce(function(value) { + this.$slugify(value, '_').then(handle => this.handle = handle); + }, 500) }, computed: { diff --git a/resources/js/components/navigation/CreateForm.vue b/resources/js/components/navigation/CreateForm.vue index 32ef88bb65f..c777a5de91b 100644 --- a/resources/js/components/navigation/CreateForm.vue +++ b/resources/js/components/navigation/CreateForm.vue @@ -31,7 +31,6 @@