Add Community Formula Library feature
Companies can now share their custom formula templates to a platform-wide community library. Other tenants can browse, preview, and import formulas as independent local copies. Includes attribution (source company name), "Inspired by" lineage for re-contributed formulas, import counts, own-formula badge, cascade diagram nullification, and AI assistant + help docs updates. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
using AutoMapper;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using PowderCoating.Application.Interfaces;
|
||||
using PowderCoating.Core.Interfaces;
|
||||
using PowderCoating.Shared.Constants;
|
||||
|
||||
namespace PowderCoating.Web.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Community formula library — browse published formulas from all companies and import
|
||||
/// them into the current company's local template list.
|
||||
/// </summary>
|
||||
[Authorize(Policy = AppConstants.Policies.CanViewData)]
|
||||
public class FormulaLibraryController : Controller
|
||||
{
|
||||
private readonly IFormulaLibraryService _libraryService;
|
||||
private readonly ITenantContext _tenantContext;
|
||||
private readonly IMapper _mapper;
|
||||
private readonly IAzureBlobStorageService _blobStorage;
|
||||
|
||||
public FormulaLibraryController(
|
||||
IFormulaLibraryService libraryService,
|
||||
ITenantContext tenantContext,
|
||||
IMapper mapper,
|
||||
IAzureBlobStorageService blobStorage)
|
||||
{
|
||||
_libraryService = libraryService;
|
||||
_tenantContext = tenantContext;
|
||||
_mapper = mapper;
|
||||
_blobStorage = blobStorage;
|
||||
}
|
||||
|
||||
/// <summary>Browse the community library with optional search and filter params.</summary>
|
||||
// GET: /FormulaLibrary
|
||||
public async Task<IActionResult> Index(
|
||||
string? search = null,
|
||||
string? outputMode = null,
|
||||
string? industryHint = null)
|
||||
{
|
||||
var companyId = _tenantContext.GetCurrentCompanyId();
|
||||
if (companyId == null) return RedirectToAction("Index", "Home");
|
||||
|
||||
var items = await _libraryService.BrowseAsync(companyId.Value, search, outputMode, industryHint);
|
||||
|
||||
ViewBag.Search = search;
|
||||
ViewBag.OutputMode = outputMode;
|
||||
ViewBag.IndustryHint = industryHint;
|
||||
ViewBag.TotalCount = items.Count();
|
||||
|
||||
return View(items);
|
||||
}
|
||||
|
||||
/// <summary>Returns full detail JSON for the import preview modal.</summary>
|
||||
// GET: /FormulaLibrary/Detail/5
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> Detail(int id)
|
||||
{
|
||||
var companyId = _tenantContext.GetCurrentCompanyId();
|
||||
if (companyId == null) return Json(new { error = "No company context." });
|
||||
|
||||
var detail = await _libraryService.GetDetailAsync(id, companyId.Value);
|
||||
if (detail == null) return NotFound();
|
||||
|
||||
return Json(detail);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serves a formula diagram image by blob storage path. Used for library cards where the
|
||||
/// diagram belongs to another company's template blob container.
|
||||
/// </summary>
|
||||
// GET: /FormulaLibrary/Diagram?path=...
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> Diagram(string path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path)) return NotFound();
|
||||
|
||||
// Sanitize: path must not escape the blob container
|
||||
if (path.Contains("..") || path.StartsWith("/") || path.StartsWith("\\"))
|
||||
return BadRequest();
|
||||
|
||||
var (ok, bytes, contentType, _) = await _blobStorage.DownloadAsync("formulatemplate-diagrams", path);
|
||||
if (!ok || bytes == null || bytes.Length == 0) return NotFound();
|
||||
return File(bytes, contentType ?? "image/jpeg");
|
||||
}
|
||||
|
||||
/// <summary>Imports a library entry as a new local template for the current company.</summary>
|
||||
// POST: /FormulaLibrary/Import
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
[Authorize(Policy = AppConstants.Policies.CompanyAdminOnly)]
|
||||
public async Task<IActionResult> Import(int libraryItemId)
|
||||
{
|
||||
var companyId = _tenantContext.GetCurrentCompanyId();
|
||||
if (companyId == null)
|
||||
return Json(new { success = false, message = "No company context." });
|
||||
|
||||
try
|
||||
{
|
||||
var userId = User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value ?? "";
|
||||
var templateId = await _libraryService.ImportAsync(libraryItemId, companyId.Value, userId);
|
||||
return Json(new { success = true, templateId });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Json(new { success = false, message = ex.Message });
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user