Newsletter recommendations tweaks, add enter to blur on validation table
This commit is contained in:
@@ -31,13 +31,14 @@ const CATEGORY_FILTERS = {
|
||||
back_in_stock: "AND is_back_in_stock = true",
|
||||
bestsellers: "AND shop_score > 20 AND COALESCE(current_stock, 0) > 0 AND COALESCE(sales_30d, 0) > 0",
|
||||
never_featured: "AND times_featured IS NULL AND line_last_featured_at IS NULL",
|
||||
no_interest: "AND COALESCE(total_sold, 0) = 0 AND COALESCE(current_stock, 0) > 0 AND COALESCE(date_online, product_created_at) <= CURRENT_DATE - INTERVAL '30 days'",
|
||||
};
|
||||
|
||||
function buildScoredCTE({ forCount = false } = {}) {
|
||||
// forCount=true returns minimal columns for COUNT(*)
|
||||
const selectColumns = forCount ? `
|
||||
p.pid,
|
||||
p.created_at,
|
||||
p.created_at as product_created_at,
|
||||
p.date_online,
|
||||
p.shop_score,
|
||||
p.preorder_count,
|
||||
|
||||
6
inventory/package-lock.json
generated
6
inventory/package-lock.json
generated
@@ -4313,9 +4313,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001739",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001739.tgz",
|
||||
"integrity": "sha512-y+j60d6ulelrNSwpPyrHdl+9mJnQzHBr08xm48Qno0nSk4h3Qojh+ziv2qE6rXf4k3tadF4o1J/1tAbVm1NtnA==",
|
||||
"version": "1.0.30001766",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001766.tgz",
|
||||
"integrity": "sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
||||
@@ -382,10 +382,12 @@ export function ProductEditForm({
|
||||
originalImagesRef.current = [...productImages];
|
||||
reset(data);
|
||||
} else {
|
||||
toast.error(result.message ?? "Failed to update product");
|
||||
if (result.error) {
|
||||
console.error("Edit error details:", result.error);
|
||||
}
|
||||
const errorDetail = Array.isArray(result.error)
|
||||
? result.error.filter((e) => e !== "Errors").join("; ")
|
||||
: typeof result.error === "string"
|
||||
? result.error
|
||||
: null;
|
||||
toast.error(errorDetail || result.message || "Failed to update product");
|
||||
}
|
||||
} catch (err) {
|
||||
toast.error(
|
||||
|
||||
@@ -45,7 +45,7 @@ interface Props {
|
||||
data: Product[];
|
||||
file: File;
|
||||
onBack?: () => void;
|
||||
onSubmit: (data: Product[], file: File, options: SubmitOptions) => void | Promise<any>;
|
||||
onSubmit: (data: Product[], file: File, options: SubmitOptions) => void | Promise<boolean | void>;
|
||||
}
|
||||
|
||||
export const ImageUploadStep = ({
|
||||
@@ -228,14 +228,16 @@ export const ImageUploadStep = ({
|
||||
showNewProduct,
|
||||
};
|
||||
|
||||
await onSubmit(updatedData, file, submitOptions);
|
||||
const success = await onSubmit(updatedData, file, submitOptions);
|
||||
|
||||
// Delete the import session on successful submit
|
||||
try {
|
||||
await deleteImportSession();
|
||||
} catch (err) {
|
||||
// Non-critical - log but don't fail the submission
|
||||
console.warn('Failed to delete import session:', err);
|
||||
// Only delete the import session after a successful submit response
|
||||
if (success) {
|
||||
try {
|
||||
await deleteImportSession();
|
||||
} catch (err) {
|
||||
// Non-critical - log but don't fail the submission
|
||||
console.warn('Failed to delete import session:', err);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Submit error:', error);
|
||||
|
||||
@@ -337,7 +337,7 @@ export const UploadFlow = ({ state, onNext, onBack }: Props) => {
|
||||
invalidData: [] as Data<string>[],
|
||||
all: data as Data<string>[]
|
||||
};
|
||||
onSubmit(result, file, options);
|
||||
return onSubmit(result, file, options);
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -113,6 +113,14 @@ const InputCellComponent = ({
|
||||
setIsFocused(true);
|
||||
}, [cellPopoverClosedAt]);
|
||||
|
||||
// Handle Enter key to blur the field (useful for barcode scanners that send Enter after scan)
|
||||
const handleKeyDown = useCallback((e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
inputRef.current?.blur();
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Update store only on blur - this is when validation runs too
|
||||
// IMPORTANT: We store FULL precision for price fields to allow accurate calculations
|
||||
// The display formatting happens separately via displayValue
|
||||
@@ -151,6 +159,7 @@ const InputCellComponent = ({
|
||||
onChange={handleChange}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
onKeyDown={handleKeyDown}
|
||||
disabled={isValidating}
|
||||
className={cn(
|
||||
'h-8 text-sm',
|
||||
|
||||
@@ -331,12 +331,16 @@ const MultilineInputComponent = ({
|
||||
onBlur(editedSuggestion);
|
||||
onDismissAiSuggestion?.(); // Clear the suggestion after accepting
|
||||
setAiSuggestionExpanded(false);
|
||||
intentionalCloseRef.current = true;
|
||||
setPopoverOpen(false);
|
||||
}, [editedSuggestion, onBlur, onDismissAiSuggestion]);
|
||||
|
||||
// Handle dismissing the AI suggestion
|
||||
const handleDismissSuggestion = useCallback(() => {
|
||||
onDismissAiSuggestion?.();
|
||||
setAiSuggestionExpanded(false);
|
||||
intentionalCloseRef.current = true;
|
||||
setPopoverOpen(false);
|
||||
}, [onDismissAiSuggestion]);
|
||||
|
||||
// Calculate display value
|
||||
@@ -446,7 +450,7 @@ const MultilineInputComponent = ({
|
||||
</Button>
|
||||
|
||||
{/* Main textarea */}
|
||||
<div data-col="left" className="flex flex-col min-h-0 w-full lg:w-1/2">
|
||||
<div data-col="left" className={cn("flex flex-col min-h-0 w-full", hasAiSuggestion && "lg:w-1/2")}>
|
||||
<div className={cn(hasAiSuggestion ? 'px-3 py-2 bg-accent' : '', 'flex flex-col flex-1 min-h-0')}>
|
||||
{/* Product name - shown inline on mobile, in measured spacer on desktop */}
|
||||
{hasAiSuggestion && productName && (
|
||||
|
||||
@@ -520,7 +520,7 @@ export function Import() {
|
||||
return stringValue;
|
||||
};
|
||||
|
||||
const handleData = async (data: ImportResult, _file: File, submitOptions: SubmitOptions) => {
|
||||
const handleData = async (data: ImportResult, _file: File, submitOptions: SubmitOptions): Promise<boolean> => {
|
||||
try {
|
||||
const rows = ((data.all?.length ? data.all : data.validData) ?? []) as Data<string>[];
|
||||
const formattedRows: NormalizedProduct[] = rows.map((row) => {
|
||||
@@ -579,7 +579,7 @@ export function Import() {
|
||||
setStartFromScratch(false);
|
||||
|
||||
toast.success(`[DEBUG] Skipped API submission for ${formattedRows.length} product(s)`);
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
const response = await submitNewProducts({
|
||||
@@ -638,11 +638,14 @@ export function Import() {
|
||||
} else {
|
||||
toast.error(resolvedFailureMessage ?? defaultFailureMessage);
|
||||
}
|
||||
|
||||
return isSuccess;
|
||||
} catch (error) {
|
||||
console.error("Import error:", error);
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : "Failed to import data. Please try again.";
|
||||
toast.error(errorMessage);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -924,7 +927,7 @@ export function Import() {
|
||||
<Alert className="border-success bg-success/10">
|
||||
<CheckCircle className="h-4 w-4" style={{ color: 'hsl(var(--success))' }} />
|
||||
<AlertTitle className="text-success-foreground">Success</AlertTitle>
|
||||
<AlertDescription className="text-success-foreground">All products created successfully.</AlertDescription>
|
||||
<AlertDescription className="text-success-foreground">All products created successfully. Please note that images may take a few minutes to finish uploading before they'll be visible in backend.</AlertDescription>
|
||||
</Alert>
|
||||
) : createdProducts.length > 0 && erroredProducts.length > 0 ? (
|
||||
<Alert className="border-warning bg-warning/10">
|
||||
|
||||
@@ -14,6 +14,7 @@ const CATEGORIES = [
|
||||
{ value: "clearance", label: "Clearance" },
|
||||
{ value: "daily_deals", label: "Daily Deals" },
|
||||
{ value: "never_featured", label: "Never Featured" },
|
||||
{ value: "no_interest", label: "No Interest" },
|
||||
]
|
||||
|
||||
export function Newsletter() {
|
||||
|
||||
Reference in New Issue
Block a user