feat(admin): category SEO fields, Templates admin, safe project PATCH
Build backend images / build content-svc (push) Failing after 21s
Build backend images / build file-svc (push) Failing after 3m49s
Build backend images / build gateway (push) Failing after 1m2s
Build backend images / build identity-svc (push) Failing after 1m1s
Build backend images / build notification-svc (push) Failing after 1m2s
Build backend images / build render-svc (push) Failing after 1m0s
Build backend images / build studio-svc (push) Failing after 58s

- categories/tags admin forms: add meta title/description/keywords, bot-follow,
  sort, is_active (backend already supported these)
- new Templates admin (/admin/templates): container CRUD with description,
  keywords, publishing, premium, primary mode, category/tag assignment, plus
  editable per-variant aspect & resolution
- content-svc: PATCH /v1/projects/{id} partial update so aspect/resolution edits
  never wipe render/colour data (SharedColorsSvg, RenderAepComp, Folder)
- admin resource proxy: add PATCH passthrough

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-02 14:26:44 +03:30
parent cd95ca2c6f
commit cf5dd4f195
10 changed files with 362 additions and 5 deletions
@@ -199,6 +199,42 @@ public class TemplateService(ContentDbContext db)
return await GetProjectDetailAsync(project.Id);
}
/// <summary>Partial update — only applies supplied (non-null) fields, so editing an
/// aspect/resolution never clears render/colour data the full update would require.</summary>
public async Task<ProjectDetailResponse> PatchProjectAsync(Guid id, PatchProjectRequest req)
{
var project = await db.Projects.FindAsync(id)
?? throw new KeyNotFoundException($"Project {id} not found");
if (req.Name != null) project.Name = req.Name;
if (req.Description != null) project.Description = req.Description;
if (req.Aspect != null) project.Aspect = req.Aspect;
if (req.Resolution != null)
{
if (!Enum.TryParse<ResolutionKind>(req.Resolution, true, out var resolution))
throw new ArgumentException($"Invalid Resolution: {req.Resolution}");
project.Resolution = resolution;
}
if (req.ChooseMode != null)
{
if (!Enum.TryParse<ChooseMode>(req.ChooseMode, true, out var chooseMode))
throw new ArgumentException($"Invalid ChooseMode: {req.ChooseMode}");
project.ChooseMode = chooseMode;
}
if (req.OriginalWidth.HasValue) project.OriginalWidth = req.OriginalWidth.Value;
if (req.OriginalHeight.HasValue) project.OriginalHeight = req.OriginalHeight.Value;
if (req.ProjectDurationSec.HasValue) project.ProjectDurationSec = req.ProjectDurationSec.Value;
if (req.MinDurationSec.HasValue) project.MinDurationSec = req.MinDurationSec;
if (req.MaxDurationSec.HasValue) project.MaxDurationSec = req.MaxDurationSec;
if (req.FreeFps.HasValue) project.FreeFps = req.FreeFps.Value;
if (req.IsPublished.HasValue) project.IsPublished = req.IsPublished.Value;
if (req.Sort.HasValue) project.Sort = req.Sort.Value;
project.UpdatedAt = DateTime.UtcNow;
await db.SaveChangesAsync();
return await GetProjectDetailAsync(project.Id);
}
public async Task DeleteProjectAsync(Guid id)
{
var project = await db.Projects.FindAsync(id)