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/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/Http/Controllers/Front/ContactController.php b/app/Http/Controllers/Front/ContactController.php index 6732d297..cb021d32 100644 --- a/app/Http/Controllers/Front/ContactController.php +++ b/app/Http/Controllers/Front/ContactController.php @@ -16,13 +16,23 @@ use LDC\LDCCRM\Models\SingleOptin; class ContactController { - public function index() + /** + * Show contact index page. + * + * @return \Illuminate\Contracts\View\View + */ + public function index(): \Illuminate\Contracts\View\View { $types = ContactFormType::toArray(); return view('front.pages.contact.index', compact('types')); } - public function show(string $slug) + /** + * Show contact show page. + * + * @return \Illuminate\Contracts\View\View + */ + public function show(string $slug): \Illuminate\Contracts\View\View { $enumType = collect(ContactFormType::all())->first(function ($enum) use ($slug) { return $enum->slug() === $slug; @@ -33,13 +43,18 @@ class ContactController return view('front.pages.contact.show', compact('enumType')); } - public function send(ContactRequest $request) + /** + * Contact send. + * + * @return \Illuminate\Http\RedirectResponse + */ + public function send(ContactRequest $request): \Illuminate\Http\RedirectResponse { - $data = $request->validated(); - $messageContact = new MailContactData($data); - $contactMessage = ""; - $hasContactError = false; - $newsletterMessage = null; + $data = $request->validated(); + $messageContact = new MailContactData($data); + $contactMessage = ""; + $hasContactError = false; + $newsletterMessage = null; $hasNewsletterError = false; try { dispatch(new SendContactMail($messageContact)) @@ -50,7 +65,7 @@ class ContactController 'exception' => $e ]); $hasContactError = true; - $contactMessage = trans('Une erreur est survenue lors de l\'envoi du message de contact.'); + $contactMessage = trans('Une erreur est survenue lors de l\'envoi du message de contact.'); } if (!empty($data['newsletter']) && $data['newsletter']) { try { @@ -62,21 +77,21 @@ class ContactController } report($e); $hasNewsletterError = true; - $newsletterMessage = trans('Une erreur est survenue lors de l\'inscription à la newsletter, merci de réessayer ultérieurement.'); + $newsletterMessage = trans('Une erreur est survenue lors de l\'inscription à la newsletter, merci de réessayer ultérieurement.'); } catch (CrmException $e) { report($e); $hasNewsletterError = true; - $newsletterMessage = trans('Une erreur est survenue lors de l\'inscription à la newsletter, merci de réessayer ultérieurement.'); + $newsletterMessage = trans('Une erreur est survenue lors de l\'inscription à la newsletter, merci de réessayer ultérieurement.'); } } ContactCount::updateOrInsert( - ['type' => $data['enumType']], + ['type' => $data['enumType']], ['submissions' => \DB::raw('submissions + 1')] ); $contactMessage = trans('Votre message a bien été envoyé.'); - $message = $newsletterMessage ? $contactMessage. '
' . $newsletterMessage : $contactMessage; + $message = $newsletterMessage ? $contactMessage. '
' . $newsletterMessage : $contactMessage; return redirect()->back() - ->with($hasContactError || $hasNewsletterError ? 'info' : 'success', new \Illuminate\Support\HtmlString($message)); + ->with($hasContactError || $hasNewsletterError ? 'info' : 'success', new \Illuminate\Support\HtmlString($message)); } } 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/Controllers/Front/NewsController.php b/app/Http/Controllers/Front/NewsController.php index 1be9a295..990f33ea 100644 --- a/app/Http/Controllers/Front/NewsController.php +++ b/app/Http/Controllers/Front/NewsController.php @@ -7,12 +7,12 @@ use Uwil\LaravelCommon\Controllers\Controller; class NewsController extends Controller { - public function index() - { - return view('front.pages.news.index'); - } - - public function show(News $news) + /** + * Show news show page. + * + * @return \Illuminate\Contracts\View\View + */ + public function show(News $news): \Illuminate\Contracts\View\View { return view('front.pages.news.show', ['model' => $news->load('categories')]); } 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..bcced317 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', ]; /** @@ -102,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); @@ -122,6 +128,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 +230,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. * @@ -278,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 diff --git a/app/Models/News.php b/app/Models/News.php index f7b9994c..f42cb2aa 100644 --- a/app/Models/News.php +++ b/app/Models/News.php @@ -48,12 +48,22 @@ class News extends Model implements TranslatableModel, LocalizedUrlRoutable HasUwilImages::inertiaFill as traitInertiaFill; } + /** + * Get the route key for the model. + * + * @return string + */ public function getRouteKeyName(): string { return request()->routeIs('back.*') ? 'id' : 'slug'; } - public function getLocalizedRouteKey($locale) + /** + * Get the value of the model's localized route key. + * + * @return string + */ + public function getLocalizedRouteKey($locale): string { return $this->getTranslation( $this->getRouteKeyName(), 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/bootstrap/app.php b/bootstrap/app.php index f4aae1b0..0e49db19 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -33,10 +33,6 @@ return Application::configure(basePath: dirname(__DIR__)) '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, ]); }) ->withExceptions(function (Exceptions $exceptions) { 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/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'); + }); + } +}; 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/routes.php b/lang/en/routes.php index 80be7e01..aecbb573 100644 --- a/lang/en/routes.php +++ b/lang/en/routes.php @@ -1,7 +1,7 @@ 'contact-us/{slug}', + 'contact' => 'contact-us/{slug}', 'contact_index' => 'contact-us', - 'news' => 'news/{news}' + 'news' => 'news/{news}' ]; 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/routes.php b/lang/fr/routes.php index 7592b138..9b9a657f 100644 --- a/lang/fr/routes.php +++ b/lang/fr/routes.php @@ -1,7 +1,7 @@ 'nous-contacter/{slug}', + 'contact' => 'nous-contacter/{slug}', 'contact_index' => 'nous-contacter', - 'news' => 'actualites/{news}' + 'news' => 'actualites/{news}' ]; 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/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/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}

diff --git a/resources/ts/back/Components/Inputs/ModelSelect.tsx b/resources/ts/back/Components/Inputs/ModelSelect.tsx index 7952d4c9..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,12 +257,22 @@ 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); }, [search, targetModel, field]); + // Update when selected is updated. + useEffect(() => { + if (selected !== null) { + handleSelect(selected); + } else { + setSelectedId(null); + setSelectedValue(null); + } + }, [selected]); + return (