Maak slug in CMS zichtbaar en inline aanpasbaar
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 <noreply@anthropic.com>
This commit is contained in:
@@ -25,7 +25,7 @@ public class OfferingsService(IOfferingRepository repository, ILogger<OfferingsS
|
||||
var offering = new Offering
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Slug = await GenerateUniqueSlugAsync(request.Title),
|
||||
Slug = await ResolveSlugAsync(request.Slug, request.Title),
|
||||
Title = request.Title,
|
||||
Description = request.Description,
|
||||
Price = request.Price,
|
||||
@@ -70,8 +70,14 @@ public class OfferingsService(IOfferingRepository repository, ILogger<OfferingsS
|
||||
if (offering is null)
|
||||
return null;
|
||||
|
||||
if (!string.Equals(offering.Title, request.Title, StringComparison.Ordinal))
|
||||
if (!string.IsNullOrWhiteSpace(request.Slug))
|
||||
{
|
||||
offering.Slug = await ResolveExplicitSlugAsync(request.Slug, excludeId: offering.Id);
|
||||
}
|
||||
else if (!string.Equals(offering.Title, request.Title, StringComparison.Ordinal))
|
||||
{
|
||||
offering.Slug = await GenerateUniqueSlugAsync(request.Title, excludeId: offering.Id);
|
||||
}
|
||||
|
||||
offering.Title = request.Title;
|
||||
offering.Description = request.Description;
|
||||
@@ -206,6 +212,25 @@ public class OfferingsService(IOfferingRepository repository, ILogger<OfferingsS
|
||||
repository.Update(current);
|
||||
}
|
||||
|
||||
private async Task<string> ResolveSlugAsync(string? explicitSlug, string title)
|
||||
=> !string.IsNullOrWhiteSpace(explicitSlug)
|
||||
? await ResolveExplicitSlugAsync(explicitSlug, excludeId: null)
|
||||
: await GenerateUniqueSlugAsync(title);
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes an admin-supplied slug and enforces uniqueness. Unlike auto-generation,
|
||||
/// a taken slug is rejected outright (OfferingSlugConflictException) rather than
|
||||
/// silently suffixed — the admin picked this value on purpose.
|
||||
/// </summary>
|
||||
private async Task<string> ResolveExplicitSlugAsync(string explicitSlug, Guid? excludeId)
|
||||
{
|
||||
var slug = Slugifier.Slugify(explicitSlug);
|
||||
if (await repository.ExistsBySlugAsync(slug, excludeId))
|
||||
throw new OfferingSlugConflictException(slug);
|
||||
|
||||
return slug;
|
||||
}
|
||||
|
||||
private async Task<string> GenerateUniqueSlugAsync(string title, Guid? excludeId = null)
|
||||
{
|
||||
var baseSlug = Slugifier.Slugify(title);
|
||||
|
||||
Reference in New Issue
Block a user