From 1b974696bf87f281c56d2af2d3eaa370ecbe2a3f Mon Sep 17 00:00:00 2001 From: Sluijsens Date: Tue, 4 Aug 2026 15:31:10 +0200 Subject: [PATCH] Maak slug in CMS zichtbaar en inline aanpasbaar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit De CMS-admin toonde het nieuwe Slug-veld nog niet — dat bestond alleen in de API-respons. Voegt een read-only slugweergave onder het titelveld toe met een potlood-icoon om 'm inline te bewerken. Backend: Create/UpdateOfferingRequest accepteren nu een optionele Slug-override. Zonder expliciete waarde blijft het bestaande gedrag (auto-genereren uit titel, regenereren bij titelwijziging). Met een expliciete waarde wordt die genormaliseerd en op uniekheid gecontroleerd; een botsing geeft 409 Conflict (RFC 9457 ProblemDetails, naar het patroon van MasterControlledAvailabilityException). Frontend: de slug wordt alleen meegestuurd als de admin 'm daadwerkelijk bewerkt heeft (dirtyFields.slug), zodat een ongemoeide slug bij een titelwijziging gewoon blijft auto-regenereren. Co-Authored-By: Claude Sonnet 5 --- .../offerings/components/OfferingForm.tsx | 86 ++++++++++++++++++- .../src/features/offerings/mocks/handlers.ts | 35 +++++++- .../offerings/pages/OfferingFormPage.test.tsx | 43 ++++++++++ .../offerings/pages/OfferingFormPage.tsx | 12 ++- .../features/offerings/schemas/offering.ts | 6 ++ .../src/features/offerings/services/types.ts | 3 + frontend/src/i18n/locales/en/translation.json | 5 ++ frontend/src/i18n/locales/nl/translation.json | 5 ++ .../Controllers/OfferingsControllerTests.cs | 23 +++++ .../Services/OfferingsServiceTests.cs | 58 +++++++++++++ .../Controllers/OfferingsController.cs | 23 ++++- .../Models/CreateOfferingRequest.cs | 3 +- .../Models/UpdateOfferingRequest.cs | 3 +- .../Services/OfferingSlugConflictException.cs | 12 +++ .../Services/OfferingsService.cs | 29 ++++++- 15 files changed, 332 insertions(+), 14 deletions(-) create mode 100644 src/SlpModularCms.Modules.Offerings/Services/OfferingSlugConflictException.cs diff --git a/frontend/src/features/offerings/components/OfferingForm.tsx b/frontend/src/features/offerings/components/OfferingForm.tsx index 37df333..d5ebd8b 100644 --- a/frontend/src/features/offerings/components/OfferingForm.tsx +++ b/frontend/src/features/offerings/components/OfferingForm.tsx @@ -1,7 +1,8 @@ +import { useState } from 'react'; import { useForm, useWatch } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { useTranslation } from 'react-i18next'; -import { Plus, Trash2 } from 'lucide-react'; +import { Check, Pencil, Plus, Trash2, X } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; @@ -23,7 +24,8 @@ export function OfferingForm({ initialValues, onSubmit, isSubmitting }: Offering control, handleSubmit, setValue, - formState: { errors }, + resetField, + formState: { errors, dirtyFields }, } = useForm({ resolver: zodResolver(offeringFormSchema), mode: 'onTouched', @@ -35,12 +37,35 @@ export function OfferingForm({ initialValues, onSubmit, isSubmitting }: Offering features: [''], ctaLabel: '', featured: false, + slug: '', }, }); // useFieldArray requires object-shaped array items — features is a plain string[], // so add/remove are handled directly via setValue instead. const features = useWatch({ control, name: 'features' }); + const slug = useWatch({ control, name: 'slug' }); + const [isEditingSlug, setIsEditingSlug] = useState(false); + + function confirmSlugEdit() { + setIsEditingSlug(false); + } + + function cancelSlugEdit() { + resetField('slug'); + setIsEditingSlug(false); + } + + // Slug is auto-generated from the title server-side unless explicitly overridden here — + // only send it along when the admin actually touched this field, so an untouched slug + // keeps auto-regenerating when the title changes (see OfferingsService.UpdateAsync). + function submitWithSlugPolicy(values: OfferingFormData) { + const payload = { ...values }; + if (!dirtyFields.slug) { + delete payload.slug; + } + onSubmit(payload); + } function addFeature() { setValue('features', [...features, ''], { shouldValidate: true }); @@ -51,13 +76,68 @@ export function OfferingForm({ initialValues, onSubmit, isSubmitting }: Offering } return ( -
+
{errors.title && }
+
+ + {isEditingSlug ? ( +
+ + + +
+ ) : ( +
+ + {slug && slug.length > 0 ? slug : t('offerings.form.slugAutoHint')} + + +
+ )} + {errors.slug && } +
+