From 9d80dd0b893d3dcd772b2ab9a4fe1359841387dd Mon Sep 17 00:00:00 2001 From: Alexis Gousseau Date: Tue, 6 Jan 2026 09:53:08 +0100 Subject: [PATCH 01/15] feat: :sparkles: set home in dynamic page --- app/Http/Controllers/Front/HomeController.php | 11 - .../DynamicPages/StoreDynamicPageRequest.php | 13 +- app/Models/DynamicPage.php | 31 +- .../DynamicPageRouteServiceProvider.php | 2 +- ...for_racine_page_in_dynamic_pages_table.php | 32 + lang/en/back.php | 3 + lang/en/front.php | 1 + lang/en/validation.php | 1 + lang/fr/back.php | 3 + lang/fr/front.php | 1 + lang/fr/validation.php | 1 + .../ts/back/Components/Inputs/ModelSelect.tsx | 10 + .../back/Components/Inputs/Slugifycator.tsx | 5 +- .../ts/back/Components/Layout/AppSidebar.tsx | 2 +- resources/ts/back/Pages/DynamicPages/Form.tsx | 34 +- .../ts/back/Pages/DynamicPages/Index.tsx | 23 +- resources/ts/back/Pages/DynamicPages/Show.tsx | 13 + resources/ts/back/Pages/Errors/Default.tsx | 6 +- resources/ts/back/types/global.d.ts | 2 + .../front/brands-icons/brands-icons.blade.php | 6 +- .../breadcrumbs/breadcrumb-layout.blade.php | 2 +- resources/views/front/pages/accueil.blade.php | 130 ++++ resources/views/front/pages/error.blade.php | 4 +- resources/views/front/pages/home.blade.php | 674 ------------------ .../front/pages/notre-engagement.blade.php | 1 + routes/breadcrumbs-front.php | 122 ++-- routes/web-front.php | 4 - 27 files changed, 368 insertions(+), 769 deletions(-) create mode 100644 database/migrations/2026_01_08_100318_add_boolean_for_racine_page_in_dynamic_pages_table.php create mode 100644 resources/views/front/pages/accueil.blade.php delete mode 100644 resources/views/front/pages/home.blade.php diff --git a/app/Http/Controllers/Front/HomeController.php b/app/Http/Controllers/Front/HomeController.php index 669c8d94..c35a8f46 100644 --- a/app/Http/Controllers/Front/HomeController.php +++ b/app/Http/Controllers/Front/HomeController.php @@ -14,17 +14,6 @@ use Uwil\LaravelCommon\Controllers\Controller; class HomeController extends Controller { - /** - * Show home page. - * - * @return \Illuminate\Contracts\View\View - */ - public function homepage(): \Illuminate\Contracts\View\View - { - return view('front.pages.home'); - } - - /** * unsubscribeNewsletter method * diff --git a/app/Http/Requests/Back/DynamicPages/StoreDynamicPageRequest.php b/app/Http/Requests/Back/DynamicPages/StoreDynamicPageRequest.php index f8c27af2..aaa3faea 100644 --- a/app/Http/Requests/Back/DynamicPages/StoreDynamicPageRequest.php +++ b/app/Http/Requests/Back/DynamicPages/StoreDynamicPageRequest.php @@ -34,6 +34,8 @@ class StoreDynamicPageRequest extends UpdateDynamicFieldRequest $this->merge([ 'config_id' => $this->config_id != 0 ? $this->config_id : null, 'isLocked' => $this->boolean('isLocked'), + 'isRoot' => $this->boolean('isRoot'), + 'slug' => $this->boolean('isRoot') ? "" : $this->slug, ]); if ($this->config_id) { parent::prepareForValidation(); @@ -50,12 +52,19 @@ class StoreDynamicPageRequest extends UpdateDynamicFieldRequest { $rules = [ 'title' => 'required|string|between:3,255', - 'slug' => 'required|string|regex:/^[-A-Za-z0-9]{3,255}$/', 'page_id' => 'sometimes|nullable|numeric|exists:dynamic_pages,id', 'config_id' => 'sometimes|nullable|numeric|exists:page_configs,id', 'status' => ['required', new Enum(PageStatusEnum::class)], 'isLocked' => 'required|boolean', + 'isRoot' => 'required|boolean', ]; + + if ($this->boolean('isRoot')) { + $rules['slug'] = ['present', 'nullable']; + } else { + $rules['slug'] = ['required', 'string', 'regex:/^[-A-Za-z0-9]{3,255}$/']; + } + if ($this->config_id) { $rules = \array_merge( ['content' => 'required|array|min:1'], @@ -67,6 +76,7 @@ class StoreDynamicPageRequest extends UpdateDynamicFieldRequest 'content' => ['required', 'string', new WysiwygLength()], ], $rules); } + return $rules; } @@ -83,6 +93,7 @@ class StoreDynamicPageRequest extends UpdateDynamicFieldRequest 'config_id' => trans_choice('models.classes.page-config', 1), 'status' => trans('validation.custom.status'), 'isLocked' => trans('validation.custom.locked'), + 'isRoot' => trans('validation.custom.root'), ]; if ($this->config_id) { $attributes = \array_merge( diff --git a/app/Models/DynamicPage.php b/app/Models/DynamicPage.php index a6377033..f7476247 100644 --- a/app/Models/DynamicPage.php +++ b/app/Models/DynamicPage.php @@ -31,6 +31,7 @@ use Uwil\LaravelCommon\Helpers\FileHelper; * @property integer|null $page_id Page associated to the dynamic page. * @property \App\Enums\DynamicPages\PageStatusEnum $status Status of the dynamic page. * @property boolean $isLocked Locked status of dynamic page. + * @property boolean $isRoot Root status of dynamic page. * @property-read \Illuminate\Support\Carbon $created_at Date on which the dynamic page was created. * @property-read \Illuminate\Support\Carbon $updated_at Date on which the dynamic page was updated. * @property-read \Illuminate\Support\Carbon $deleted_at Date on which the dynamic page was updated. @@ -58,6 +59,7 @@ class DynamicPage extends Model implements TranslatableModel 'config_id', 'status', 'isLocked', + 'isRoot', ]; /** @@ -69,6 +71,7 @@ class DynamicPage extends Model implements TranslatableModel 'content' => 'array', 'status' => PageStatusEnum::class, 'isLocked' => 'boolean', + 'isRoot' => 'boolean', ]; /** @@ -122,6 +125,7 @@ class DynamicPage extends Model implements TranslatableModel ]); } $model->status = PageStatusEnum::draft; + $model->isRoot = false; }); static::forceDeleted(function (self $model) { // TODO: update this after recursive deleting images. @@ -223,14 +227,33 @@ class DynamicPage extends Model implements TranslatableModel private static function manageFields(self $model): void { // Check unique fields. - $slug = $model->slug ?? Str::slug($model->title); - TranslationHelper::assertTranslationFieldIsUnique($model, $model->title, 'title'); - TranslationHelper::assertTranslationFieldIsUnique($model, $slug, 'slug'); - $model->setAttribute('slug', $slug); + if($model->isRoot) { + self::assertRootIsUnique($model); + } else { + $slug = $model->slug ?? Str::slug($model->title); + TranslationHelper::assertTranslationFieldIsUnique($model, $model->title, 'title'); + TranslationHelper::assertTranslationFieldIsUnique($model, $slug, 'slug'); + $model->setAttribute('slug', $slug); + } // Check translations. self::validateFallbackTranslations($model); } + /** + * Assert there is only one root page. + * + * @param self $model + * @return void + */ + public static function assertRootIsUnique(self $model): void + { + $oldDynamicPageRoot = self::getModelsCached()->where('isRoot', true)->first(); + if ($oldDynamicPageRoot) { + $oldDynamicPageRoot->isRoot = false; + $oldDynamicPageRoot->saveQuietly(); + } + } + /** * Assert fallback locale translation is present. * diff --git a/app/Providers/DynamicPageRouteServiceProvider.php b/app/Providers/DynamicPageRouteServiceProvider.php index b0ded6c9..9ba17f7c 100644 --- a/app/Providers/DynamicPageRouteServiceProvider.php +++ b/app/Providers/DynamicPageRouteServiceProvider.php @@ -53,7 +53,7 @@ class DynamicPageRouteServiceProvider extends ServiceProvider $slug = $dynamicPageModel->getTranslation('slug', $locale); // If no slug for the locale → skip. - if (!$slug) continue; + if (!empty($slug) && !$slug) continue; // Retrieves the parent segments for this locale. $segments = DynamicPageHelper::getAllDynamicPageSegments( diff --git a/database/migrations/2026_01_08_100318_add_boolean_for_racine_page_in_dynamic_pages_table.php b/database/migrations/2026_01_08_100318_add_boolean_for_racine_page_in_dynamic_pages_table.php new file mode 100644 index 00000000..2c1f6b51 --- /dev/null +++ b/database/migrations/2026_01_08_100318_add_boolean_for_racine_page_in_dynamic_pages_table.php @@ -0,0 +1,32 @@ +boolean('isRoot')->default(false)->comment('Page root status.')->after('isLocked'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down(): void + { + Schema::table('dynamic_pages', function (Blueprint $table) { + $table->dropColumn('isRoot'); + }); + } +}; diff --git a/lang/en/back.php b/lang/en/back.php index 60eb7d48..0aac6f7e 100644 --- a/lang/en/back.php +++ b/lang/en/back.php @@ -86,6 +86,8 @@ return [ "form_subtitle_visibility" => "Manage account display.", "form_title_isLocked" => "Restriction", "form_subtitle_isLocked" => "Manage page locking.", + "form_title_page_accessibility" => "Accessibility", + "form_subtitle_page_accessibility" => "Manage access the dynamic page of the site.", "form_associate_to" => ":attribute associated to this model|:attribute associated to this model", "form_associate_to_plural" => ":attribute associated to this model|:attribute associated to this model", "form_title_config" => "Setup", @@ -124,6 +126,7 @@ return [ "list_children_needed" => "Children are required for this type :enumLabel", "empty_warning" => "No data recorded", "page_locked" => "This page is currently locked", + "page_root" => "This page is currently home", "unsupported_type" => "Unsupported type:", "missing_translations" => "The translation of the :field for the language :locales is missing.|The translation of the :field for :locales languages are missing.", diff --git a/lang/en/front.php b/lang/en/front.php index fea40ca0..89d1079b 100644 --- a/lang/en/front.php +++ b/lang/en/front.php @@ -4,6 +4,7 @@ return [ "multiple_choice" => 'multiple choice', "required_field" => 'compulsory fields', "go_to_the_back_end" => "access the back-office", + "back_to_homepage" => "Back to the homepage", "homepage" => "LDC", "nos-engagements" => [ 'key' => "Presentation of our commitments", diff --git a/lang/en/validation.php b/lang/en/validation.php index 46d99792..ced66178 100644 --- a/lang/en/validation.php +++ b/lang/en/validation.php @@ -240,6 +240,7 @@ return [ "validation" => "validation", "reference" => "reference", "locked" => "locked", + "root" => "root", "field" => "field", "key" => "unique key", "slug" => "slug", diff --git a/lang/fr/back.php b/lang/fr/back.php index c0807425..c55edf1f 100644 --- a/lang/fr/back.php +++ b/lang/fr/back.php @@ -85,6 +85,8 @@ return [ "form_subtitle_visibility" => "Gérer l'affichage du compte.", "form_title_isLocked" => "Restriction", "form_subtitle_isLocked" => "Gérer le vérouillage de la page.", + "form_title_page_accessibility" => "Accessibilité", + "form_subtitle_page_accessibility" => "Gérer l’accès à la page dynamique du site.", "form_associate_to" => ":attribute associé au modèle|:attribute associée au modèle", "form_associate_to_plural" => ":attribute associés au modèle|:attribute associées au modèle", "form_title_config" => "Réglage", @@ -124,6 +126,7 @@ return [ "list_children_needed" => "Des enfants sont nécessaires pour ce type :enumLabel", "empty_warning" => "Aucune donnée enregistrée", "page_locked" => "Cette page est actuellement vérouillée", + "page_root" => "Cette page est actuellement l'accueil", "unsupported_type" => "Type non supporté:", "missing_translations" => "La traduction du champs :field pour la langue :locales est manquante.|La traduction du champs :field pour l es langues :locales sont manquantes.", diff --git a/lang/fr/front.php b/lang/fr/front.php index 61354488..ab9de292 100644 --- a/lang/fr/front.php +++ b/lang/fr/front.php @@ -4,6 +4,7 @@ return [ "multiple_choice" => 'choix multiple', "required_field" => 'champs obligatoires', "go_to_the_back_end" => "accéder au back-office", + "back_to_homepage" => "Revenir à la page d'accueil", "homepage" => "LDC", "nos-engagements" => "nos engagements", "nos-engagements" => [ diff --git a/lang/fr/validation.php b/lang/fr/validation.php index 81e87aac..b13eed7c 100644 --- a/lang/fr/validation.php +++ b/lang/fr/validation.php @@ -240,6 +240,7 @@ return [ "validation" => "validation", "reference" => "référence", "locked" => "vérouillé", + "root" => "page racine (accueil)", "field" => "champs", "key" => "clé", "slug" => "chemin d'accès", diff --git a/resources/ts/back/Components/Inputs/ModelSelect.tsx b/resources/ts/back/Components/Inputs/ModelSelect.tsx index 7952d4c9..db9d5012 100644 --- a/resources/ts/back/Components/Inputs/ModelSelect.tsx +++ b/resources/ts/back/Components/Inputs/ModelSelect.tsx @@ -244,6 +244,16 @@ export default function ModelSelect({ return () => clearTimeout(handler); }, [search, targetModel, field]); + // Update when selected is updated. + useEffect(() => { + if (selected !== null) { + handleSelect(selected); + } else { + setSelectedId(null); + setSelectedValue(null); + } + }, [selected]); + return (
-
-
-
-
-
Activez les alertes pour recevoir par email les nouvelles offres - correspondant à vos critères
-
-
Métier
- -
-
-
Type de contrat
- -
-
-
Temps de travail
- -
-
-
Email*
- -
-
- - -
+
@endif @if (isset($pageFinance))
From 10ac83d457c50ee6b9b80fa3e08f1fc8cf2b0a9a Mon Sep 17 00:00:00 2001 From: Alexis Gousseau Date: Fri, 9 Jan 2026 10:55:52 +0100 Subject: [PATCH 05/15] fix: :fire: remove useless middleware on front routes --- bootstrap/app.php | 10 +++------- routes/web-front.php | 5 ----- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/bootstrap/app.php b/bootstrap/app.php index f3a53a8a..0e49db19 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -30,13 +30,9 @@ return Application::configure(basePath: dirname(__DIR__)) \Illuminate\Session\Middleware\StartSession::class, ]); $middleware->alias([ - 'auth' => \Uwil\LaravelBackend\Middlewares\Authenticate::class, - 'guest' => \Uwil\LaravelBackend\Middlewares\RedirectIfAuthenticated::class, - 'lang' => \Uwil\LaravelBackend\Middlewares\Lang::class, - 'localeSessionRedirect' => \Mcamara\LaravelLocalization\Middleware\LocaleSessionRedirect::class, - 'localeCookieRedirect' => \Mcamara\LaravelLocalization\Middleware\LocaleCookieRedirect::class, - 'localizationRedirect' => \Mcamara\LaravelLocalization\Middleware\LaravelLocalizationRedirectFilter::class, - 'localeViewPath' => \Mcamara\LaravelLocalization\Middleware\LaravelLocalizationViewPath::class, + 'auth' => \Uwil\LaravelBackend\Middlewares\Authenticate::class, + 'guest' => \Uwil\LaravelBackend\Middlewares\RedirectIfAuthenticated::class, + 'lang' => \Uwil\LaravelBackend\Middlewares\Lang::class, ]); }) ->withExceptions(function (Exceptions $exceptions) { diff --git a/routes/web-front.php b/routes/web-front.php index 4741b254..b6294c51 100644 --- a/routes/web-front.php +++ b/routes/web-front.php @@ -10,11 +10,6 @@ Route::group( [ 'prefix' => LaravelLocalization::setLocale(), 'as' => 'front.', - 'middleware' => [ - 'localeSessionRedirect', - 'localizationRedirect', - 'localeViewPath' - ] ], function () { Route::name('contact.')->controller(ContactController::class)->group(function () { From 51ad519c998d4add23c4167be884ee633b5a8246 Mon Sep 17 00:00:00 2001 From: Alexis Gousseau Date: Fri, 9 Jan 2026 11:02:39 +0100 Subject: [PATCH 06/15] fix: :bug: set id_key from slug or title when the dynamic page is root --- app/Models/DynamicPage.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/Models/DynamicPage.php b/app/Models/DynamicPage.php index f7476247..2eb400b1 100644 --- a/app/Models/DynamicPage.php +++ b/app/Models/DynamicPage.php @@ -105,7 +105,10 @@ class DynamicPage extends Model implements TranslatableModel { static::creating(function (self $model) { self::manageFields($model); - $model->id_key = $model->slug; + // Set id_key from slug or title when the dynamic page is root. + $model->id_key = isset($model->slug) && !empty($model->slug) + ? $model->slug + : Str::slug($model->title); }); static::updating(function (self $model) { self::manageFields($model); From 1c48863f6965f90025eb38849c9b1c7c3533d11a Mon Sep 17 00:00:00 2001 From: Alexis Gousseau Date: Fri, 9 Jan 2026 11:03:37 +0100 Subject: [PATCH 07/15] f: :wheelchair: update icons from dynamic home page --- resources/ts/back/Pages/DynamicPages/Form.tsx | 4 ++-- resources/ts/back/Pages/DynamicPages/Index.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/resources/ts/back/Pages/DynamicPages/Form.tsx b/resources/ts/back/Pages/DynamicPages/Form.tsx index e0bb0595..ed8e90c0 100644 --- a/resources/ts/back/Pages/DynamicPages/Form.tsx +++ b/resources/ts/back/Pages/DynamicPages/Form.tsx @@ -1,6 +1,6 @@ // * Packages import { useForm, usePage } from "@inertiajs/react"; -import { AwardIcon, BookTextIcon, CogIcon, EyeIcon, GavelIcon, ShieldAlertIcon } from "lucide-react"; +import { LinkIcon, BookTextIcon, CogIcon, EyeIcon, ShieldAlertIcon } from "lucide-react"; import { route } from "ziggy-js"; import { useEffect, useState } from "react"; // * Components shadcn @@ -231,7 +231,7 @@ export default function Form({ } + icon={} description={useTrans("back.form_subtitle_page_accessibility")} className="sm:grid-cols-2 gap-4" > diff --git a/resources/ts/back/Pages/DynamicPages/Index.tsx b/resources/ts/back/Pages/DynamicPages/Index.tsx index dea7b695..8c4fb9ef 100644 --- a/resources/ts/back/Pages/DynamicPages/Index.tsx +++ b/resources/ts/back/Pages/DynamicPages/Index.tsx @@ -4,8 +4,8 @@ import { AlertCircleIcon, ArchiveIcon, ArrowLeftIcon, - AwardIcon, CogIcon, + HouseIcon, LockIcon, PlusIcon, StickyNoteIcon, @@ -86,7 +86,7 @@ export default function Index({ {row.original.isRoot && ( - + {useTrans("back.page_root")} From f9808fa11030db338a6245e8717bd51885885fa0 Mon Sep 17 00:00:00 2001 From: Alexis Gousseau Date: Fri, 9 Jan 2026 11:04:40 +0100 Subject: [PATCH 08/15] fix: :bug: determines accessibility to the home page --- resources/ts/back/Components/Layout/AppSidebar.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/resources/ts/back/Components/Layout/AppSidebar.tsx b/resources/ts/back/Components/Layout/AppSidebar.tsx index cb8b25e5..ef8ddf56 100644 --- a/resources/ts/back/Components/Layout/AppSidebar.tsx +++ b/resources/ts/back/Components/Layout/AppSidebar.tsx @@ -102,8 +102,10 @@ export default function AppSidebar() { }, { title: useTrans("back.your_website"), - link: route("front.dynamic-pages.accueil." + appEnv.locale), - show: true, + link: route().has("front.dynamic-pages.accueil." + appEnv.locale) + ? route("front.dynamic-pages.accueil." + appEnv.locale) + : "", + show: route().has("front.dynamic-pages.accueil." + appEnv.locale), icon: SquareArrowOutUpRightIcon, }, { From 1bd68b8ec377d5e472a134c807a7427a1eb0bfaa Mon Sep 17 00:00:00 2001 From: Alexis Gousseau Date: Fri, 9 Jan 2026 11:07:11 +0100 Subject: [PATCH 09/15] fix: :bug: safely access SEO fields with data_get --- resources/views/front/pages/accueil.blade.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/views/front/pages/accueil.blade.php b/resources/views/front/pages/accueil.blade.php index d9eeb7ee..0fec7b20 100644 --- a/resources/views/front/pages/accueil.blade.php +++ b/resources/views/front/pages/accueil.blade.php @@ -1,7 +1,7 @@ @extends('front.layouts.layout') -@section('title', $dynamicPageModel->content['list_seo_fields'][0]['string_seo_title']) -@section('description', $dynamicPageModel->content['list_seo_fields'][0]['textarea_seo_description']) +@section('title', data_get($dynamicPageModel, 'content.list_seo_fields.0.string_seo_title')) +@section('description', data_get($dynamicPageModel, 'content.list_seo_fields.0.textarea_seo_description')) @section('content') {{-- VIDEO --}} From 7ba9486a98151c9611a7a21565a4aa56e2f3ec2a Mon Sep 17 00:00:00 2001 From: Alexis Gousseau Date: Fri, 9 Jan 2026 11:44:42 +0100 Subject: [PATCH 10/15] fix: :wheelchair: improve search of model select input (identification/icons) --- .../ts/back/Components/Inputs/ModelSelect.tsx | 43 +++++++++++++++---- .../Wysiwyg/Plugins/Link/LinkButton.tsx | 2 +- 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/resources/ts/back/Components/Inputs/ModelSelect.tsx b/resources/ts/back/Components/Inputs/ModelSelect.tsx index db9d5012..db5f9992 100644 --- a/resources/ts/back/Components/Inputs/ModelSelect.tsx +++ b/resources/ts/back/Components/Inputs/ModelSelect.tsx @@ -1,6 +1,6 @@ // * Packages import { usePage } from "@inertiajs/react"; -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import axios from "axios"; import { CheckIcon, ChevronLeftIcon, ChevronRightIcon, ChevronsUpDownIcon, LoaderCircleIcon, SearchIcon, XIcon } from "lucide-react"; import { route } from "ziggy-js"; @@ -76,6 +76,9 @@ export default function ModelSelect({ // String research in models collection. const [search, setSearch] = useState(""); + // Search identification. + const requestIdRef = useRef(0); + // Field value of the selected model. const [selectedValue, setSelectedValue] = useState(null); @@ -88,6 +91,9 @@ export default function ModelSelect({ const [pageSize] = useState(5); const [lastPage, setLastPage] = useState(1); + // Hack to block closing popover when changing pages. + const [blockingClosePopover, setBlockingClosePopover] = useState(false); + /** * Get the identification field of specific option. * @param option Model @@ -190,6 +196,8 @@ export default function ModelSelect({ * @return void */ const getData = async (pageNumber = 1) => { + // Increment search id. + const requestId = ++requestIdRef.current; setLoading(true); try { const data = await fetchOptions(pageNumber) as { @@ -197,6 +205,8 @@ export default function ModelSelect({ field: string, model: DynamicPageModel, }; + // Ignore obsolete answers. + if (requestId !== requestIdRef.current) return; if (data.model) { const value = extractLocalized(data.model, data.field, locale); setSelectedValue(value); @@ -208,11 +218,17 @@ export default function ModelSelect({ setPage(data.items.current_page); setLastPage(data.items.last_page ?? 1); } catch (err) { + // Ignore obsolete errors. + if (requestId !== requestIdRef.current) return; console.error(err); setOptions([]); setLastPage(1); } finally { - setLoading(false); + // Only the last call can stop the loading. + if (requestId === requestIdRef.current) { + setLoading(false); + setTimeout(() => setBlockingClosePopover(false), 100) + } } }; @@ -223,12 +239,15 @@ export default function ModelSelect({ */ const goToPage = async (pageNumber: number) => { if (pageNumber < 1 || pageNumber > lastPage) return; - getData(pageNumber); + setBlockingClosePopover(true); + await getData(pageNumber); }; // Fetch with debounce. useEffect(() => { - setDynamicField(field ?? null); + if (disabled) { + return; + } // Reset options list if empty search or null if (targetModel && search.length === 0) { @@ -238,7 +257,7 @@ export default function ModelSelect({ // Nothing to do if search is below 3 caracters. if (search.length < 3) return; - + // Get updated options list with search. const handler = setTimeout(() => getData(), 300); return () => clearTimeout(handler); @@ -274,13 +293,21 @@ export default function ModelSelect({ )} > {selectedValue ?? placeholder} - + {loading ? : } - + { + if (blockingClosePopover) { + e.preventDefault(); + return; + } + }} + >
- {loading ? : } +
handleSelectEntry(value)} parentErrors={errors} /> From 758a72b3c7368346becf4fe223aededd856a4951 Mon Sep 17 00:00:00 2001 From: Alexis Gousseau Date: Fri, 9 Jan 2026 17:24:14 +0100 Subject: [PATCH 11/15] fix: :bug: reorder wysiwyg extensions --- resources/sass/back/app.scss | 27 +- resources/sass/back/inputs/_wysiwyg.scss | 26 ++ .../Inputs/Wysiwyg/SimpleWysiwyg.tsx | 253 +++++++++--------- 3 files changed, 156 insertions(+), 150 deletions(-) create mode 100644 resources/sass/back/inputs/_wysiwyg.scss diff --git a/resources/sass/back/app.scss b/resources/sass/back/app.scss index 821f00f1..ba467341 100644 --- a/resources/sass/back/app.scss +++ b/resources/sass/back/app.scss @@ -1,5 +1,7 @@ @use '/node_modules/flag-icons/css/flag-icons.min.css'; @use './fonts'; +@use './inputs/wysiwyg'; + @tailwind base; @tailwind components; @tailwind utilities; @@ -117,30 +119,7 @@ html { } } -.richtext-outline, -.richtext-outline-1, -.richtext-shadow { - all: unset !important; -} - -// Wysiwyg classes. -.reactjs-tiptap-editor { - @apply rounded-md border border-input bg-background; -} - -.wysiwyg-error .reactjs-tiptap-editor { - @apply border-destructive/50; -} - -.tiptap .link { - cursor: pointer; -} - -/* stylelint-disable-next-line selector-class-pattern */ -.ProseMirror-focused { - @apply rounded-b ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2; -} - +// Multiselect input. .multiselect-singleline-scroll { scrollbar-color: hsl(var(--border) / 40%) transparent; scrollbar-width: thin; diff --git a/resources/sass/back/inputs/_wysiwyg.scss b/resources/sass/back/inputs/_wysiwyg.scss new file mode 100644 index 00000000..f4ada4f9 --- /dev/null +++ b/resources/sass/back/inputs/_wysiwyg.scss @@ -0,0 +1,26 @@ +.richtext-outline, +.richtext-outline-1, +.richtext-shadow { + all: unset !important; +} + +.reactjs-tiptap-editor { + @apply rounded-md border border-input bg-background; +} + +.wysiwyg-error .reactjs-tiptap-editor { + @apply border-destructive/50; +} + +.tiptap .link { + cursor: pointer; +} + +#tippy-4 { + z-index: 1 !important; +} + +/* stylelint-disable-next-line selector-class-pattern */ +.ProseMirror-focused { + @apply rounded-b ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2; +} \ No newline at end of file diff --git a/resources/ts/back/Components/Inputs/Wysiwyg/SimpleWysiwyg.tsx b/resources/ts/back/Components/Inputs/Wysiwyg/SimpleWysiwyg.tsx index d954d1fa..38a061eb 100644 --- a/resources/ts/back/Components/Inputs/Wysiwyg/SimpleWysiwyg.tsx +++ b/resources/ts/back/Components/Inputs/Wysiwyg/SimpleWysiwyg.tsx @@ -33,139 +33,140 @@ import useTrans from "@/hooks/use-translations"; import { cn } from "@/lib/utils"; type SimpleWysiwygProps = { - className?: string; - name: string; - model: string; - label: string; - value?: string; - id?: string; - placeholder?: string; - wordCounter?: boolean; - required?: boolean; - disabled?: boolean; - parentErrors?: Record | null, - // eslint-disable-next-line no-unused-vars - setData: (name: string, value: string) => void; + className?: string; + name: string; + model: string; + label: string; + value?: string; + id?: string; + placeholder?: string; + wordCounter?: boolean; + required?: boolean; + disabled?: boolean; + parentErrors?: Record | null, + // eslint-disable-next-line no-unused-vars + setData: (name: string, value: string) => void; }; export default function SimpleWysiwyg({ - className, - value = "", - model, - name, - label, - id = name as string, - placeholder = "back.enter_text_here", - wordCounter = true, - required = true, - disabled = false, - parentErrors = null, - setData, + className, + value = "", + model, + name, + label, + id = name as string, + placeholder = "back.enter_text_here", + wordCounter = true, + required = true, + disabled = false, + parentErrors = null, + setData, }: SimpleWysiwygProps) { - const { errors } = usePage().props; - const localErrors = parentErrors ?? errors; + const { errors } = usePage().props; + const localErrors = parentErrors ?? errors; - const extensions = [ - BaseKit.configure({ - placeholder: { - showOnlyCurrent: true, - placeholder: useTrans(placeholder), - }, - characterCount: false, - }), - Heading, - SearchAndReplace, - Bold, - Italic, - TextUnderline, - Strike, - BulletList, - OrderedList, - Indent, - TextAlign, - History, - Blockquote, - FontSize, - HorizontalRule, - Clear, - Color, - Image.configure({ - upload: async (file: File): Promise => { - // Prepare the form data. - const data = new FormData(); - data.append("upload", file); - data.append("folder", model); + const extensions = [ + BaseKit.configure({ + placeholder: { + showOnlyCurrent: true, + placeholder: useTrans(placeholder), + }, + characterCount: false, + }), + History, + SearchAndReplace, + Clear, + Heading, + Bold, + Italic, + TextUnderline, + Strike, + Color, + FontSize, + BulletList, + OrderedList, + Indent, + TextAlign, + Blockquote, + HorizontalRule, + Image.configure({ + upload: async (file: File): Promise => { + // Prepare the form data. + const data = new FormData(); + data.append("upload", file); + data.append("folder", model); - const uploadRoute = route("back.wysiwyg.upload"); + const uploadRoute = route("back.wysiwyg.upload"); - const header = { - headers: { - "Content-Type": "multipart/form-data", - }, - }; - - try { - const response = await axios.post(uploadRoute, data, header); - return response.data.url; - } catch (error: Error | unknown) { - throw new Error(`Erreur upload: ${(error as Error).message}`); - } - }, - }), - // Custom plugins. - Link, - ]; - - const [content, setContent] = useState(value); - const [version, setVersion] = useState(1); - - function stripHtmlTags(html: string) { - const temp = document.createElement('div'); - temp.innerHTML = html; - const text = temp.textContent || temp.innerText || ''; - return text.trim() === '' ? null : text; - } - const onChangeContent = (changedValue: string) => { - const value = stripHtmlTags(changedValue) ? changedValue : ''; - setContent(value); - setData(name, value); - }; - - const customOptions: UseEditorOptions = { - editorProps: { - attributes: { - required: String(required), - id, - }, - }, - }; - - useEffect(() => { - if (content !== value) { - setContent(value); - setVersion(version + 1); + try { + const response = await axios.post( + uploadRoute, + data, + { headers: { "Content-Type": "multipart/form-data" } } + ); + return response.data.url; + } catch (error: Error | unknown) { + throw new Error(`Erreur upload: ${(error as Error).message}`); } - }, [value]); + }, + }), + Link, + ]; - return ( -
- - {wordCounter && ]*>/g, "")} />} -
- -
- -
- ); + const [content, setContent] = useState(value); + const [version, setVersion] = useState(1); + + function stripHtmlTags(html: string) { + const temp = document.createElement('div'); + temp.innerHTML = html; + const text = temp.textContent || temp.innerText || ''; + return text.trim() === '' ? null : text; + } + + const onChangeContent = (changedValue: string) => { + const value = stripHtmlTags(changedValue) ? changedValue : ""; + setContent(value); + setData(name, value); + }; + + const customOptions: UseEditorOptions = { + editorProps: { + attributes: { + required: String(required), + id, + }, + }, + }; + + useEffect(() => { + if (content !== value) { + setContent(value); + setVersion(version + 1); + } + }, [value]); + + return ( +
+ + {wordCounter && ( + ]*>/g, "")} /> + )} +
+ +
+ +
+ ); } From 49884d510a621ba47646374a045afebb3a00a579 Mon Sep 17 00:00:00 2001 From: Alexis Gousseau Date: Mon, 12 Jan 2026 09:35:45 +0100 Subject: [PATCH 12/15] fix: :truck: rename page files in english --- resources/views/front/pages/{accueil.blade.php => home.blade.php} | 0 .../front/pages/{nous-rejoindre.blade.php => join-us.blade.php} | 0 .../front/pages/{nous-connaitre.blade.php => know-us.blade.php} | 0 .../{notre-engagement.blade.php => our-commitment.blade.php} | 0 .../{nos-engagements.blade.php => our-commitments.blade.php} | 0 .../front/pages/{notre-pole.blade.php => our-division.blade.php} | 0 .../front/pages/{nos-metiers.blade.php => our-jobs.blade.php} | 0 7 files changed, 0 insertions(+), 0 deletions(-) rename resources/views/front/pages/{accueil.blade.php => home.blade.php} (100%) rename resources/views/front/pages/{nous-rejoindre.blade.php => join-us.blade.php} (100%) rename resources/views/front/pages/{nous-connaitre.blade.php => know-us.blade.php} (100%) rename resources/views/front/pages/{notre-engagement.blade.php => our-commitment.blade.php} (100%) rename resources/views/front/pages/{nos-engagements.blade.php => our-commitments.blade.php} (100%) rename resources/views/front/pages/{notre-pole.blade.php => our-division.blade.php} (100%) rename resources/views/front/pages/{nos-metiers.blade.php => our-jobs.blade.php} (100%) diff --git a/resources/views/front/pages/accueil.blade.php b/resources/views/front/pages/home.blade.php similarity index 100% rename from resources/views/front/pages/accueil.blade.php rename to resources/views/front/pages/home.blade.php diff --git a/resources/views/front/pages/nous-rejoindre.blade.php b/resources/views/front/pages/join-us.blade.php similarity index 100% rename from resources/views/front/pages/nous-rejoindre.blade.php rename to resources/views/front/pages/join-us.blade.php diff --git a/resources/views/front/pages/nous-connaitre.blade.php b/resources/views/front/pages/know-us.blade.php similarity index 100% rename from resources/views/front/pages/nous-connaitre.blade.php rename to resources/views/front/pages/know-us.blade.php diff --git a/resources/views/front/pages/notre-engagement.blade.php b/resources/views/front/pages/our-commitment.blade.php similarity index 100% rename from resources/views/front/pages/notre-engagement.blade.php rename to resources/views/front/pages/our-commitment.blade.php diff --git a/resources/views/front/pages/nos-engagements.blade.php b/resources/views/front/pages/our-commitments.blade.php similarity index 100% rename from resources/views/front/pages/nos-engagements.blade.php rename to resources/views/front/pages/our-commitments.blade.php diff --git a/resources/views/front/pages/notre-pole.blade.php b/resources/views/front/pages/our-division.blade.php similarity index 100% rename from resources/views/front/pages/notre-pole.blade.php rename to resources/views/front/pages/our-division.blade.php diff --git a/resources/views/front/pages/nos-metiers.blade.php b/resources/views/front/pages/our-jobs.blade.php similarity index 100% rename from resources/views/front/pages/nos-metiers.blade.php rename to resources/views/front/pages/our-jobs.blade.php From 6a84e94f55f946c30afaf8dfd627cd4991bb22d8 Mon Sep 17 00:00:00 2001 From: Alexis Gousseau Date: Mon, 12 Jan 2026 10:31:33 +0100 Subject: [PATCH 13/15] fix: :bug: update unique key based on id_key and delete_at date in dynamic pages --- .../Back/DynamicPageController.php | 1 - ..._unique_key_of_id_key_in_dynamic_pages.php | 42 +++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 database/migrations/2026_01_12_102420_alter_unique_key_of_id_key_in_dynamic_pages.php diff --git a/app/Http/Controllers/Back/DynamicPageController.php b/app/Http/Controllers/Back/DynamicPageController.php index 132f1e2b..7ea8782f 100644 --- a/app/Http/Controllers/Back/DynamicPageController.php +++ b/app/Http/Controllers/Back/DynamicPageController.php @@ -123,7 +123,6 @@ class DynamicPageController extends Controller { return DB::transaction(function () use ($request) { $dynamicPageModel = new DynamicPage(); - $dynamicPageModel->inertiaFill($request->validated()); if ($dynamicPageModel->saveOrFail()) { diff --git a/database/migrations/2026_01_12_102420_alter_unique_key_of_id_key_in_dynamic_pages.php b/database/migrations/2026_01_12_102420_alter_unique_key_of_id_key_in_dynamic_pages.php new file mode 100644 index 00000000..da7b85c5 --- /dev/null +++ b/database/migrations/2026_01_12_102420_alter_unique_key_of_id_key_in_dynamic_pages.php @@ -0,0 +1,42 @@ +dropUnique('dynamic_pages_id_key_unique'); + }); + // Set new unique key with delete_at date. + Schema::table('dynamic_pages', function (Blueprint $table) { + $table->unique(['id_key', 'deleted_at'], 'dynamic_pages_id_key_deleted_at_unique'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down(): void + { + // Delete old key. + Schema::table('dynamic_pages', function (Blueprint $table) { + $table->dropUnique('dynamic_pages_id_key_deleted_at_unique'); + }); + // Set new unique key. + Schema::table('dynamic_pages', function (Blueprint $table) { + $table->unique('id_key', 'dynamic_pages_id_key_unique'); + }); + } +}; From 2194de1788bd9d78452c2e5428158e04941500fa Mon Sep 17 00:00:00 2001 From: Alexis Gousseau Date: Mon, 12 Jan 2026 10:46:42 +0100 Subject: [PATCH 14/15] git: :bug: get only path of url from getAccessModel method --- app/Http/Controllers/Back/InputsController.php | 6 +++--- app/Models/DynamicPage.php | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/Http/Controllers/Back/InputsController.php b/app/Http/Controllers/Back/InputsController.php index 5a114945..ed0e524b 100644 --- a/app/Http/Controllers/Back/InputsController.php +++ b/app/Http/Controllers/Back/InputsController.php @@ -120,15 +120,15 @@ class InputsController extends Controller * * @param integer $modelId Id of the target model. * @param integer $modelName Name of the target model. - * @return string + * @return string|null */ - private function getAccessModel(int $modelId, string $modelName): string + private function getAccessModel(int $modelId, string $modelName): string|null { $targetModel = $this->getClassFromTableName($modelName)::getModelsCached()->firstWhere('id', $modelId); if (!$targetModel) { return null; } - return route("front.$modelName.show", $targetModel->slug); + return parse_url(route("front.$modelName.show", $targetModel->slug), PHP_URL_PATH) ?? "/"; } /** diff --git a/app/Models/DynamicPage.php b/app/Models/DynamicPage.php index 2eb400b1..bcced317 100644 --- a/app/Models/DynamicPage.php +++ b/app/Models/DynamicPage.php @@ -304,16 +304,16 @@ class DynamicPage extends Model implements TranslatableModel * Get the url to access a specific resource. * * @param integer $id Id field of the target model. - * @return string + * @return string|null */ - public static function getAccessModel(int $id): string + public static function getAccessModel(int $id): string|null { $targetModel = self::getModelsCached()->firstWhere('id', $id); if (!$targetModel) { return null; } $url = RouteHelper::routeNameChecker("front.dynamic-pages.{$targetModel->id_key}"); - return parse_url($url, PHP_URL_PATH); + return parse_url($url, PHP_URL_PATH) ?? "/"; } // * RELATIONSHIPS From 213f59befe37959138f5456f4b22c5c733bb8286 Mon Sep 17 00:00:00 2001 From: Alexis Gousseau Date: Mon, 12 Jan 2026 11:23:27 +0100 Subject: [PATCH 15/15] fix: :lipstick: set flex on definitive path in GoToFrontView component --- resources/ts/back/Components/GoToFrontView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/ts/back/Components/GoToFrontView.tsx b/resources/ts/back/Components/GoToFrontView.tsx index b247f0da..de256205 100644 --- a/resources/ts/back/Components/GoToFrontView.tsx +++ b/resources/ts/back/Components/GoToFrontView.tsx @@ -75,7 +75,7 @@ export default function GoToFrontView({ target="_blank" rel="noreferrer" > -

{definitivePath}

+

{definitivePath}