feat(admin): standalone Projects page + per-project asset manager
Build backend images / build content-svc (push) Failing after 1m36s
Build backend images / build file-svc (push) Failing after 1m28s
Build backend images / build gateway (push) Failing after 2m11s
Build backend images / build identity-svc (push) Failing after 2m11s
Build backend images / build notification-svc (push) Failing after 3m46s
Build backend images / build render-svc (push) Failing after 55s
Build backend images / build studio-svc (push) Failing after 1m2s

- content-svc: GET /v1/projects (browse/search all projects across containers,
  paginated, admin) returning template name/slug + AE status; project_assets
  table (mig 23) + entity; GET/POST/DELETE /v1/projects/{id}/assets
- /admin/projects: searchable, paginated list of every renderable project with
  thumbnail, template, aspect/resolution, AE-file + publish status
- ProjectAssets component: list/upload/delete named footage/image/audio/font
  files per project (reused in the projects page; AE file upload alongside)
- nav + fa/en "Projects" label

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-03 00:39:33 +03:30
parent c4839bd35f
commit 7fe5f8a563
13 changed files with 364 additions and 2 deletions
@@ -314,6 +314,49 @@ public class TemplateService(ContentDbContext db)
p.AepFileUrl, p.AepFileSizeBytes, p.RenderAepComp
);
/// <summary>Browse/search all projects (template items) across containers.</summary>
public async Task<PagedResponse<ProjectListItemResponse>> ListProjectsAsync(string? q, Guid? containerId, int page, int pageSize)
{
if (page < 1) page = 1;
if (pageSize < 1 || pageSize > 200) pageSize = 30;
var query = db.Projects.Where(p => p.DeletedAt == null);
if (containerId.HasValue) query = query.Where(p => p.ContainerId == containerId.Value);
if (!string.IsNullOrWhiteSpace(q)) query = query.Where(p => EF.Functions.ILike(p.Name, $"%{q}%"));
var total = await query.LongCountAsync();
var items = await query
.OrderByDescending(p => p.UpdatedAt)
.Skip((page - 1) * pageSize).Take(pageSize)
.Select(p => new ProjectListItemResponse(
p.Id, p.ContainerId, p.Container.Name, p.Container.Slug, p.Name, p.Image,
p.Aspect, p.Resolution.ToString(), p.AepFileUrl, p.RenderAepComp, p.IsPublished, p.Sort))
.ToListAsync();
return new PagedResponse<ProjectListItemResponse>(items,
new PaginationMeta(page, pageSize, total, (int)Math.Ceiling((double)total / pageSize)));
}
public async Task<List<ProjectAssetResponse>> ListAssetsAsync(Guid projectId) =>
await db.ProjectAssets.Where(a => a.ProjectId == projectId).OrderBy(a => a.Sort)
.Select(a => new ProjectAssetResponse(a.Id, a.ProjectId, a.Name, a.Kind, a.Url, a.SizeBytes, a.Sort))
.ToListAsync();
public async Task<ProjectAssetResponse> AddAssetAsync(Guid projectId, CreateAssetRequest req)
{
var a = new ProjectAsset
{
ProjectId = projectId, Name = req.Name, Kind = string.IsNullOrWhiteSpace(req.Kind) ? "footage" : req.Kind,
Url = req.Url, MinioKey = req.MinioKey, SizeBytes = req.SizeBytes, Sort = req.Sort,
};
db.ProjectAssets.Add(a);
await db.SaveChangesAsync();
return new ProjectAssetResponse(a.Id, a.ProjectId, a.Name, a.Kind, a.Url, a.SizeBytes, a.Sort);
}
public async Task DeleteAssetAsync(Guid assetId)
{
var a = await db.ProjectAssets.FindAsync(assetId);
if (a != null) { db.ProjectAssets.Remove(a); await db.SaveChangesAsync(); }
}
/// <summary>Attach an uploaded After Effects file (and render composition) to a project.</summary>
public async Task<ProjectDetailResponse> SetProjectAepAsync(Guid id, SetAepRequest req)
{