Merge pull request #267 from useautumn/johnyeocx/fix-product-delete-logic

Fix product delete logic and improve onboarding UX
This commit is contained in:
John Yeo
2025-10-16 12:18:28 +01:00
committed by GitHub
12 changed files with 115 additions and 47 deletions

View File

@@ -77,7 +77,7 @@ export default function PricingTablePreview({
} else if (productCount === 2) {
return "flex flex-col gap-6 max-w-2xl mx-auto px-4 sm:grid sm:grid-cols-2 sm:flex-none"; // Vertical on mobile, 2 columns on sm+
} else {
return "flex flex-col gap-6 max-w-7xl mx-auto px-4 sm:grid sm:grid-cols-2 lg:grid-cols-3 sm:flex-none"; // Vertical on mobile, 2 columns on sm+, 3 on lg+
return "flex flex-col gap-6 max-w-7xl mx-auto px-4 sm:grid md:grid-cols-2 2xl:grid-cols-3 sm:flex-none"; // Vertical on mobile, 2 columns on sm+, 3 on lg+
}
};

View File

@@ -16,6 +16,7 @@ export const CustomToaster = () => {
className={isOnboarding ? "" : "flex justify-center"}
duration={6000}
toastOptions={{
duration: 6000,
className:
"w-96 px-3 pt-3 pb-2.5 bg-white rounded-xl shadow-[0_4px_4px_0_rgba(0,0,0,0.02),inset_0_-4px_6px_0_rgba(0,0,0,0.04),0_4px_24px_0_rgba(0,0,0,0.03)] outline outline-16 inline-flex flex-row justify-start items-start gap-3 overflow-hidden",
style: {

View File

@@ -7,7 +7,7 @@ function Card({ className, ...props }: React.ComponentProps<"div">) {
<div
data-slot="card"
className={cn(
"bg-card text-card-foreground flex flex-col gap-4 rounded-xl border py-4 shadow-sm",
"bg-card text-card-foreground flex flex-col gap-3 rounded-xl border py-4 shadow-sm",
className,
)}
{...props}

View File

@@ -163,7 +163,7 @@ html {
--color-t10: var(--t10);
--color-t11: var(--t11);
--color-t12: var(--t12);
--color-input: var(--input);
--color-icon1: var(--icon1);
@@ -278,6 +278,10 @@ html {
--color-purple-medium: #b07aff;
--color-purple-stripe: #665cff;
--color-purple-gradient: #6f47ff;
/* --breakpoint-xl: 85rem; */
/* --breakpoint-2xl: 100rem;
--breakpoint-3xl: 120rem; */
}
/* Hide number input spinners globally */

View File

@@ -1,14 +1,14 @@
import { productV2ToBasePrice } from "@autumn/shared";
import { CrosshairSimpleIcon } from "@phosphor-icons/react";
import { PricingTableContainer } from "@/components/autumn/PricingTableContainer";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { PlanTypeBadges } from "@/components/v2/badges/PlanTypeBadges";
import { IconButton } from "@/components/v2/buttons/IconButton";
import { Card, CardContent, CardHeader } from "@/components/v2/cards/Card";
import { Separator } from "@/components/v2/separator";
import { useProductsQuery } from "@/hooks/queries/useProductsQuery";
import { useFeatureStore } from "@/hooks/stores/useFeatureStore";
import { useProductStore } from "@/hooks/stores/useProductStore";
import { useSheetStore } from "@/hooks/stores/useSheetStore";
import { useIsEditingPlan, useSheetStore } from "@/hooks/stores/useSheetStore";
import { keyToTitle } from "@/utils/formatUtils/formatTextUtils";
import { PlanCardToolbar } from "../products/plan/components/plan-card/PlanCardToolbar";
import { PlanFeatureList } from "../products/plan/components/plan-card/PlanFeatureList";
@@ -32,6 +32,7 @@ export const OnboardingPreview = ({
const playgroundMode = useOnboardingStore((state) => state.playgroundMode);
const feature = useFeatureStore((state) => state.feature);
const setSheet = useSheetStore((state) => state.setSheet);
const isPlanBeingEdited = useIsEditingPlan();
const handleDeletePlanSuccess = useOnboardingStore(
(s) => s.handleDeletePlanSuccess,
);
@@ -78,7 +79,7 @@ export const OnboardingPreview = ({
}
return (
<Card className="min-w-[28rem] max-w-xl mx-4 bg-card border-border border-[0.5px] gap-0 p-4">
<Card className="min-w-[28rem] max-w-xl mx-4 bg-card border-border border-[0.5px] p-4">
<CardHeader className="gap-0 px-0">
<div className="flex flex-row items-center justify-between w-full">
<div className="flex flex-row items-center gap-2 min-w-0 flex-1">
@@ -110,24 +111,13 @@ export const OnboardingPreview = ({
</div>
</div>
{/* {showBasicInfo && product?.description && (
<span className="text-sm text-t3 max-w-[80%] line-clamp-2">
{product.description}
</span>
)} */}
{/* {showBasicInfo &&
!(product?.description || product?.name || basePrice?.amount) && (
<span className="text-body-secondary">
Enter data on the right to see the preview
</span>
)} */}
{showPricing && (
<IconButton
variant="secondary"
icon={<CrosshairSimpleIcon />}
className="mt-2 pointer-events-none"
className="mt-2 !opacity-100"
onClick={handleEdit}
disabled={isPlanBeingEdited}
>
{basePrice?.amount ? (
<span className="text-sm font-medium text-t2">
@@ -141,20 +131,15 @@ export const OnboardingPreview = ({
)}
</IconButton>
)}
{showDummyFeature && feature && (
<>
<Separator className="my-2" />
<DummyFeatureRow feature={feature} />
</>
)}
{/* {!showFeatures && !showDummyFeature && (
<span className="text-body-secondary mt-2">
Create a feature on the right
</span>
)} */}
{showFeatures && <Separator className="my-2" />}
</CardHeader>
{showDummyFeature && feature && (
<>
<Separator className="my-2" />
<DummyFeatureRow feature={feature} />
</>
)}
{showFeatures && <Separator />}
<CardContent className="max-w-full px-0 gap-0">
{showFeatures && (
<div>

View File

@@ -1,3 +1,4 @@
import type { ProductV2 } from "@autumn/shared";
import type { AxiosError } from "axios";
import { useState } from "react";
import { toast } from "sonner";
@@ -26,16 +27,26 @@ import { getBackendErr } from "@/utils/genUtils";
import { useProductQuery } from "../../product/hooks/useProductQuery";
export const DeletePlanDialog = ({
propProduct,
open,
setOpen,
onDeleteSuccess,
}: {
propProduct?: ProductV2;
open: boolean;
setOpen: (open: boolean) => void;
onDeleteSuccess?: () => Promise<void>;
}) => {
const axiosInstance = useAxiosInstance();
const product = useProductStore((s) => s.product);
const storeProduct = useProductStore((s) => s.product);
let product: ProductV2;
if (propProduct) {
product = propProduct;
} else {
product = storeProduct;
}
const [loading, setLoading] = useState(false);
const [deleteAllVersions, setDeleteAllVersions] = useState(false);
const { invalidate: invalidateProducts } = useProductsQuery();
@@ -58,13 +69,14 @@ export const DeletePlanDialog = ({
);
await Promise.all([invalidateProducts(), invalidateProduct()]);
setOpen(false);
toast.success("Product deleted successfully");
// Call onDeleteSuccess callback if provided (for onboarding)
if (onDeleteSuccess) {
await onDeleteSuccess();
}
setOpen(false);
toast.success("Product deleted successfully");
} catch (error: unknown) {
toast.error(getBackendErr(error as AxiosError, "Error deleting product"));
} finally {
@@ -78,6 +90,10 @@ export const DeletePlanDialog = ({
await ProductService.updateProduct(axiosInstance, product.id, {
archived: true,
});
if (onDeleteSuccess) {
await onDeleteSuccess();
}
toast.success(`${product.name} archived successfully`);
setOpen(false);
await Promise.all([invalidateProducts(), invalidateProduct()]);
@@ -94,6 +110,10 @@ export const DeletePlanDialog = ({
await ProductService.updateProduct(axiosInstance, product.id, {
archived: false,
});
if (onDeleteSuccess) {
await onDeleteSuccess();
}
await refetchProduct();
toast.success(`${product.name} unarchived successfully`);
setOpen(false);

View File

@@ -110,7 +110,7 @@ export const EditPlanHeader = () => {
className="p-0"
items={[
{
name: "Plans",
name: "Products",
href: "/products?tab=products",
},
{

View File

@@ -1,6 +1,6 @@
import { useHotkeys } from "react-hotkeys-hook";
import { Card, CardContent } from "@/components/v2/cards/Card";
import { Separator } from "@/components/v2/separator";
import { useFeatureNavigation } from "../../hooks/useFeatureNavigation";
import { PlanCardHeader } from "./PlanCardHeader";
import { PlanFeatureList } from "./PlanFeatureList";
@@ -16,6 +16,11 @@ export default function PlanCard() {
return (
<Card className={`min-w-sm max-w-xl mx-4 bg-card w-[80%]`}>
<PlanCardHeader />
<div className="px-4">
<Separator />
</div>
<CardContent className="max-w-full">
<PlanFeatureList />
</CardContent>

View File

@@ -1,5 +1,6 @@
import { mapToProductV3 } from "@autumn/shared";
import { CrosshairSimpleIcon } from "@phosphor-icons/react";
import { useNavigate } from "react-router";
import { PlanTypeBadges } from "@/components/v2/badges/PlanTypeBadges";
import { IconButton } from "@/components/v2/buttons/IconButton";
import { CardHeader } from "@/components/v2/cards/Card";
@@ -9,6 +10,7 @@ import { keyToTitle } from "@/utils/formatUtils/formatTextUtils";
import { PlanCardToolbar } from "./PlanCardToolbar";
export const PlanCardHeader = () => {
const navigate = useNavigate();
const product = useProductStore((s) => s.product);
const setSheet = useSheetStore((s) => s.setSheet);
const isPlanBeingEdited = useIsEditingPlan();

View File

@@ -1,11 +1,13 @@
import { PencilSimpleIcon, TrashIcon } from "@phosphor-icons/react";
import { useState } from "react";
import { useNavigate } from "react-router";
import { Button } from "@/components/v2/buttons/Button";
import { CopyButton } from "@/components/v2/buttons/CopyButton";
import { IconButton } from "@/components/v2/buttons/IconButton";
import { useProductStore } from "@/hooks/stores/useProductStore";
import { useIsEditingPlan } from "@/hooks/stores/useSheetStore";
import { cn } from "@/lib/utils";
import { pushPage } from "@/utils/genUtils";
import { DeletePlanDialog } from "../DeletePlanDialog";
interface PlanCardToolbarProps {
@@ -18,7 +20,7 @@ interface PlanCardToolbarProps {
export const PlanCardToolbar = ({
onEdit,
onDeleteSuccess,
// onDeleteSuccess,
editDisabled,
deleteDisabled,
deleteTooltip,
@@ -26,13 +28,26 @@ export const PlanCardToolbar = ({
const product = useProductStore((s) => s.product);
const [deleteOpen, setDeleteOpen] = useState(false);
const isEditingPlan = useIsEditingPlan();
const navigate = useNavigate();
console.log("deleteOpen", deleteOpen);
return (
<>
<DeletePlanDialog
open={deleteOpen}
setOpen={setDeleteOpen}
onDeleteSuccess={onDeleteSuccess}
onDeleteSuccess={async () => {
console.log("onDeleteSuccess");
pushPage({
navigate,
path: "/products",
queryParams: {
tab: "products",
},
preserveParams: true,
});
}}
/>
<div className="flex flex-row items-center gap-1">
{product?.id && (

View File

@@ -28,10 +28,12 @@ export const DeleteProductDialog = ({
product,
open,
setOpen,
onDeleteSuccess,
}: {
product: ProductV2;
open: boolean;
setOpen: (open: boolean) => void;
onDeleteSuccess?: () => Promise<void>;
}) => {
const axiosInstance = useAxiosInstance();
const [loading, setLoading] = useState(false);
@@ -55,6 +57,11 @@ export const DeleteProductDialog = ({
await refetchProducts();
setOpen(false);
if (onDeleteSuccess) {
await onDeleteSuccess();
}
toast.success("Product deleted successfully");
} catch (error: unknown) {
toast.error(getBackendErr(error as AxiosError, "Error deleting product"));
@@ -71,6 +78,9 @@ export const DeleteProductDialog = ({
});
toast.success(`${product.name} archived successfully`);
setOpen(false);
if (onDeleteSuccess) {
await onDeleteSuccess();
}
await refetchProducts();
} catch (error: unknown) {
toast.error(
@@ -88,6 +98,9 @@ export const DeleteProductDialog = ({
archived: false,
});
await refetchProducts();
if (onDeleteSuccess) {
await onDeleteSuccess();
}
toast.success(`${product.name} unarchived successfully`);
setOpen(false);
} catch (error: unknown) {

View File

@@ -1,6 +1,7 @@
import type { ProductV2 } from "@autumn/shared";
import { Archive, ArchiveRestore, Copy, Pen } from "lucide-react";
import type { ProductCounts, ProductV2 } from "@autumn/shared";
import { Archive, ArchiveRestore, Copy, Delete, Pen } from "lucide-react";
import { useState } from "react";
import { useNavigate } from "react-router";
import { ToolbarButton } from "@/components/general/table-components/ToolbarButton";
import {
DropdownMenu,
@@ -8,26 +9,30 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { DeletePlanDialog } from "../../plan/components/DeletePlanDialog";
import { CopyProductDialog } from "../components/CopyProductDialog";
import { DeleteProductDialog } from "../components/DeleteProductDialog";
import { UpdateProductDialog } from "../components/UpdateProductDialog";
export const ProductRowToolbar = ({
className,
isOnboarding = false,
product,
productCounts,
}: {
isOnboarding?: boolean;
className?: string;
product: ProductV2;
productCounts: ProductCounts | undefined;
}) => {
const [dropdownOpen, setDropdownOpen] = useState(false);
const [updateOpen, setUpdateOpen] = useState(false);
const [copyOpen, setCopyOpen] = useState(false);
const [deleteOpen, setDeleteOpen] = useState(false);
const navigate = useNavigate();
let deleteText = "Archive";
let DeleteIcon = Archive;
const allCount = productCounts?.all || 0;
let deleteText = allCount > 0 ? "Archive" : "Delete";
let DeleteIcon = allCount > 0 ? Archive : Delete;
if (product.archived) {
deleteText = "Unarchive";
@@ -46,11 +51,29 @@ export const ProductRowToolbar = ({
setOpen={setCopyOpen}
product={product}
/>
<DeleteProductDialog
<DeletePlanDialog
propProduct={product}
open={deleteOpen}
setOpen={setDeleteOpen}
// onDeleteSuccess={async () => {
// pushPage({
// navigate,
// path: "/products",
// queryParams: {
// tab: "products",
// },
// preserveParams: true,
// });
// }}
/>
{/* <DeleteProductDialog
product={product}
open={deleteOpen}
setOpen={setDeleteOpen}
/>
// productCounts={productCounts}
// dropdownOpen={dropdownOpen}
/> */}
<DropdownMenu open={dropdownOpen} onOpenChange={setDropdownOpen}>
<DropdownMenuTrigger asChild>