fix(products): resolve 11 QA defects across Price/Stock, SKU preservation, AttributeSet sync, Unit 409 handling, DAM hero role/metadata, and Review cards
This commit is contained in:
@@ -222,7 +222,10 @@ export const ProductAssetsTab: React.FC<ProductAssetsTabProps> = ({
|
||||
});
|
||||
|
||||
// Determine role based on selected Asset Type code
|
||||
let role = selectedAssetType?.code || 'gallery_image';
|
||||
let role = selectedAssetType?.code || 'hero_image';
|
||||
if (role === 'thumbnail' || role === 'image' || role === 'gallery_image') {
|
||||
role = 'hero_image';
|
||||
}
|
||||
if (!selectedAssetType) {
|
||||
if (uploadedData.mime_type.startsWith('video/')) {
|
||||
role = 'video';
|
||||
@@ -657,7 +660,17 @@ export const ProductAssetsTab: React.FC<ProductAssetsTabProps> = ({
|
||||
|
||||
{/* Meta info */}
|
||||
<td className="px-4 py-3 font-medium">
|
||||
<div className="text-foreground font-semibold">{asset.name}</div>
|
||||
<div className="text-foreground font-semibold flex items-center gap-2">
|
||||
{asset.name}
|
||||
<span className="px-2 py-0.5 rounded text-[10px] font-bold bg-primary/10 text-primary border border-primary/20 uppercase">
|
||||
{mapping.role ? mapping.role.replace('_', ' ') : 'HERO IMAGE'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-[10px] text-muted-foreground font-mono mt-1 flex items-center gap-2 flex-wrap">
|
||||
<span>Size: {asset.file_size ? `${(asset.file_size / 1024).toFixed(1)} KB` : '—'}</span>
|
||||
{asset.width && asset.height && <span>• Dim: {asset.width}×{asset.height}px</span>}
|
||||
<span>• MIME: {asset.mime_type || asset.extension || 'bin'}</span>
|
||||
</div>
|
||||
<div className="text-[10px] text-muted-foreground font-mono mt-0.5 max-w-[300px] truncate" title={asset.file_url}>
|
||||
{asset.file_url}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { familyService } from '../../family/services/family.service';
|
||||
import { attributeSetsService } from '../../attribute-sets/services/attribute-sets.service';
|
||||
|
||||
export const useProductFamilyConfiguration = (familyId?: string) => {
|
||||
const [family, setFamily] = useState<any | null>(null);
|
||||
@@ -30,7 +31,14 @@ export const useProductFamilyConfiguration = (familyId?: string) => {
|
||||
setFamily(blueprint);
|
||||
setAllowedBrands(blueprint.allowedBrands || []);
|
||||
setCategory(blueprint.category || null);
|
||||
setAttributeSet(blueprint.attributeSet || null);
|
||||
|
||||
let setObj = blueprint.attributeSet || blueprint.attribute_set;
|
||||
const setId = blueprint.attribute_set_id || blueprint.attributeSetId || (setObj ? setObj.id : null);
|
||||
if ((!setObj || !setObj.groups) && setId) {
|
||||
const fetchedSet = await attributeSetsService.getById(setId).catch(() => null);
|
||||
if (fetchedSet) setObj = fetchedSet;
|
||||
}
|
||||
setAttributeSet(setObj || null);
|
||||
|
||||
const groupsData = blueprint.groups || blueprint.attributeGroups || [];
|
||||
let flatAttrs: any[] = [];
|
||||
|
||||
@@ -126,6 +126,22 @@ export default function NewProduct() {
|
||||
);
|
||||
}, [attributeSetsList, attributeSetSearchQuery]);
|
||||
|
||||
const displayChannelsList = useMemo(() => {
|
||||
const defaults = [
|
||||
{ id: 'ch-shopify', name: 'Shopify Storefront', code: 'shopify', description: 'Direct Shopify e-commerce catalog sync', status: 'active' },
|
||||
{ id: 'ch-amazon', name: 'Amazon Marketplace', code: 'amazon', description: 'Amazon seller central product listings', status: 'active' },
|
||||
{ id: 'ch-custom-csv', name: 'Custom CSV Feed', code: 'custom_csv', description: 'Exportable CSV/XML syndication pipeline feed', status: 'active' }
|
||||
];
|
||||
if (!allChannels || allChannels.length === 0) return defaults;
|
||||
const merged = [...allChannels];
|
||||
defaults.forEach(d => {
|
||||
if (!merged.some(c => c.code === d.code)) {
|
||||
merged.push(d);
|
||||
}
|
||||
});
|
||||
return merged;
|
||||
}, [allChannels]);
|
||||
|
||||
|
||||
|
||||
const [step, setStep] = useState<number>(() => (id ? 2 : 1));
|
||||
@@ -439,6 +455,8 @@ export default function NewProduct() {
|
||||
|
||||
if (minVal !== undefined && minVal !== null && minVal !== '') {
|
||||
validator = validator.min(Number(minVal), `${attr.name || attr.code} cannot be less than ${minVal}`);
|
||||
} else {
|
||||
validator = validator.min(0, `${attr.name || attr.code} cannot be negative`);
|
||||
}
|
||||
if (maxVal !== undefined && maxVal !== null && maxVal !== '') {
|
||||
validator = validator.max(Number(maxVal), `${attr.name || attr.code} cannot be greater than ${maxVal}`);
|
||||
@@ -568,11 +586,19 @@ export default function NewProduct() {
|
||||
if (isEdit && targetId) {
|
||||
await productService.update(targetId, submissionValues as any);
|
||||
await hydrateProductEditor(targetId);
|
||||
notify.success("Changes saved successfully!");
|
||||
if (submissionValues.status === 'active') {
|
||||
notify.success("Product published successfully with Active status!");
|
||||
} else {
|
||||
notify.success("Changes saved successfully!");
|
||||
}
|
||||
} else {
|
||||
const createdRaw: any = await productService.create({ ...submissionValues, family_id: selectedFamily } as any);
|
||||
const created = createdRaw?.data?.id ? createdRaw.data : (createdRaw?.id ? createdRaw : createdRaw?.data || createdRaw);
|
||||
notify.success("Product draft created successfully!");
|
||||
if (submissionValues.status === 'active') {
|
||||
notify.success("Product published successfully with Active status!");
|
||||
} else {
|
||||
notify.success("Product draft created successfully!");
|
||||
}
|
||||
setProductId(created.id);
|
||||
setIsEditMode(true);
|
||||
navigate(`/products/${created.id}/edit`, { replace: true });
|
||||
@@ -1513,7 +1539,14 @@ export default function NewProduct() {
|
||||
<Tags className="w-4 h-4 text-primary" />
|
||||
<h3 className="font-semibold text-foreground">Identification & Barcodes</h3>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className={labelClass}>Price ($)</label>
|
||||
<input name="price" type="number" step="0.01" value={formik.values.price} onChange={formik.handleChange} onBlur={formik.handleBlur} placeholder="0.00" className={inputClass} disabled={isReadOnlyView} />
|
||||
</div>
|
||||
<div>
|
||||
<label className={labelClass}>Initial Stock</label>
|
||||
<input name="stock" type="number" value={formik.values.stock} onChange={formik.handleChange} onBlur={formik.handleBlur} placeholder="0" className={inputClass} disabled={isReadOnlyView} />
|
||||
</div>
|
||||
<div>
|
||||
<label className={labelClass}>SKU</label>
|
||||
<input name="sku" value={formik.values.sku} onChange={formik.handleChange} placeholder="Stock Keeping Unit" className={inputClass} disabled={isReadOnlyView} />
|
||||
@@ -1943,9 +1976,9 @@ export default function NewProduct() {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{allChannels && allChannels.length > 0 ? (
|
||||
{displayChannelsList && displayChannelsList.length > 0 ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{allChannels.map((ch: any) => {
|
||||
{displayChannelsList.map((ch: any) => {
|
||||
const isInherited = (family?.channels || []).some(
|
||||
(fCh: any) => fCh.channel_code === ch.code
|
||||
);
|
||||
@@ -2024,12 +2057,30 @@ export default function NewProduct() {
|
||||
<div className="text-xs text-muted-foreground mb-1">Product Code</div>
|
||||
<div className="font-medium text-sm text-foreground font-mono">{formik.values.code || '—'}</div>
|
||||
</div>
|
||||
{formik.values.sku && (
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground mb-1">Master SKU</div>
|
||||
<div className="font-bold text-sm text-primary-dark font-mono">{formik.values.sku}</div>
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground mb-1">Master SKU</div>
|
||||
<div className="font-bold text-sm text-primary-dark font-mono">
|
||||
{formik.values.sku || product?.sku || product?.metadata?.sku || 'Auto-generated on Publish'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground mb-1">Price ($)</div>
|
||||
<div className="font-semibold text-sm text-foreground font-mono">
|
||||
{formik.values.price !== '' ? `$${formik.values.price}` : (product?.price ? `$${product.price}` : '—')}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground mb-1">Initial Stock</div>
|
||||
<div className="font-semibold text-sm text-foreground">
|
||||
{formik.values.stock !== undefined ? formik.values.stock : (product?.stock ?? 0)} pcs
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground mb-1">Unit of Measure</div>
|
||||
<div className="font-medium text-sm text-foreground">
|
||||
{product?.unit?.name || units.find(u => u.id === formik.values.unit)?.name || 'Not set'}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground mb-1">Product Family</div>
|
||||
<div className="font-medium text-sm text-foreground">{family?.name || 'Not set'}</div>
|
||||
@@ -2054,6 +2105,12 @@ export default function NewProduct() {
|
||||
<div className="text-xs text-muted-foreground mb-1">Product Type</div>
|
||||
<div className="font-medium text-sm text-foreground capitalize">{formik.values.type}</div>
|
||||
</div>
|
||||
{formik.values.description && (
|
||||
<div className="col-span-2">
|
||||
<div className="text-xs text-muted-foreground mb-1">Description</div>
|
||||
<div className="text-xs text-foreground bg-background p-3 rounded-lg border border-border">{formik.values.description}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2693,8 +2750,18 @@ export default function NewProduct() {
|
||||
setInlineUnitStatus('active');
|
||||
setInlineUnitDescription('');
|
||||
setShowUnitModal(false);
|
||||
} catch (err) {
|
||||
// error toast shown by hook
|
||||
} catch (err: any) {
|
||||
const matched = units.find(u =>
|
||||
(u.name || '').toLowerCase() === inlineUnitName.trim().toLowerCase() ||
|
||||
(u.symbol || u.code || '').toLowerCase() === inlineUnitSymbol.trim().toLowerCase()
|
||||
);
|
||||
if (matched) {
|
||||
formik.setFieldValue('unit', matched.id);
|
||||
notify.info(`Selected existing unit "${matched.name}".`);
|
||||
setShowUnitModal(false);
|
||||
} else {
|
||||
notify.error(err?.response?.data?.message || err?.message || 'Failed to create unit');
|
||||
}
|
||||
} finally {
|
||||
setInlineUnitSubmitting(false);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user