Feature: Human-Readable Text Format Product Import
Implemented a new text-based import format for bulk product imports that is easier to read, write, and version control compared to CSV format. ## New Features ### Import Service (ProductImportService.cs) - Added `ImportFromHumanTextAsync()` - Main text format parser - Added `GenerateTemplateAsHumanText()` - Template generator - Parser supports: - Product blocks starting with `#` - Descriptions between `<text>` tags (optional) - Key-value properties (category, price, weight, unit, stock) - Variants (lines starting with `-`) - Multi-buy offers (lines starting with `+`) - Variant collections (optional, after product name) ### Admin UI - New controller actions: - `ImportText()` - GET: Show import form - `ImportText(textContent, file)` - POST: Process import - `DownloadTextTemplate()` - Download .txt template - New view: `ImportText.cshtml` - Textarea for pasting text - File upload for .txt files - Format documentation sidebar - Links to CSV import and template downloads - Updated `Index.cshtml` with dropdown menu for import options ### Template & Documentation - Created `docs/ProductImportTemplate.txt` with 7 example products - Demonstrates all format features: - Products with/without descriptions - Variants with stock levels - Multi-buy pricing tiers - Multiple weight units ## Text Format Specification ``` # Product Name; OptionalVariantCollection <text> Multi-line description (optional) </text> category: CategoryName price: 10.00 weight: 100 unit: Grams stock: 50 - Variant1; 8.00; 50 - Variant2; 12.00; 30 + Multi-buy1; 2; 19.00 + Multi-buy2; 3; 25.00 ``` ## Benefits - ✅ Git-friendly (easy to diff and version) - ✅ Human-readable and editable - ✅ Supports all product features - ✅ Multi-line descriptions - ✅ Clear structure with # delimiters - ✅ Optional fields (description, variants, multi-buys) - ✅ Comprehensive error reporting 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -244,6 +244,58 @@ public class ProductsController : Controller
|
||||
return File(Encoding.UTF8.GetBytes(templateContent), "text/csv", fileName);
|
||||
}
|
||||
|
||||
// Human-Readable Text Format Import
|
||||
public IActionResult ImportText()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> ImportText(string textContent, IFormFile? file)
|
||||
{
|
||||
string importText = textContent;
|
||||
|
||||
// If file uploaded, use file content instead of textarea
|
||||
if (file != null && file.Length > 0)
|
||||
{
|
||||
if (!file.FileName.EndsWith(".txt", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
ModelState.AddModelError("", "Only .txt files are supported for text format import");
|
||||
return View();
|
||||
}
|
||||
|
||||
using var reader = new StreamReader(file.OpenReadStream());
|
||||
importText = await reader.ReadToEndAsync();
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(importText))
|
||||
{
|
||||
ModelState.AddModelError("", "Please provide text content or upload a .txt file");
|
||||
return View();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var result = await _importService.ImportFromHumanTextAsync(importText);
|
||||
ViewData["ImportResult"] = result;
|
||||
return View("ImportResult", result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModelState.AddModelError("", $"Import failed: {ex.Message}");
|
||||
return View();
|
||||
}
|
||||
}
|
||||
|
||||
public IActionResult DownloadTextTemplate()
|
||||
{
|
||||
var templateContent = _importService.GenerateTemplateAsHumanText();
|
||||
var fileName = "product_import_template.txt";
|
||||
|
||||
return File(Encoding.UTF8.GetBytes(templateContent), "text/plain", fileName);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetVariantCollection(Guid id)
|
||||
{
|
||||
|
||||
148
LittleShop/Areas/Admin/Views/Products/ImportText.cshtml
Normal file
148
LittleShop/Areas/Admin/Views/Products/ImportText.cshtml
Normal file
@@ -0,0 +1,148 @@
|
||||
@{
|
||||
ViewData["Title"] = "Import Products (Text Format)";
|
||||
}
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="@Url.Action("Index")">Products</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Import (Text Format)</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<h1><i class="fas fa-file-alt"></i> Import Products (Human-Readable Text)</h1>
|
||||
<p class="text-muted">Bulk import products with variants and multi-buys using an easy-to-read text format</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">Paste or Upload Text</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="post" enctype="multipart/form-data" asp-action="ImportText">
|
||||
@Html.AntiForgeryToken()
|
||||
|
||||
@if (!ViewData.ModelState.IsValid)
|
||||
{
|
||||
<div class="alert alert-danger">
|
||||
<ul class="mb-0">
|
||||
@foreach (var error in ViewData.ModelState.Values.SelectMany(v => v.Errors))
|
||||
{
|
||||
<li>@error.ErrorMessage</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="textContent" class="form-label">Product Import Text</label>
|
||||
<textarea name="textContent" id="textContent" class="form-control font-monospace" rows="15" placeholder="Paste your product import text here..."></textarea>
|
||||
<div class="form-text">
|
||||
Paste your product data in the human-readable format, or upload a .txt file below.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="file" class="form-label">Or Upload .txt File</label>
|
||||
<input type="file" name="file" id="file" class="form-control" accept=".txt" />
|
||||
<div class="form-text">
|
||||
Upload a text file with product data (optional, overrides textarea content).
|
||||
<a href="@Url.Action("DownloadTextTemplate")" class="text-decoration-none">
|
||||
<i class="fas fa-download"></i> Download template
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-upload"></i> Import Products
|
||||
</button>
|
||||
<a href="@Url.Action("Import")" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-file-csv"></i> Use CSV Format Instead
|
||||
</a>
|
||||
<a href="@Url.Action("Index")" class="btn btn-secondary">
|
||||
<i class="fas fa-times"></i> Cancel
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h6 class="card-title mb-0">Text Format Structure</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p><strong>Product Block (starts with #):</strong></p>
|
||||
<pre class="small"><code># Product Name; OptionalVariantCollection
|
||||
<text>
|
||||
Description here (optional)
|
||||
</text>
|
||||
category: CategoryName
|
||||
price: 10.00
|
||||
weight: 100
|
||||
unit: Grams
|
||||
stock: 50
|
||||
|
||||
- Variant1; 8.00; 50
|
||||
- Variant2; 12.00; 30
|
||||
|
||||
+ Multi-buy1; 2; 19.00
|
||||
+ Multi-buy2; 3; 25.00</code></pre>
|
||||
|
||||
<p class="mt-3"><strong>Key Points:</strong></p>
|
||||
<ul class="small">
|
||||
<li>Each product starts with <code>#</code></li>
|
||||
<li>Description between <code><text></code> tags (optional)</li>
|
||||
<li><code>category:</code> is required</li>
|
||||
<li>Variants start with <code>-</code></li>
|
||||
<li>Multi-buys start with <code>+</code></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<h6 class="card-title mb-0">Field Formats</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="small"><strong>Variant Line:</strong></p>
|
||||
<code class="small">- Name; Price; StockLevel</code>
|
||||
<p class="small mt-1">Example: <code>- Small; 8.00; 50</code></p>
|
||||
|
||||
<p class="small mt-3"><strong>Multi-Buy Line:</strong></p>
|
||||
<code class="small">+ Name; Quantity; Price</code>
|
||||
<p class="small mt-1">Example: <code>+ Twin Pack; 2; 19.00</code></p>
|
||||
|
||||
<p class="small mt-3"><strong>Weight Units:</strong></p>
|
||||
<ul class="small mb-0">
|
||||
<li><code>Grams</code> (default)</li>
|
||||
<li><code>Kilograms</code></li>
|
||||
<li><code>Pounds</code></li>
|
||||
<li><code>Ounces</code></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<h6 class="card-title mb-0">Quick Actions</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-grid gap-2">
|
||||
<a href="@Url.Action("DownloadTextTemplate")" class="btn btn-outline-info btn-sm">
|
||||
<i class="fas fa-download"></i> Download Template
|
||||
</a>
|
||||
<a href="@Url.Action("Import")" class="btn btn-outline-primary btn-sm">
|
||||
<i class="fas fa-file-csv"></i> Switch to CSV Import
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -9,9 +9,20 @@
|
||||
<h1><i class="fas fa-box"></i> Products</h1>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<a href="@Url.Action("Create")" class="btn btn-primary">
|
||||
<i class="fas fa-plus"></i> <span class="d-none d-sm-inline">Add Product</span>
|
||||
</a>
|
||||
<div class="btn-group" role="group">
|
||||
<a href="@Url.Action("Create")" class="btn btn-primary">
|
||||
<i class="fas fa-plus"></i> <span class="d-none d-sm-inline">Add Product</span>
|
||||
</a>
|
||||
<button type="button" class="btn btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<i class="fas fa-file-import"></i> <span class="d-none d-sm-inline">Import</span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" href="@Url.Action("ImportText")"><i class="fas fa-file-alt"></i> Text Format</a></li>
|
||||
<li><a class="dropdown-item" href="@Url.Action("Import")"><i class="fas fa-file-csv"></i> CSV Format</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item" href="@Url.Action("Export")"><i class="fas fa-file-export"></i> Export Products</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user