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:
2026-05-27 21:54:51 -04:00
parent 32d09b38f1
commit ca7e905832
24 changed files with 12959 additions and 10 deletions
@@ -35,6 +35,7 @@ public class CompanySettingsController : Controller
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly IAzureBlobStorageService _blobStorage;
private readonly ICustomFormulaAiService _formulaAiService;
private readonly IFormulaLibraryService _formulaLibraryService;
public CompanySettingsController(
IUnitOfWork unitOfWork,
@@ -49,7 +50,8 @@ public class CompanySettingsController : Controller
UserManager<ApplicationUser> userManager,
SignInManager<ApplicationUser> signInManager,
IAzureBlobStorageService blobStorage,
ICustomFormulaAiService formulaAiService)
ICustomFormulaAiService formulaAiService,
IFormulaLibraryService formulaLibraryService)
{
_unitOfWork = unitOfWork;
_mapper = mapper;
@@ -64,6 +66,7 @@ public class CompanySettingsController : Controller
_signInManager = signInManager;
_blobStorage = blobStorage;
_formulaAiService = formulaAiService;
_formulaLibraryService = formulaLibraryService;
}
/// <summary>
@@ -3080,6 +3083,11 @@ public class CompanySettingsController : Controller
_mapper.Map(dto, entity);
entity.UpdatedAt = DateTime.UtcNow;
// If this was imported from the library, mark it as modified so the share button appears
if (entity.SourceFormulaLibraryItemId.HasValue)
entity.IsModifiedFromSource = true;
await _unitOfWork.CompleteAsync();
return Json(new { success = true });
@@ -3100,6 +3108,52 @@ public class CompanySettingsController : Controller
return Json(new { success = true });
}
// ── Community Library: share / unshare / status ───────────────────────
/// <summary>
/// Returns the community library status for a given template: whether it is published,
/// eligible to share, and where it was originally imported from if applicable.
/// </summary>
[HttpGet]
public async Task<IActionResult> FormulaLibraryStatus(int templateId)
{
if (!AllowCustomFormulas()) return Json(new { canShare = false });
var companyId = _tenantContext.GetCurrentCompanyId()!.Value;
var status = await _formulaLibraryService.GetTemplateLibraryStatusAsync(templateId, companyId);
return Json(status);
}
/// <summary>
/// Publishes a company template to the community library (or re-publishes after unshare).
/// Only templates that are original creations or modified imports may be shared.
/// </summary>
[HttpPost]
public async Task<IActionResult> ShareFormula([FromBody] PowderCoating.Application.DTOs.Company.ShareFormulaRequest request)
{
if (!AllowCustomFormulas()) return Json(new { success = false, message = "Custom Formulas are not available on your current plan." });
var companyId = _tenantContext.GetCurrentCompanyId()!.Value;
var userId = User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value ?? "";
try
{
var libraryItemId = await _formulaLibraryService.ShareAsync(companyId, userId, request);
return Json(new { success = true, libraryItemId });
}
catch (InvalidOperationException ex)
{
return Json(new { success = false, message = ex.Message });
}
}
/// <summary>Removes a template from the community library. Existing company imports are unaffected.</summary>
[HttpPost]
public async Task<IActionResult> UnshareFormula(int libraryItemId)
{
var companyId = _tenantContext.GetCurrentCompanyId()!.Value;
await _formulaLibraryService.UnshareAsync(libraryItemId, companyId);
return Json(new { success = true });
}
/// <summary>
/// Uploads a diagram image for a template to blob storage container
/// <c>formulatemplate-diagrams/{companyId}/{templateId}/diagram.{ext}</c>.