fix: archive / delete on product row toolbar

This commit is contained in:
John Yeo
2025-07-30 21:37:52 -07:00
parent 531cc6cd83
commit 6fe67e89ef
6 changed files with 520 additions and 498 deletions

View File

@@ -15,8 +15,8 @@ import { mapToProductV2 } from "./productV2Utils.js";
import { isFeaturePriceItem } from "./product-items/productItemUtils/getItemType.js";
import RecaseError, {
handleFrontendReqError,
handleRequestError,
handleFrontendReqError,
handleRequestError,
} from "@/utils/errorUtils.js";
import { createOrgResponse } from "../orgs/orgUtils.js";
import { sortFullProducts } from "./productUtils/sortProductUtils.js";
@@ -24,349 +24,343 @@ import { sortFullProducts } from "./productUtils/sortProductUtils.js";
export const productRouter: Router = Router({ mergeParams: true });
productRouter.get("/data", async (req: any, res) => {
try {
let { db } = req;
try {
let { db } = req;
const [products, features, org, coupons, rewardPrograms] =
await Promise.all([
ProductService.listFull({
db,
orgId: req.orgId,
env: req.env,
returnAll: true,
showOnlyArchived: false,
}),
FeatureService.getFromReq(req),
OrgService.getFromReq(req),
RewardService.list({ db, orgId: req.orgId, env: req.env }),
RewardProgramService.list({
db,
orgId: req.orgId,
env: req.env,
}),
]);
const [products, features, org, coupons, rewardPrograms] =
await Promise.all([
ProductService.listFull({
db,
orgId: req.orgId,
env: req.env,
returnAll: true,
showOnlyArchived: false,
}),
FeatureService.getFromReq(req),
OrgService.getFromReq(req),
RewardService.list({ db, orgId: req.orgId, env: req.env }),
RewardProgramService.list({
db,
orgId: req.orgId,
env: req.env,
}),
]);
sortFullProducts({
products: getLatestProducts(products),
});
sortFullProducts({
products: getLatestProducts(products),
});
res.status(200).json({
products: products.map((product) => {
return mapToProductV2({ product, features });
}),
versionCounts: getProductVersionCounts(products),
features,
org: createOrgResponse(org),
rewards: coupons,
rewardPrograms,
});
} catch (error) {
console.error("Failed to get products", error);
res.status(500).send(error);
}
res.status(200).json({
products: products.map((product) => {
return mapToProductV2({ product, features });
}),
versionCounts: getProductVersionCounts(products),
features,
org: createOrgResponse(org),
rewards: coupons,
rewardPrograms,
});
} catch (error) {
console.error("Failed to get products", error);
res.status(500).send(error);
}
});
productRouter.post("/data", async (req: any, res) => {
try {
let { db } = req;
let { showArchived } = req.body;
console.log("showArchived", showArchived);
try {
let { db } = req;
let { showArchived } = req.body;
console.log("showArchived", showArchived);
const [products, features, org, coupons, rewardPrograms] =
await Promise.all([
ProductService.listFull({
db,
orgId: req.orgId,
env: req.env,
returnAll: true,
showOnlyArchived: showArchived,
}),
FeatureService.getFromReq(req),
OrgService.getFromReq(req),
RewardService.list({ db, orgId: req.orgId, env: req.env }),
RewardProgramService.list({
db,
orgId: req.orgId,
env: req.env,
}),
]);
const [products, features, org, coupons, rewardPrograms] =
await Promise.all([
ProductService.listFull({
db,
orgId: req.orgId,
env: req.env,
returnAll: true,
showOnlyArchived: showArchived,
}),
FeatureService.getFromReq(req),
OrgService.getFromReq(req),
RewardService.list({ db, orgId: req.orgId, env: req.env }),
RewardProgramService.list({
db,
orgId: req.orgId,
env: req.env,
}),
]);
res.status(200).json({
products: sortFullProducts({ products }).map((product) => {
return mapToProductV2({ product, features });
}),
versionCounts: getProductVersionCounts(products),
features,
org: createOrgResponse(org),
rewards: coupons,
rewardPrograms,
});
} catch (error) {
console.error("Failed to get products", error);
res.status(500).send(error);
}
res.status(200).json({
products: sortFullProducts({ products }).map((product) => {
return mapToProductV2({ product, features });
}),
versionCounts: getProductVersionCounts(products),
features,
org: createOrgResponse(org),
rewards: coupons,
rewardPrograms,
});
} catch (error) {
console.error("Failed to get products", error);
res.status(500).send(error);
}
});
productRouter.get("/data/deletion_text/:internal_product_id", async (req: any, res) => {
try {
let { db, orgId, env } = req;
let { internal_product_id } = req.params;
productRouter.get(
"/data/deletion_text/:internal_product_id",
async (req: any, res) => {
try {
let { db, orgId, env } = req;
let { internal_product_id } = req.params;
let deletionText = await ProductService.getDeletionText({
db,
internal_product_id,
orgId,
env,
});
let deletionText = await ProductService.getDeletionText({
db,
internal_product_id,
orgId,
env,
});
console.log("deletionText", deletionText);
res.status(200).send({
customerName: deletionText[0]?.customerName,
totalCount: deletionText[0]?.totalCount,
});
} catch (error) {
console.error("Failed to get deletion text", error);
res.status(500).send(error);
}
});
res.status(200).send({
customerName: deletionText[0]?.customerName,
totalCount: deletionText[0]?.totalCount,
});
} catch (error) {
console.error("Failed to get deletion text", error);
res.status(500).send(error);
}
}
);
productRouter.get("/counts", async (req: any, res) => {
try {
let { db } = req;
let products = await ProductService.listFull({
db,
orgId: req.orgId,
env: req.env,
returnAll: true,
});
try {
let { db } = req;
let products = await ProductService.listFull({
db,
orgId: req.orgId,
env: req.env,
// returnAll: true,
});
let counts = await Promise.all(
products.map(async (product) => {
return CusProdReadService.getCounts({
db,
internalProductId: product.internal_id,
});
})
);
let counts = await Promise.all(
products.map(async (product) => {
return CusProdReadService.getCountsForAllVersions({
db,
productId: product.id,
orgId: req.orgId,
env: req.env,
});
})
);
let result: { [key: string]: any } = {};
for (let i = 0; i < products.length; i++) {
if (!result[products[i].id]) {
result[products[i].id] = counts[i];
} else {
for (let key in counts[i]) {
let countVal =
counts[i][key as keyof (typeof counts)[number]] || 0;
result[products[i].id][key] += countVal;
}
}
}
let result: { [key: string]: any } = {};
for (let i = 0; i < products.length; i++) {
if (!result[products[i].id]) {
result[products[i].id] = counts[i];
}
}
res.status(200).send(result);
} catch (error) {
console.error("Failed to get product counts", error);
res.status(500).send(error);
}
res.status(200).send(result);
} catch (error) {
console.error("Failed to get product counts", error);
res.status(500).send(error);
}
});
productRouter.get("/:productId/data", async (req: any, res) => {
try {
const { productId } = req.params;
const { version } = req.query;
const { db, orgId, env } = req;
try {
const { productId } = req.params;
const { version } = req.query;
const { db, orgId, env } = req;
const [product, features, org, numVersions, existingMigrations] =
await Promise.all([
ProductService.getFull({
db,
idOrInternalId: productId,
orgId,
env,
version: version ? parseInt(version) : undefined,
}),
FeatureService.getFromReq(req),
OrgService.getFromReq(req),
ProductService.getProductVersionCount({
db,
productId,
orgId,
env,
}),
MigrationService.getExistingJobs({
db,
orgId,
env,
}),
]);
const [product, features, org, numVersions, existingMigrations] =
await Promise.all([
ProductService.getFull({
db,
idOrInternalId: productId,
orgId,
env,
version: version ? parseInt(version) : undefined,
}),
FeatureService.getFromReq(req),
OrgService.getFromReq(req),
ProductService.getProductVersionCount({
db,
productId,
orgId,
env,
}),
MigrationService.getExistingJobs({
db,
orgId,
env,
}),
]);
if (!product) {
throw new RecaseError({
message: `Product ${productId} ${
version ? `(v${version})` : ""
} not found`,
code: ErrCode.ProductNotFound,
statusCode: StatusCodes.NOT_FOUND,
});
}
let entitlements = product.entitlements;
let prices = product.prices;
if (!product) {
throw new RecaseError({
message: `Product ${productId} ${
version ? `(v${version})` : ""
} not found`,
code: ErrCode.ProductNotFound,
statusCode: StatusCodes.NOT_FOUND,
});
}
let entitlements = product.entitlements;
let prices = product.prices;
entitlements = entitlements.sort((a: any, b: any) => {
return b.feature.id.localeCompare(a.feature.id);
});
entitlements = entitlements.sort((a: any, b: any) => {
return b.feature.id.localeCompare(a.feature.id);
});
prices = prices.sort((a: any, b: any) => {
return b.id.localeCompare(a.id);
});
prices = prices.sort((a: any, b: any) => {
return b.id.localeCompare(a.id);
});
let productV2 = mapToProductV2({ product, features });
let productV2 = mapToProductV2({ product, features });
res.status(200).send({
product: productV2,
entitlements,
prices,
features,
org: {
id: org.id,
name: org.name,
test_pkey: org.test_pkey,
live_pkey: org.live_pkey,
default_currency: org.default_currency,
},
numVersions,
existingMigrations,
});
} catch (error) {
handleFrontendReqError({
error,
req,
res,
action: "Get product data (internal)",
});
}
res.status(200).send({
product: productV2,
entitlements,
prices,
features,
org: {
id: org.id,
name: org.name,
test_pkey: org.test_pkey,
live_pkey: org.live_pkey,
default_currency: org.default_currency,
},
numVersions,
existingMigrations,
});
} catch (error) {
handleFrontendReqError({
error,
req,
res,
action: "Get product data (internal)",
});
}
});
productRouter.get("/:productId/count", async (req: any, res) => {
try {
const { db, orgId, env } = req;
const { productId } = req.params;
const { version } = req.query;
try {
const { db, orgId, env } = req;
const { productId } = req.params;
const { version } = req.query;
const product = await ProductService.get({
db,
id: productId,
orgId,
env,
version: version ? parseInt(version) : undefined,
});
const product = await ProductService.get({
db,
id: productId,
orgId,
env,
version: version ? parseInt(version) : undefined,
});
if (!product) {
throw new RecaseError({
message: `Product ${productId} ${
version ? `(v${version})` : ""
} not found`,
code: ErrCode.ProductNotFound,
statusCode: StatusCodes.NOT_FOUND,
});
}
if (!product) {
throw new RecaseError({
message: `Product ${productId} ${
version ? `(v${version})` : ""
} not found`,
code: ErrCode.ProductNotFound,
statusCode: StatusCodes.NOT_FOUND,
});
}
// Get counts from postgres
const counts = await CusProdReadService.getCounts({
db,
internalProductId: product.internal_id,
});
// Get counts from postgres
const counts = await CusProdReadService.getCounts({
db,
internalProductId: product.internal_id,
});
res.status(200).send(counts);
} catch (error) {
handleFrontendReqError({
error,
req,
res,
action: "Get product counts (internal)",
});
}
res.status(200).send(counts);
} catch (error) {
handleFrontendReqError({
error,
req,
res,
action: "Get product counts (internal)",
});
}
});
productRouter.post("/product_options", async (req: any, res: any) => {
try {
const { items } = req.body;
try {
const { items } = req.body;
const features = await FeatureService.getFromReq(req);
const featureToOptions: { [key: string]: FeatureOptions } = {};
const features = await FeatureService.getFromReq(req);
const featureToOptions: { [key: string]: FeatureOptions } = {};
for (const item of items) {
if (
isFeaturePriceItem(item) &&
item.usage_model == UsageModel.Prepaid
) {
featureToOptions[item.feature_id] = {
feature_id: item.feature_id,
quantity: 0,
};
}
}
for (const item of items) {
if (isFeaturePriceItem(item) && item.usage_model == UsageModel.Prepaid) {
featureToOptions[item.feature_id] = {
feature_id: item.feature_id,
quantity: 0,
};
}
}
res.status(200).send({ options: Object.values(featureToOptions) });
} catch (error) {
handleFrontendReqError({
error,
req,
res,
action: "Get product options",
});
}
res.status(200).send({ options: Object.values(featureToOptions) });
} catch (error) {
handleFrontendReqError({
error,
req,
res,
action: "Get product options",
});
}
});
productRouter.get("/:productId/info", async (req: any, res: any) => {
try {
// 1. Get number of versions
const { db, orgId, env } = req;
let product = await ProductService.get({
db,
id: req.params.productId,
orgId: req.orgId,
env: req.env,
});
try {
// 1. Get number of versions
const { db, orgId, env } = req;
let product = await ProductService.get({
db,
id: req.params.productId,
orgId: req.orgId,
env: req.env,
});
// let numVersions = await ProductService.getProductVersionCount({
// db,
// productId: req.params.productId,
// orgId: req.orgId,
// env: req.env,
// });
if (!product) {
throw new RecaseError({
message: `Product ${req.params.productId} not found`,
code: ErrCode.ProductNotFound,
statusCode: StatusCodes.NOT_FOUND,
});
}
// let numVersions = await ProductService.getProductVersionCount({
// db,
// productId: req.params.productId,
// orgId: req.orgId,
// env: req.env,
// });
if (!product) {
throw new RecaseError({
message: `Product ${req.params.productId} not found`,
code: ErrCode.ProductNotFound,
statusCode: StatusCodes.NOT_FOUND,
});
}
let [allVersions, latestVersion] = await Promise.all([
CusProdReadService.existsForProduct({
db,
productId: req.params.productId,
}),
CusProdReadService.existsForProduct({
db,
internalProductId: product.internal_id,
}),
]);
let [allVersions, latestVersion] = await Promise.all([
CusProdReadService.existsForProduct({
db,
productId: req.params.productId,
}),
CusProdReadService.existsForProduct({
db,
internalProductId: product.internal_id,
}),
]);
// 2. Get cus products
// 2. Get cus products
res.status(200).send({
numVersion: product.version,
hasCusProducts: allVersions,
hasCusProductsLatest: latestVersion,
});
} catch (error) {
handleRequestError({
error,
req,
res,
action: "Get product info",
});
}
res.status(200).send({
numVersion: product.version,
hasCusProducts: allVersions,
hasCusProductsLatest: latestVersion,
});
} catch (error) {
handleRequestError({
error,
req,
res,
action: "Get product info",
});
}
});

