feat(admin): manually edit scene inputs (content elements)

Scene content elements (the editable Text/Media/Color/… inputs inside a scene)
had no CRUD — only AEP-import created them, so admins couldn't define or edit
them. Added full management:

content-svc:
- SceneElementsController: GET/POST/PUT/DELETE /v1/scene-elements?scene_id=
- SceneColorService: Get/Create/Update/DeleteContentElementAsync
- ContentElementResponse + SaveContentElementRequest (key, title, type,
  default_value, hint, position, text-box/font/media flags)
gateway: route /v1/scene-elements/*path → content
frontend: SceneColorEditor scenes tab → per-scene "ورودی‌ها" expander with full
  add/edit/delete of inputs (15 element types: Text/Media/Color/Number/…)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-06 06:54:22 +03:30
parent 9d499a89de
commit ddc0a2d0d9
5 changed files with 336 additions and 36 deletions
@@ -174,6 +174,63 @@ public class SceneColorService(ContentDbContext db)
p.Items.OrderBy(i => i.Sort).Select(i => new ColorPresetItemResponse(i.Id, i.ElementKey, i.Value, i.Sort)).ToList()))
.FirstAsync();
// ── Scene content elements (editable inputs) ────────────────────────────
public async Task<List<ContentElementResponse>> GetContentElementsAsync(Guid sceneId) =>
await db.SceneContentElements.Where(e => e.SceneId == sceneId)
.OrderBy(e => e.PositionInContainer)
.Select(e => ToElementResponse(e)).ToListAsync();
public async Task<ContentElementResponse> CreateContentElementAsync(SaveContentElementRequest req)
{
var e = new SceneContentElement { SceneId = req.SceneId };
ApplyElement(e, req);
db.SceneContentElements.Add(e);
await db.SaveChangesAsync();
return ToElementResponse(e);
}
public async Task<ContentElementResponse> UpdateContentElementAsync(Guid id, SaveContentElementRequest req)
{
var e = await db.SceneContentElements.FindAsync(id)
?? throw new KeyNotFoundException($"Element {id} not found");
ApplyElement(e, req);
await db.SaveChangesAsync();
return ToElementResponse(e);
}
public async Task DeleteContentElementAsync(Guid id)
{
var e = await db.SceneContentElements.FindAsync(id)
?? throw new KeyNotFoundException($"Element {id} not found");
db.SceneContentElements.Remove(e);
await db.SaveChangesAsync();
}
private static void ApplyElement(SceneContentElement e, SaveContentElementRequest req)
{
e.Key = req.Key;
e.Title = req.Title;
e.Hint = req.Hint;
e.Type = Enum.TryParse<ContentElementType>(req.Type, true, out var t) ? t : ContentElementType.Text;
e.DefaultValue = req.DefaultValue;
e.PositionInContainer = req.PositionInContainer;
e.IsTextBox = req.IsTextBox;
e.MaxSize = req.MaxSize;
e.FontSize = req.FontSize;
e.IsFontChangeable = req.IsFontChangeable;
e.IsFontSizeChangeable = req.IsFontSizeChangeable;
e.VideoSupport = req.VideoSupport;
e.Width = req.Width;
e.Height = req.Height;
e.Thumbnail = req.Thumbnail;
}
private static ContentElementResponse ToElementResponse(SceneContentElement e) => new(
e.Id, e.SceneId, e.Key, e.Title, e.Hint, e.Type.ToString(), e.DefaultValue,
e.PositionInContainer, e.IsTextBox, e.MaxSize, e.FontSize, e.IsFontChangeable,
e.IsFontSizeChangeable, e.VideoSupport, e.Width, e.Height, e.Thumbnail);
// ── helpers ─────────────────────────────────────────────────────────────
private static SceneResponse ToSceneResponse(Scene s) => new(
@@ -32,6 +32,33 @@ public class ScenesController(SceneColorService svc) : ControllerBase
}
}
[ApiController]
[Route("v1/scene-elements")]
public class SceneElementsController(SceneColorService svc) : ControllerBase
{
[HttpGet]
public async Task<IActionResult> List([FromQuery(Name = "scene_id")] Guid sceneId) =>
Ok(await svc.GetContentElementsAsync(sceneId));
[Authorize(Roles = "Admin")]
[HttpPost]
public async Task<IActionResult> Create([FromBody] SaveContentElementRequest req) =>
Ok(await svc.CreateContentElementAsync(req));
[Authorize(Roles = "Admin")]
[HttpPut("{id:guid}")]
public async Task<IActionResult> Update(Guid id, [FromBody] SaveContentElementRequest req) =>
Ok(await svc.UpdateContentElementAsync(id, req));
[Authorize(Roles = "Admin")]
[HttpDelete("{id:guid}")]
public async Task<IActionResult> Delete(Guid id)
{
await svc.DeleteContentElementAsync(id);
return NoContent();
}
}
[ApiController]
[Route("v1/shared-colors")]
public class SharedColorsController(SceneColorService svc) : ControllerBase
@@ -48,3 +48,21 @@ public record ColorPresetItemInput(string ElementKey, string Value, int Sort);
public record SaveColorPresetRequest(
Guid ProjectId, string? Name, int Sort, List<ColorPresetItemInput> Items
);
// ── Scene content elements (the editable inputs inside a scene) ───────────────
public record ContentElementResponse(
Guid Id, Guid SceneId, string Key, string Title, string? Hint,
string Type, string? DefaultValue, int PositionInContainer,
bool IsTextBox, int? MaxSize, int? FontSize, bool IsFontChangeable,
bool IsFontSizeChangeable, bool VideoSupport, int? Width, int? Height,
string? Thumbnail
);
public record SaveContentElementRequest(
Guid SceneId, string Key, string Title, string? Hint,
string Type, string? DefaultValue, int PositionInContainer,
bool IsTextBox, int? MaxSize, int? FontSize,
bool IsFontChangeable, bool IsFontSizeChangeable,
bool VideoSupport, int? Width, int? Height, string? Thumbnail
);