feat(content): scenes + shared-colors + colour-presets endpoints
Build backend images / build content-svc (push) Failing after 2m53s
Build backend images / build file-svc (push) Failing after 3m55s
Build backend images / build gateway (push) Failing after 53s
Build backend images / build identity-svc (push) Failing after 3m26s
Build backend images / build notification-svc (push) Failing after 3m5s
Build backend images / build render-svc (push) Failing after 46s
Build backend images / build studio-svc (push) Failing after 2m22s
Build backend images / build content-svc (push) Failing after 2m53s
Build backend images / build file-svc (push) Failing after 3m55s
Build backend images / build gateway (push) Failing after 53s
Build backend images / build identity-svc (push) Failing after 3m26s
Build backend images / build notification-svc (push) Failing after 3m5s
Build backend images / build render-svc (push) Failing after 46s
Build backend images / build studio-svc (push) Failing after 2m22s
Completes the content backend for the studio building blocks (all project-scoped):
- GET /v1/scenes?project_id= + POST/PUT/DELETE (scene metadata CRUD)
- GET /v1/shared-colors?project_id= + POST/PUT/DELETE
- GET /v1/color-presets?project_id= + POST/PUT/DELETE (palette + items)
SceneColorService + DTOs; reads open, writes [Authorize(Roles=Admin)].
Gateway routes /v1/{scenes,shared-colors,color-presets}/* → content.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
using FlatRender.ContentSvc.Domain.Entities;
|
||||
using FlatRender.ContentSvc.Domain.Enums;
|
||||
using FlatRender.ContentSvc.Infrastructure.Data;
|
||||
using FlatRender.ContentSvc.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace FlatRender.ContentSvc.Application.Services;
|
||||
|
||||
/// <summary>CRUD for project-scoped scenes, shared colors and colour presets.</summary>
|
||||
public class SceneColorService(ContentDbContext db)
|
||||
{
|
||||
// ── Scenes ──────────────────────────────────────────────────────────────
|
||||
|
||||
public async Task<List<SceneResponse>> GetScenesAsync(Guid projectId) =>
|
||||
await db.Scenes.Where(s => s.ProjectId == projectId && s.DeletedAt == null)
|
||||
.OrderBy(s => s.Sort)
|
||||
.Select(s => ToSceneResponse(s)).ToListAsync();
|
||||
|
||||
public async Task<SceneResponse> CreateSceneAsync(CreateSceneRequest req)
|
||||
{
|
||||
var scene = new Scene
|
||||
{
|
||||
ProjectId = req.ProjectId,
|
||||
Key = req.Key,
|
||||
Title = req.Title,
|
||||
LocalizedTitle = req.LocalizedTitle,
|
||||
SceneType = ParseSceneKind(req.SceneType),
|
||||
Image = req.Image,
|
||||
Demo = req.Demo,
|
||||
SceneColorSvg = req.SceneColorSvg,
|
||||
SnapshotUrl = req.SnapshotUrl,
|
||||
GenerateKf = req.GenerateKf,
|
||||
DefaultDurationSec = req.DefaultDurationSec,
|
||||
MinDurationSec = req.MinDurationSec,
|
||||
MaxDurationSec = req.MaxDurationSec,
|
||||
OverlapAtEndSec = req.OverlapAtEndSec,
|
||||
CanHandleDuration = req.CanHandleDuration,
|
||||
ManualColorSelection = req.ManualColorSelection,
|
||||
Sort = req.Sort,
|
||||
IsActive = req.IsActive,
|
||||
};
|
||||
db.Scenes.Add(scene);
|
||||
await db.SaveChangesAsync();
|
||||
return ToSceneResponse(scene);
|
||||
}
|
||||
|
||||
public async Task<SceneResponse> UpdateSceneAsync(Guid id, UpdateSceneRequest req)
|
||||
{
|
||||
var scene = await db.Scenes.FindAsync(id) ?? throw new KeyNotFoundException($"Scene {id} not found");
|
||||
scene.Key = req.Key;
|
||||
scene.Title = req.Title;
|
||||
scene.LocalizedTitle = req.LocalizedTitle;
|
||||
scene.SceneType = ParseSceneKind(req.SceneType);
|
||||
scene.Image = req.Image;
|
||||
scene.Demo = req.Demo;
|
||||
scene.SceneColorSvg = req.SceneColorSvg;
|
||||
scene.SnapshotUrl = req.SnapshotUrl;
|
||||
scene.GenerateKf = req.GenerateKf;
|
||||
scene.DefaultDurationSec = req.DefaultDurationSec;
|
||||
scene.MinDurationSec = req.MinDurationSec;
|
||||
scene.MaxDurationSec = req.MaxDurationSec;
|
||||
scene.OverlapAtEndSec = req.OverlapAtEndSec;
|
||||
scene.CanHandleDuration = req.CanHandleDuration;
|
||||
scene.ManualColorSelection = req.ManualColorSelection;
|
||||
scene.Sort = req.Sort;
|
||||
scene.IsActive = req.IsActive;
|
||||
scene.UpdatedAt = DateTime.UtcNow;
|
||||
await db.SaveChangesAsync();
|
||||
return ToSceneResponse(scene);
|
||||
}
|
||||
|
||||
public async Task DeleteSceneAsync(Guid id)
|
||||
{
|
||||
var scene = await db.Scenes.FindAsync(id) ?? throw new KeyNotFoundException($"Scene {id} not found");
|
||||
scene.DeletedAt = DateTime.UtcNow;
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
// ── Shared colours ──────────────────────────────────────────────────────
|
||||
|
||||
public async Task<List<SharedColorResponse>> GetSharedColorsAsync(Guid projectId) =>
|
||||
await db.SharedColors.Where(c => c.ProjectId == projectId).OrderBy(c => c.Sort)
|
||||
.Select(c => new SharedColorResponse(c.Id, c.ProjectId, c.ElementKey, c.Title, c.Icon,
|
||||
c.AttrValue.ToString(), c.DefaultColor, c.Sort)).ToListAsync();
|
||||
|
||||
public async Task<SharedColorResponse> CreateSharedColorAsync(SaveSharedColorRequest req)
|
||||
{
|
||||
var c = new SharedColor
|
||||
{
|
||||
ProjectId = req.ProjectId,
|
||||
ElementKey = req.ElementKey,
|
||||
Title = req.Title,
|
||||
Icon = req.Icon,
|
||||
AttrValue = ParseAttrValue(req.AttrValue),
|
||||
DefaultColor = req.DefaultColor,
|
||||
Sort = req.Sort,
|
||||
};
|
||||
db.SharedColors.Add(c);
|
||||
await db.SaveChangesAsync();
|
||||
return new SharedColorResponse(c.Id, c.ProjectId, c.ElementKey, c.Title, c.Icon, c.AttrValue.ToString(), c.DefaultColor, c.Sort);
|
||||
}
|
||||
|
||||
public async Task<SharedColorResponse> UpdateSharedColorAsync(Guid id, SaveSharedColorRequest req)
|
||||
{
|
||||
var c = await db.SharedColors.FindAsync(id) ?? throw new KeyNotFoundException($"Color {id} not found");
|
||||
c.ElementKey = req.ElementKey;
|
||||
c.Title = req.Title;
|
||||
c.Icon = req.Icon;
|
||||
c.AttrValue = ParseAttrValue(req.AttrValue);
|
||||
c.DefaultColor = req.DefaultColor;
|
||||
c.Sort = req.Sort;
|
||||
c.UpdatedAt = DateTime.UtcNow;
|
||||
await db.SaveChangesAsync();
|
||||
return new SharedColorResponse(c.Id, c.ProjectId, c.ElementKey, c.Title, c.Icon, c.AttrValue.ToString(), c.DefaultColor, c.Sort);
|
||||
}
|
||||
|
||||
public async Task DeleteSharedColorAsync(Guid id)
|
||||
{
|
||||
var c = await db.SharedColors.FindAsync(id) ?? throw new KeyNotFoundException($"Color {id} not found");
|
||||
db.SharedColors.Remove(c);
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
// ── Colour presets (palettes) ───────────────────────────────────────────
|
||||
|
||||
public async Task<List<ColorPresetResponse>> GetColorPresetsAsync(Guid projectId) =>
|
||||
await db.SharedColorPresets.Where(p => p.ProjectId == projectId).OrderBy(p => p.Sort)
|
||||
.Select(p => new ColorPresetResponse(p.Id, p.ProjectId, p.Name, p.Sort,
|
||||
p.Items.OrderBy(i => i.Sort).Select(i => new ColorPresetItemResponse(i.Id, i.ElementKey, i.Value, i.Sort)).ToList()))
|
||||
.ToListAsync();
|
||||
|
||||
public async Task<ColorPresetResponse> CreateColorPresetAsync(SaveColorPresetRequest req)
|
||||
{
|
||||
var preset = new SharedColorPreset
|
||||
{
|
||||
ProjectId = req.ProjectId,
|
||||
Name = req.Name,
|
||||
Sort = req.Sort,
|
||||
Items = (req.Items ?? []).Select(i => new SharedColorPresetItem
|
||||
{
|
||||
ElementKey = i.ElementKey, Value = i.Value, Sort = i.Sort,
|
||||
}).ToList(),
|
||||
};
|
||||
db.SharedColorPresets.Add(preset);
|
||||
await db.SaveChangesAsync();
|
||||
return await GetPresetAsync(preset.Id);
|
||||
}
|
||||
|
||||
public async Task<ColorPresetResponse> UpdateColorPresetAsync(Guid id, SaveColorPresetRequest req)
|
||||
{
|
||||
var preset = await db.SharedColorPresets.Include(p => p.Items).FirstOrDefaultAsync(p => p.Id == id)
|
||||
?? throw new KeyNotFoundException($"Preset {id} not found");
|
||||
preset.Name = req.Name;
|
||||
preset.Sort = req.Sort;
|
||||
db.SharedColorPresetItems.RemoveRange(preset.Items);
|
||||
preset.Items = (req.Items ?? []).Select(i => new SharedColorPresetItem
|
||||
{
|
||||
PresetId = preset.Id, ElementKey = i.ElementKey, Value = i.Value, Sort = i.Sort,
|
||||
}).ToList();
|
||||
await db.SaveChangesAsync();
|
||||
return await GetPresetAsync(preset.Id);
|
||||
}
|
||||
|
||||
public async Task DeleteColorPresetAsync(Guid id)
|
||||
{
|
||||
var preset = await db.SharedColorPresets.FindAsync(id) ?? throw new KeyNotFoundException($"Preset {id} not found");
|
||||
db.SharedColorPresets.Remove(preset);
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
private async Task<ColorPresetResponse> GetPresetAsync(Guid id) =>
|
||||
await db.SharedColorPresets.Where(p => p.Id == id)
|
||||
.Select(p => new ColorPresetResponse(p.Id, p.ProjectId, p.Name, p.Sort,
|
||||
p.Items.OrderBy(i => i.Sort).Select(i => new ColorPresetItemResponse(i.Id, i.ElementKey, i.Value, i.Sort)).ToList()))
|
||||
.FirstAsync();
|
||||
|
||||
// ── helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
private static SceneResponse ToSceneResponse(Scene s) => new(
|
||||
s.Id, s.ProjectId, s.Key, s.Title, s.LocalizedTitle, s.SceneType.ToString(),
|
||||
s.Image, s.Demo, s.SceneColorSvg, s.SnapshotUrl, s.GenerateKf,
|
||||
s.DefaultDurationSec, s.MinDurationSec, s.MaxDurationSec, s.OverlapAtEndSec,
|
||||
s.CanHandleDuration, s.ManualColorSelection, s.Sort, s.IsActive);
|
||||
|
||||
private static SceneKind ParseSceneKind(string? v) =>
|
||||
Enum.TryParse<SceneKind>(v, true, out var k) ? k : SceneKind.Normal;
|
||||
|
||||
private static AttrValueKind ParseAttrValue(string? v) =>
|
||||
Enum.TryParse<AttrValueKind>(v, true, out var a) ? a : AttrValueKind.fill;
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
using FlatRender.ContentSvc.Application.Services;
|
||||
using FlatRender.ContentSvc.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace FlatRender.ContentSvc.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("v1/scenes")]
|
||||
public class ScenesController(SceneColorService svc) : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> List([FromQuery(Name = "project_id")] Guid projectId) =>
|
||||
Ok(await svc.GetScenesAsync(projectId));
|
||||
|
||||
[Authorize(Roles = "Admin")]
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Create([FromBody] CreateSceneRequest req) =>
|
||||
Ok(await svc.CreateSceneAsync(req));
|
||||
|
||||
[Authorize(Roles = "Admin")]
|
||||
[HttpPut("{id:guid}")]
|
||||
public async Task<IActionResult> Update(Guid id, [FromBody] UpdateSceneRequest req) =>
|
||||
Ok(await svc.UpdateSceneAsync(id, req));
|
||||
|
||||
[Authorize(Roles = "Admin")]
|
||||
[HttpDelete("{id:guid}")]
|
||||
public async Task<IActionResult> Delete(Guid id)
|
||||
{
|
||||
await svc.DeleteSceneAsync(id);
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
|
||||
[ApiController]
|
||||
[Route("v1/shared-colors")]
|
||||
public class SharedColorsController(SceneColorService svc) : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> List([FromQuery(Name = "project_id")] Guid projectId) =>
|
||||
Ok(await svc.GetSharedColorsAsync(projectId));
|
||||
|
||||
[Authorize(Roles = "Admin")]
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Create([FromBody] SaveSharedColorRequest req) =>
|
||||
Ok(await svc.CreateSharedColorAsync(req));
|
||||
|
||||
[Authorize(Roles = "Admin")]
|
||||
[HttpPut("{id:guid}")]
|
||||
public async Task<IActionResult> Update(Guid id, [FromBody] SaveSharedColorRequest req) =>
|
||||
Ok(await svc.UpdateSharedColorAsync(id, req));
|
||||
|
||||
[Authorize(Roles = "Admin")]
|
||||
[HttpDelete("{id:guid}")]
|
||||
public async Task<IActionResult> Delete(Guid id)
|
||||
{
|
||||
await svc.DeleteSharedColorAsync(id);
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
|
||||
[ApiController]
|
||||
[Route("v1/color-presets")]
|
||||
public class ColorPresetsController(SceneColorService svc) : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> List([FromQuery(Name = "project_id")] Guid projectId) =>
|
||||
Ok(await svc.GetColorPresetsAsync(projectId));
|
||||
|
||||
[Authorize(Roles = "Admin")]
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Create([FromBody] SaveColorPresetRequest req) =>
|
||||
Ok(await svc.CreateColorPresetAsync(req));
|
||||
|
||||
[Authorize(Roles = "Admin")]
|
||||
[HttpPut("{id:guid}")]
|
||||
public async Task<IActionResult> Update(Guid id, [FromBody] SaveColorPresetRequest req) =>
|
||||
Ok(await svc.UpdateColorPresetAsync(id, req));
|
||||
|
||||
[Authorize(Roles = "Admin")]
|
||||
[HttpDelete("{id:guid}")]
|
||||
public async Task<IActionResult> Delete(Guid id)
|
||||
{
|
||||
await svc.DeleteColorPresetAsync(id);
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
namespace FlatRender.ContentSvc.Models;
|
||||
|
||||
// ── Scenes (project-scoped scene templates) ──────────────────────────────────
|
||||
|
||||
public record SceneResponse(
|
||||
Guid Id, Guid ProjectId, string Key, string Title, string? LocalizedTitle,
|
||||
string SceneType, string? Image, string? Demo, string? SceneColorSvg, string? SnapshotUrl,
|
||||
bool GenerateKf, decimal? DefaultDurationSec, decimal? MinDurationSec, decimal? MaxDurationSec,
|
||||
decimal OverlapAtEndSec, bool CanHandleDuration, bool ManualColorSelection, int Sort, bool IsActive
|
||||
);
|
||||
|
||||
public record CreateSceneRequest(
|
||||
Guid ProjectId, string Key, string Title, string? LocalizedTitle, string? SceneType,
|
||||
string? Image, string? Demo, string? SceneColorSvg, string? SnapshotUrl, bool GenerateKf,
|
||||
decimal? DefaultDurationSec, decimal? MinDurationSec, decimal? MaxDurationSec, decimal OverlapAtEndSec,
|
||||
bool CanHandleDuration, bool ManualColorSelection, int Sort, bool IsActive
|
||||
);
|
||||
|
||||
public record UpdateSceneRequest(
|
||||
string Key, string Title, string? LocalizedTitle, string? SceneType,
|
||||
string? Image, string? Demo, string? SceneColorSvg, string? SnapshotUrl, bool GenerateKf,
|
||||
decimal? DefaultDurationSec, decimal? MinDurationSec, decimal? MaxDurationSec, decimal OverlapAtEndSec,
|
||||
bool CanHandleDuration, bool ManualColorSelection, int Sort, bool IsActive
|
||||
);
|
||||
|
||||
// ── Shared colors (project-scoped named colors) ──────────────────────────────
|
||||
|
||||
public record SharedColorResponse(
|
||||
Guid Id, Guid ProjectId, string ElementKey, string Title, string? Icon,
|
||||
string AttrValue, string DefaultColor, int Sort
|
||||
);
|
||||
|
||||
public record SaveSharedColorRequest(
|
||||
Guid ProjectId, string ElementKey, string Title, string? Icon,
|
||||
string? AttrValue, string DefaultColor, int Sort
|
||||
);
|
||||
|
||||
// ── Color presets (named palettes of colors) ─────────────────────────────────
|
||||
|
||||
public record ColorPresetItemResponse(Guid Id, string ElementKey, string Value, int Sort);
|
||||
|
||||
public record ColorPresetResponse(
|
||||
Guid Id, Guid ProjectId, string? Name, int Sort, List<ColorPresetItemResponse> Items
|
||||
);
|
||||
|
||||
public record ColorPresetItemInput(string ElementKey, string Value, int Sort);
|
||||
|
||||
public record SaveColorPresetRequest(
|
||||
Guid ProjectId, string? Name, int Sort, List<ColorPresetItemInput> Items
|
||||
);
|
||||
@@ -64,6 +64,7 @@ builder.Services.AddScoped<TaxonomyService>();
|
||||
builder.Services.AddScoped<TemplateService>();
|
||||
builder.Services.AddScoped<CmsService>();
|
||||
builder.Services.AddScoped<AiContentService>();
|
||||
builder.Services.AddScoped<SceneColorService>();
|
||||
|
||||
// HTTP client for the OpenAI-compatible AI provider (base URL is per-tenant config).
|
||||
builder.Services.AddHttpClient("openai");
|
||||
|
||||
@@ -116,6 +116,9 @@ func main() {
|
||||
v1.Any("/comments/*path", apiRL, auth, content.Handler())
|
||||
v1.Any("/favorites/*path", apiRL, auth, content.Handler())
|
||||
v1.Any("/ai/*path", apiRL, auth, content.Handler())
|
||||
v1.Any("/scenes/*path", apiRL, optionalAuth, content.Handler())
|
||||
v1.Any("/shared-colors/*path", apiRL, optionalAuth, content.Handler())
|
||||
v1.Any("/color-presets/*path", apiRL, optionalAuth, content.Handler())
|
||||
|
||||
// ── File Service ─────────────────────────────────────────────────────────
|
||||
v1.Any("/files/*path", apiRL, auth, file.Handler())
|
||||
|
||||
Reference in New Issue
Block a user