Merge pull request #267 from useautumn/johnyeocx/fix-product-delete-logic
Fix product delete logic and improve onboarding UX
This commit is contained in:
@@ -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+
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -110,7 +110,7 @@ export const EditPlanHeader = () => {
|
||||
className="p-0"
|
||||
items={[
|
||||
{
|
||||
name: "Plans",
|
||||
name: "Products",
|
||||
href: "/products?tab=products",
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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) {
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user