Skip to content

Commit c88bca3

Browse files
committed
refactor: error handling on poor product saves
1 parent 6cbb3de commit c88bca3

2 files changed

Lines changed: 29 additions & 4 deletions

File tree

  • apps
    • backend/src/app/api/latest/internal/config/override/[level]
    • dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]

apps/backend/src/app/api/latest/internal/config/override/[level]/route.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -170,9 +170,18 @@ const writeResponseSchema = yupObject({
170170

171171
function findIncludeByDefaultPath(value: unknown, path: string[] = []): string | null {
172172
if (value === "include-by-default") {
173-
// Only flag the deprecated sentinel when it sits at `payments.products.*.prices`;
174-
// anywhere else it's just a string literal that happens to match.
175-
if (path.length === 4 && path[0] === "payments" && path[1] === "products" && path[3] === "prices") {
173+
// Only flag the deprecated sentinel when it sits at `payments.products.<id>.prices`;
174+
// anywhere else it's just a string literal that happens to match. The product-ID
175+
// segment can itself contain dots (override keys are dot-paths and we split on
176+
// ".", which fragments dotted IDs into multiple path entries), so we anchor on
177+
// the leading `payments.products` prefix and the trailing `prices` suffix
178+
// instead of an exact path length.
179+
if (
180+
path.length >= 4
181+
&& path[0] === "payments"
182+
&& path[1] === "products"
183+
&& path[path.length - 1] === "prices"
184+
) {
176185
return path.join(".");
177186
}
178187
return null;

apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/page-client.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Link, StyledLink } from "@/components/link";
66
import { useRouter } from "@/components/router";
77
import {
88
ActionCell,
9+
Alert,
910
AvatarCell,
1011
Badge,
1112
Button,
@@ -277,6 +278,10 @@ function ProductDetailsSection({ productId, product, config }: ProductDetailsSec
277278
// ===== LOCAL STATE FOR DEFERRED SAVE =====
278279
// Track all pending changes. undefined means "use original value"
279280
const [pendingChanges, setPendingChanges] = useState<PendingProductChanges>({});
281+
// Inline validation error shown above the editable grid. We avoid `window.alert()`
282+
// (jarring/blocking) and `toast()` (per AGENTS.md, blocking errors are easily
283+
// missed as toasts) in favor of a destructive Alert in the design system.
284+
const [saveValidationError, setSaveValidationError] = useState<string | null>(null);
280285

281286
// Computed local values (pending change or original)
282287
const localDisplayName = pendingChanges.displayName !== undefined ? pendingChanges.displayName : (product.displayName || '');
@@ -308,6 +313,7 @@ function ProductDetailsSection({ productId, product, config }: ProductDetailsSec
308313
// Discard all pending changes
309314
const handleDiscard = () => {
310315
setPendingChanges({});
316+
setSaveValidationError(null);
311317
// Reset add-on dialog state
312318
setIsAddOn(product.isAddOnTo !== false && typeof product.isAddOnTo === 'object');
313319
setSelectedAddOnProducts(
@@ -321,9 +327,10 @@ function ProductDetailsSection({ productId, product, config }: ProductDetailsSec
321327
const handleSave = async () => {
322328
const effectivePrices = pendingChanges.prices ?? product.prices;
323329
if (Object.keys(effectivePrices).length === 0) {
324-
alert("A product must have at least one price. Add a price option or make the product free before saving.");
330+
setSaveValidationError("A product must have at least one price. Add a price option or make the product free before saving.");
325331
return;
326332
}
333+
setSaveValidationError(null);
327334

328335
const configUpdate: Record<string, any> = {};
329336

@@ -538,6 +545,10 @@ function ProductDetailsSection({ productId, product, config }: ProductDetailsSec
538545

539546
// ===== PRICES HANDLERS (for deferred save) =====
540547
const handlePricesChange = (newPrices: Product['prices']) => {
548+
// Clear the "needs at least one price" error as soon as the user adds one back.
549+
if (Object.keys(newPrices).length > 0) {
550+
setSaveValidationError(null);
551+
}
541552
// Deep compare to see if we're back to original
542553
const originalPrices = product.prices;
543554
if (JSON.stringify(newPrices) === JSON.stringify(originalPrices)) {
@@ -731,6 +742,11 @@ function ProductDetailsSection({ productId, product, config }: ProductDetailsSec
731742

732743
return (
733744
<>
745+
{saveValidationError && (
746+
<Alert variant="destructive" className="mb-4">
747+
{saveValidationError}
748+
</Alert>
749+
)}
734750
<DesignEditableGrid
735751
items={gridItems}
736752
columns={2}

0 commit comments

Comments
 (0)