View File

@@ -6,7 +6,50 @@ import {
Organization,
products,
} from "@autumn/shared";
import { and, eq } from "drizzle-orm";
import { and, eq, inArray } from "drizzle-orm";
const clearCustomersInBatches = async ({
db,
org,
batchSize = 450,
}: {
db: DrizzleCli;
org: Organization;
batchSize?: number;
}) => {
let deletedCount = 0;
while (true) {
// Get a batch of customer IDs to delete
const customerBatch = await db
.select({ internalId: customers.internal_id })
.from(customers)
.where(
and(eq(customers.org_id, org.id), eq(customers.env, AppEnv.Sandbox))
)
.limit(batchSize);
if (customerBatch.length === 0) {
break; // No more customers to delete
}
// Delete the batch
const customerIds = customerBatch
.map((c) => c.internalId)
.filter((id) => id !== null);
await db
.delete(customers)
.where(inArray(customers.internal_id, customerIds));
deletedCount += customerBatch.length;
console.log(
`Deleted ${customerBatch.length} customers (total: ${deletedCount})`
);
}
return deletedCount;
};
export const clearOrg = async ({
db,
@@ -15,13 +58,8 @@ export const clearOrg = async ({
db: DrizzleCli;
org: Organization;
}) => {
await db
.delete(customers)
.where(
and(eq(customers.org_id, org.id), eq(customers.env, AppEnv.Sandbox)),
);
console.log("Cleared customers");
const deletedCount = await clearCustomersInBatches({ db, org });
console.log(`Cleared ${deletedCount} customers`);
await db
.delete(products)

View File

@@ -57,7 +57,7 @@ export const FrontendProductSchema = ProductSchema.omit({
entitlements: z.array(
EntitlementSchema.extend({
feature: FeatureSchema,
}),
})
),
free_trial: FreeTrialSchema,
options: z.any(),
@@ -68,13 +68,21 @@ export const FullProductSchema = ProductSchema.extend({
entitlements: z.array(
EntitlementSchema.extend({
feature: FeatureSchema,
}),
})
),
free_trial: FreeTrialSchema.nullish(),
free_trials: z.array(FreeTrialSchema).nullish(),
free_trial_ids: z.array(z.string()).nullish(),
});
export type ProductCounts = {
active: number;
canceled: number;
custom: number;
trialing: number;
all: number;
};
export type Product = z.infer<typeof ProductSchema>;
export type FrontendProduct = z.infer<typeof FrontendProductSchema>;
export type FullProduct = z.infer<typeof FullProductSchema>;

View File

@@ -12,179 +12,172 @@ import { ProductCountsTooltip } from "./components/ProductCountsTooltip";
import { ProductTypeBadge } from "./components/ProductTypeBadge";
export const ProductsTable = ({
products,
onRowClick,
products,
onRowClick,
}: {
products: Product[];
onRowClick?: (id: string) => void;
products: Product[];
onRowClick?: (id: string) => void;
}) => {
const { env, onboarding, showArchived } = useProductsContext();
const navigate = useNavigate();
const { allCounts } = useProductsContext();
const { env, onboarding, showArchived } = useProductsContext();
const navigate = useNavigate();
const { allCounts } = useProductsContext();
return (
<>
{products && products.length > 0 ? (
<Row
type="header"
className={cn(
"grid-cols-18 -mb-1",
onboarding && "grid-cols-12"
)}
isOnboarding={onboarding}
>
<Item className="col-span-3">Name</Item>
<Item className="col-span-3">Product ID</Item>
{!onboarding && (
<>
<Item className="col-span-3">Active</Item>
<Item className="col-span-3">Type</Item>
<Item className="col-span-3">Group</Item>
<Item className="col-span-2">Created At</Item>
</>
)}
<Item
className={cn("col-span-1", onboarding && "col-span-6")}
></Item>
</Row>
) : (
!onboarding && (
<div
className={cn(
"flex flex-col justify-center items-center h-10 px-10 text-t3 min-h-[60vh] gap-4",
"justify-start items-start mt-3",
onboarding && "px-2 mt-4"
)}
>
{showArchived ? (
<span>You haven't archived any products yet.</span>
) : (
<>
{/* <img
return (
<>
{products && products.length > 0 ? (
<Row
type="header"
className={cn("grid-cols-18 -mb-1", onboarding && "grid-cols-12")}
isOnboarding={onboarding}
>
<Item className="col-span-3">Name</Item>
<Item className="col-span-3">Product ID</Item>
{!onboarding && (
<>
<Item className="col-span-3">Active</Item>
<Item className="col-span-3">Type</Item>
<Item className="col-span-3">Group</Item>
<Item className="col-span-2">Created At</Item>
</>
)}
<Item className={cn("col-span-1", onboarding && "col-span-6")}></Item>
</Row>
) : (
!onboarding && (
<div
className={cn(
"flex flex-col justify-center items-center h-10 px-10 text-t3 min-h-[60vh] gap-4",
"justify-start items-start mt-3",
onboarding && "px-2 mt-4"
)}
>
{showArchived ? (
<span>You haven't archived any products yet.</span>
) : (
<>
{/* <img
src="./product.png"
alt="Products"
className="w-48 h-48 opacity-60 filter grayscale"
// className="w-48 h-48 opacity-80 filter brightness-0 invert" // this is for dark mode
/> */}
<span>
Each product defines features your customers
get access to and how much they cost. Create
separate products for any free plans, paid
plans and any add-on or top up products
</span>
</>
)}
</div>
)
)}
<span>
Each product defines features your customers get access to and
how much they cost. Create separate products for any free
plans, paid plans and any add-on or top up products
</span>
</>
)}
</div>
)
)}
{products &&
products
.reduce((acc, product) => {
const existingIndex = acc.findIndex(p => p.id === product.id);
if (existingIndex === -1) {
acc.push(product);
} else {
const existing = acc[existingIndex];
if (showArchived) {
// If showing archived, always keep the newest version
if (product.version > existing.version) {
acc[existingIndex] = product;
}
} else {
// If not showing archived, prefer non-archived versions
if (product.archived && !existing.archived) {
// Keep existing non-archived version
} else if (!product.archived && existing.archived) {
// Replace archived with non-archived
acc[existingIndex] = product;
} else if (product.version > existing.version) {
// Both have same archived status, keep newer version
acc[existingIndex] = product;
}
}
}
return acc;
}, [] as typeof products)
.map((product) => (
<Row
key={product.id}
className={cn(
"grid-cols-18 gap-2 items-center text-sm cursor-pointer hover:bg-primary/5 text-t2 whitespace-nowrap",
onboarding && "grid-cols-12"
)}
isOnboarding={onboarding}
onClick={() => {
if (onRowClick) {
onRowClick(product.id);
} else {
navigateTo(
`/products/${product.id}`,
navigate,
env
);
}
}}
>
<Item className="col-span-3">
<AdminHover
texts={[
{
key: "Internal ID",
value: product.internal_id,
},
{
key: "Version",
value: product.version.toString(),
},
]}
>
<span className="truncate">{product.name}</span>
</AdminHover>
</Item>
<Item className="col-span-3 font-mono -translate-x-1">
<CopyButton
text={product.id || ""}
className="bg-transparent text-t3 border-none px-1 shadow-none max-w-full"
>
<span className="truncate">{product.id}</span>
</CopyButton>
</Item>
{!onboarding && (
<>
<Item className="col-span-3">
<ProductCountsTooltip
allCounts={allCounts}
product={product}
/>
</Item>
<Item className="col-span-3">
<ProductTypeBadge product={product} />
</Item>
<Item className="col-span-3">
{!onboarding && product.group}
</Item>
<Item className="col-span-2 lg:overflow-visible text-t3 text-xs">
{
formatUnixToDateTime(product.created_at)
.date
}
</Item>
</>
)}
<Item
className={cn(
"col-span-1 items-center justify-end",
onboarding && "col-span-6"
)}
>
<ProductRowToolbar product={product} />
</Item>
</Row>
))}
</>
);
{products &&
products
.reduce(
(acc, product) => {
const existingIndex = acc.findIndex((p) => p.id === product.id);
if (existingIndex === -1) {
acc.push(product);
} else {
const existing = acc[existingIndex];
if (showArchived) {
// If showing archived, always keep the newest version
if (product.version > existing.version) {
acc[existingIndex] = product;
}
} else {
// If not showing archived, prefer non-archived versions
if (product.archived && !existing.archived) {
// Keep existing non-archived version
} else if (!product.archived && existing.archived) {
// Replace archived with non-archived
acc[existingIndex] = product;
} else if (product.version > existing.version) {
// Both have same archived status, keep newer version
acc[existingIndex] = product;
}
}
}
return acc;
},
[] as typeof products
)
.map((product) => (
<Row
key={product.id}
className={cn(
"grid-cols-18 gap-2 items-center text-sm cursor-pointer hover:bg-primary/5 text-t2 whitespace-nowrap",
onboarding && "grid-cols-12"
)}
isOnboarding={onboarding}
onClick={() => {
if (onRowClick) {
onRowClick(product.id);
} else {
navigateTo(`/products/${product.id}`, navigate, env);
}
}}
>
<Item className="col-span-3">
<AdminHover
texts={[
{
key: "Internal ID",
value: product.internal_id,
},
{
key: "Version",
value: product.version.toString(),
},
]}
>
<span className="truncate">{product.name}</span>
</AdminHover>
</Item>
<Item className="col-span-3 font-mono -translate-x-1">
<CopyButton
text={product.id || ""}
className="bg-transparent text-t3 border-none px-1 shadow-none max-w-full"
>
<span className="truncate">{product.id}</span>
</CopyButton>
</Item>
{!onboarding && (
<>
<Item className="col-span-3">
<ProductCountsTooltip
allCounts={allCounts}
product={product}
/>
</Item>
<Item className="col-span-3">
<ProductTypeBadge product={product} />
</Item>
<Item className="col-span-3">
{!onboarding && product.group}
</Item>
<Item className="col-span-2 lg:overflow-visible text-t3 text-xs">
{formatUnixToDateTime(product.created_at).date}
</Item>
</>
)}
<Item
className={cn(
"col-span-1 items-center justify-end",
onboarding && "col-span-6"
)}
>
<ProductRowToolbar
product={product}
productCounts={allCounts[product.id]}
/>
</Item>
</Row>
))}
</>
);
};

