Add catalog item images with thumbnail preview in wizard
Each catalog item now supports one optional image (jpg/jpeg/png/gif/webp,
max 10 MB). Uploading generates a 200x200 JPEG thumbnail automatically via
SixLabors.ImageSharp. Images are stored in Azure Blob Storage under a new
catalogimages container, keyed by {companyId}/catalog/{itemId}/.
- CatalogItem entity: ImagePath + ThumbnailPath (nullable string fields)
- Migration: AddCatalogItemImages applied
- ICatalogImageService / CatalogImageService: upload, thumbnail generation,
delete; old blobs replaced atomically on re-upload
- CatalogItemsController: Create/Edit accept optional IFormFile image;
Image(id, thumbnail) action serves blobs with [Authorize] so wizard users
can load thumbnails without CanManageProducts policy
- Catalog index (_CategoryNode): 40x40 thumbnail (or placeholder icon)
left of each item name
- Details view: image card in right column with click-to-full-size link
- Create/Edit views: file picker with live preview; Edit shows current
thumbnail with Remove checkbox
- Wizard (item-wizard.js): thumbnails in product list with hover preview
that follows the cursor (showCatalogPreview / moveCatalogPreview);
fixed Bootstrap d-flex !important bug that broke the filter box by
moving flex layout to an inner wrapper div
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -341,9 +341,19 @@ function renderStep2Html() {
|
||||
}
|
||||
|
||||
function renderProductFields() {
|
||||
const catalogItems = catalogData.map(c =>
|
||||
`<div class="catalog-list-item px-3 py-2" data-value="${c.value}" onclick="pickCatalogItem(this)">${escHtml(c.text)}</div>`
|
||||
).join('');
|
||||
ensureCatalogPreviewEl();
|
||||
const catalogItems = catalogData.map(c => {
|
||||
const thumbHtml = c.thumbnailPath
|
||||
? `<img src="/CatalogItems/Image?id=${c.value}&thumbnail=true" alt=""
|
||||
style="width:36px;height:36px;object-fit:cover;border-radius:4px;flex-shrink:0;cursor:zoom-in;"
|
||||
onmouseenter="showCatalogPreview(event,'/CatalogItems/Image?id=${c.value}&thumbnail=true')"
|
||||
onmousemove="moveCatalogPreview(event)"
|
||||
onmouseleave="hideCatalogPreview()" />`
|
||||
: `<span style="width:36px;height:36px;background:#f0f0f0;border-radius:4px;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;"><i class='bi bi-image text-muted' style='font-size:.85rem;'></i></span>`;
|
||||
// Inner div carries the flex layout — the outer catalog-list-item div must stay a plain block element
|
||||
// so filterCatalog() can set el.style.display='none' without Bootstrap d-flex !important overriding it.
|
||||
return `<div class="catalog-list-item px-2 py-2" data-value="${c.value}" onclick="pickCatalogItem(this)"><div style="display:flex;align-items:center;gap:0.5rem;">${thumbHtml}<span>${escHtml(c.text)}</span></div></div>`;
|
||||
}).join('');
|
||||
|
||||
return `
|
||||
<div class="mb-3">
|
||||
@@ -385,6 +395,49 @@ function pickCatalogItem(el) {
|
||||
document.getElementById('err_catalogItemId')?.classList.add('d-none');
|
||||
}
|
||||
|
||||
// ── Catalog thumbnail hover preview ──────────────────────────────────────────
|
||||
|
||||
function ensureCatalogPreviewEl() {
|
||||
if (document.getElementById('catalogThumbPreview')) return;
|
||||
const el = document.createElement('div');
|
||||
el.id = 'catalogThumbPreview';
|
||||
el.style.cssText = 'position:fixed;display:none;z-index:9999;pointer-events:none;' +
|
||||
'border:1px solid #dee2e6;border-radius:8px;box-shadow:0 4px 16px rgba(0,0,0,0.18);' +
|
||||
'background:#fff;padding:4px;';
|
||||
el.innerHTML = '<img id="catalogThumbPreviewImg" style="display:block;width:200px;height:200px;object-fit:contain;border-radius:4px;" />';
|
||||
document.body.appendChild(el);
|
||||
}
|
||||
|
||||
function showCatalogPreview(event, url) {
|
||||
const preview = document.getElementById('catalogThumbPreview');
|
||||
const img = document.getElementById('catalogThumbPreviewImg');
|
||||
if (!preview || !img) return;
|
||||
img.src = url;
|
||||
_placeCatalogPreview(event, preview);
|
||||
preview.style.display = 'block';
|
||||
}
|
||||
|
||||
function moveCatalogPreview(event) {
|
||||
const preview = document.getElementById('catalogThumbPreview');
|
||||
if (preview && preview.style.display !== 'none') _placeCatalogPreview(event, preview);
|
||||
}
|
||||
|
||||
function hideCatalogPreview() {
|
||||
const preview = document.getElementById('catalogThumbPreview');
|
||||
if (preview) preview.style.display = 'none';
|
||||
}
|
||||
|
||||
function _placeCatalogPreview(event, preview) {
|
||||
const pad = 16, pw = 216, ph = 216;
|
||||
let x = event.clientX + pad;
|
||||
let y = event.clientY - ph / 2;
|
||||
if (x + pw > window.innerWidth) x = event.clientX - pw - pad;
|
||||
if (y < 8) y = 8;
|
||||
if (y + ph > window.innerHeight) y = window.innerHeight - ph - 8;
|
||||
preview.style.left = x + 'px';
|
||||
preview.style.top = y + 'px';
|
||||
}
|
||||
|
||||
function renderCalculatedFields() {
|
||||
const areaUnit = pageMeta.areaUnit || 'sq ft';
|
||||
return `
|
||||
|
||||
Reference in New Issue
Block a user