View File

@@ -18,7 +18,7 @@ export const ProductCountsTooltip = ({
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<p className="rounded-full text-t3 px-2 font-mono py-0">
<p className="rounded-full text-t3 font-mono py-0">
{(allCounts && allCounts[product.id]?.active) || 0}
</p>
</TooltipTrigger>

View File

@@ -9,26 +9,24 @@ import {
import { useState } from "react";
import { toast } from "sonner";
import { Product } from "@autumn/shared";
import { Product, ProductCounts } from "@autumn/shared";
import { ProductService } from "@/services/products/ProductService";
import { useProductsContext } from "../ProductsContext";
import { useAxiosInstance } from "@/services/useAxiosInstance";
import { getBackendErr } from "@/utils/genUtils";
import { ToolbarButton } from "@/components/general/table-components/ToolbarButton";
import { Dialog, DialogTrigger } from "@/components/ui/dialog";
import { UpdateProductDialog } from "../UpdateProduct";
import { CopyDialog } from "./CopyDialog";
import { Copy, Delete, Pen, ArchiveRestore } from "lucide-react";
import { Copy, Delete, Pen, ArchiveRestore, Archive } from "lucide-react";
import { DeleteProductDialog } from "./DeleteProductDialog";
export const ProductRowToolbar = ({
product,
productCounts,
}: {
className?: string;
product: Product;
productCounts: ProductCounts;
}) => {
const { mutate, env } = useProductsContext();
const axiosInstance = useAxiosInstance({ env });
const [deleteLoading, setDeleteLoading] = useState(false);
const [dropdownOpen, setDropdownOpen] = useState(false);
const [copyLoading, setCopyLoading] = useState(false);
@@ -38,18 +36,9 @@ export const ProductRowToolbar = ({
const [dialogType, setDialogType] = useState<"update" | "copy">("update");
const [deleteOpen, setDeleteOpen] = useState(false);
const handleDelete = async () => {
setDeleteLoading(true);
try {
await ProductService.deleteProduct(axiosInstance, product.id);
await mutate();
} catch (error) {
console.log("Error deleting product", error);
toast.error(getBackendErr(error, "Failed to delete product"));
}
setDeleteLoading(false);
setDeleteOpen(false);
};
const allCount = productCounts.all;
const deleteText = allCount > 0 ? "Archive" : "Delete";
const DeleteIcon = allCount > 0 ? Archive : Delete;
return (
<>
@@ -122,13 +111,13 @@ export const ProductRowToolbar = ({
}}
>
<div className="flex items-center justify-between w-full gap-2">
{product.archived ? 'Unarchive' : 'Delete'}
{deleteText}
{deleteLoading ? (
<SmallSpinner />
) : product.archived ? (
<ArchiveRestore size={12} className="text-t3" />
) : (
<Delete size={12} className="text-t3" />
<DeleteIcon size={12} className="text-t3" />
)}
</div>
</DropdownMenuItem>