edit and re publist

This commit is contained in:
Admin 2025-08-18 14:46:53 +07:00
parent 461331bace
commit ac365acbfc
14 changed files with 258 additions and 63 deletions

View File

@ -3,6 +3,7 @@ import PQueue from "p-queue";
let ports: chrome.runtime.Port[] = [];
const queue = new PQueue({ concurrency: 1 });
const targetUrl = "https://www.facebook.com/marketplace/you/selling";
chrome.runtime.onConnect.addListener((port) => {
ports.push(port);
@ -264,11 +265,34 @@ function ensureMarketplaceSellingTab() {
});
}
// Hàm check tab load xong
function waitForTargetTabLoad(): Promise<void> {
return new Promise((resolve) => {
// Query tab có url trước
chrome.tabs.query({ url: targetUrl }, (tabs) => {
if (tabs.length > 0 && tabs[0].status === "complete") {
return resolve();
}
});
// Nếu chưa load xong → lắng nghe sự kiện
const listener = (tabId: number, changeInfo: any, tab: chrome.tabs.Tab) => {
if (changeInfo.status === "complete" && tab.url === targetUrl) {
chrome.tabs.onUpdated.removeListener(listener);
resolve();
}
};
chrome.tabs.onUpdated.addListener(listener);
});
}
const init = async () => {
handleListenPublists();
handleListenDeletes();
handleListenEdits();
ensureMarketplaceSellingTab();
createAlarm();
};
chrome.runtime.onMessage.addListener((message, sender) => {
@ -278,4 +302,53 @@ chrome.runtime.onMessage.addListener((message, sender) => {
}
});
// Khởi tạo alarm (3040 giây, random)
function createAlarm() {
const delay = 30 + Math.floor(Math.random() * 11); // 30 → 40 giây
chrome.alarms.create("checkQueue", { delayInMinutes: delay / 60 });
}
// Task reload extension (nằm trong queue)
async function reloadExtensionTask() {
console.log("🔄 Reloading extension...");
chrome.runtime.reload();
// Sau khi reload, extension sẽ restart → task này chưa resolve.
// => Đặt 1 flag trong storage để biết đang chờ tab load
chrome.storage.local.set({ waitingForTab: true });
}
// Khi extension khởi động lại → check có đang chờ tab load không
chrome.runtime.onStartup.addListener(async () => {
chrome.storage.local.get("waitingForTab", async (data) => {
if (data.waitingForTab) {
console.log("⏳ Extension restarted, waiting for target tab...");
await waitForTargetTabLoad();
console.log("✅ Target tab loaded → task done");
chrome.storage.local.remove("waitingForTab");
}
});
});
// Alarm chạy định kỳ
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === "checkQueue") {
console.log("Alarm triggered, checking queue...");
const isIdle = queue.size === 0 && queue.pending === 0;
if (isIdle) {
console.log("Queue is empty → enqueue reload task");
queue.add(reloadExtensionTask);
} else {
console.log("Queue still busy → skip reload");
}
// đặt lại alarm mới
createAlarm();
}
});
init();

View File

@ -2,9 +2,13 @@
import type { TableState } from "~/components/core/data-table";
import { removeUndefinedValues } from "~/features/remove-falsy-values";
import { mapTableStateToPaginationQueryDSL } from "~/features/table";
import type { DeepPartial } from "react-hook-form";
import { handleError, handleSuccess } from ".";
import axios from "../lib/axios";
import type { DeepPartial } from "react-hook-form";
export interface IAPIOption {
toast_success: boolean;
}
export class BaseApiService<T extends { id: number }> {
constructor(protected readonly resourceUrl: string) {}
@ -17,84 +21,111 @@ export class BaseApiService<T extends { id: number }> {
const response = await axios({
url: this.resourceUrl,
params: params,
// withCredentials: true,
withCredentials: true,
method: "GET",
});
return response.data;
}
async get(id: T["id"]) {
const response = await axios({
url: this.resourceUrl + "/" + id,
// withCredentials: true,
method: "GET",
});
return response.data;
}
async create(values: Partial<Omit<T, "id" | "created_at" | "updated_at">>) {
async get(id: T["id"], options: IAPIOption = { toast_success: false }) {
try {
const newData = removeUndefinedValues(values);
const { data } = await axios({
const response = await axios({
url: this.resourceUrl + "/" + id,
withCredentials: true,
method: "GET",
});
if (options.toast_success) {
handleSuccess(response.data, this.resourceUrl);
}
return response.data;
} catch (error) {
handleError(error);
}
}
async create(
data: Partial<Omit<T, "id" | "created_at" | "updated_at">>,
options: IAPIOption = { toast_success: true }
) {
try {
const newData = removeUndefinedValues(data);
const { data: result } = await axios({
url: this.resourceUrl,
// withCredentials: true,
withCredentials: true,
method: "POST",
data: newData,
});
handleSuccess(data, this.resourceUrl);
return data;
if (options.toast_success) {
handleSuccess(result, this.resourceUrl);
}
return result;
} catch (error) {
handleError(error);
}
}
async update(id: T["id"], values: Partial<T>) {
async update(
id: T["id"],
data: Partial<T>,
options: IAPIOption = { toast_success: false }
) {
try {
const cleaned = removeUndefinedValues(values);
const { data } = await axios({
const cleaned = removeUndefinedValues(data);
const { data: result } = await axios({
url: `${this.resourceUrl}/${id}`,
// withCredentials: true,
withCredentials: true,
method: "PUT",
data: cleaned,
});
handleSuccess(data, this.resourceUrl);
if (options.toast_success) {
handleSuccess(result, this.resourceUrl);
}
return data;
return result;
} catch (error) {
handleError(error);
}
}
async delete(entity: T) {
async delete(entity: T, options: IAPIOption = { toast_success: false }) {
try {
const { data } = await axios({
url: `${this.resourceUrl}/${entity.id}`,
// withCredentials: true,
withCredentials: true,
method: "DELETE",
});
handleSuccess(data, this.resourceUrl);
if (options.toast_success) {
handleSuccess(data, this.resourceUrl);
}
return data;
} catch (error) {
handleError(error);
}
}
async bulkDelete(entities: T[]) {
async bulkDelete(
entities: T[],
options: IAPIOption = { toast_success: false }
) {
const ids = entities.map((e) => e.id);
try {
const { data } = await axios({
url: `${this.resourceUrl}/bulk-delete`,
// withCredentials: true,
withCredentials: true,
method: "DELETE",
data: { ids },
});
handleSuccess(data, this.resourceUrl);
if (options.toast_success) {
handleSuccess(data, this.resourceUrl);
}
return data;
} catch (error) {
@ -102,16 +133,21 @@ export class BaseApiService<T extends { id: number }> {
}
}
async bulkUpdate(entities: T[]) {
async bulkUpdate(
entities: T[],
options: IAPIOption = { toast_success: false }
) {
try {
const { data } = await axios({
url: `${this.resourceUrl}/bulk-update`,
// withCredentials: true,
withCredentials: true,
method: "PUT",
data: entities,
});
handleSuccess(data, this.resourceUrl);
if (options.toast_success) {
handleSuccess(data, this.resourceUrl);
}
return data;
} catch (error) {
@ -124,17 +160,20 @@ export class BaseApiService<T extends { id: number }> {
id: number,
endpoint: string,
payload?: Record<string, any>,
method?: string
method?: string,
options: IAPIOption = { toast_success: false }
) {
try {
const { data } = await axios({
url: `${this.resourceUrl}/${endpoint}/${id}`,
method: method || "POST",
data: removeUndefinedValues(payload || {}),
// withCredentials: true,
withCredentials: true,
});
handleSuccess(data, this.resourceUrl);
if (options.toast_success) {
handleSuccess(data, this.resourceUrl);
}
return data;
} catch (error) {
handleError(error);

View File

@ -22,7 +22,7 @@ export function ConfirmAlert({
}: {
children: ReactNode;
title?: string;
description?: string;
description?: string | ReactNode;
onConfirm: () => void | Promise<void>;
}) {
const [loading, setLoading] = useState(false);

View File

@ -0,0 +1,50 @@
import { type ReactNode, useState } from "react";
import { Checkbox } from "../ui/checkbox";
import { Label } from "../ui/label";
import { ConfirmAlert } from "./confirm-alert";
export function ConfirmDeleteAlert({
children,
title = "Confirm Delete",
data,
onConfirm,
}: {
children: ReactNode;
title?: string;
data: IProduct;
onConfirm: (unlist: boolean) => void | Promise<void>;
}) {
const [unlist, setUnlist] = useState(false);
return (
<ConfirmAlert
title={title}
description={
<div className="space-y-2">
<p className="text-sm text-muted-foreground">
This action will permanently delete the item. You can also choose to
unlist it before deleting.
</p>
{data.status && (
<div className="flex items-start gap-3">
<Checkbox
id="unlist-option"
checked={unlist}
onCheckedChange={(checked) => setUnlist(!!checked)}
/>
<div className="grid gap-1">
<Label htmlFor="unlist-option" className="font-medium">
Also unlist this item before deletion
</Label>
</div>
</div>
)}
</div>
}
onConfirm={() => onConfirm(unlist)}
>
{children}
</ConfirmAlert>
);
}

View File

@ -164,7 +164,8 @@ interface CustomAction<T> {
icon?: React.ReactNode;
variant?: "default" | "secondary" | "destructive" | "outline";
action: (row: T) => void;
show?: (row: T) => boolean; // Điều kiện hiển thị action
show?: (row: T) => boolean; // Điều kiện hiển thị action,
render?: (row: T, content: ReactNode) => ReactNode;
}
export interface InitialState<T> {
@ -227,7 +228,7 @@ export interface DataTableProps<T> {
onPageSizeChange?: (newPageSize: number) => void;
onRowClick?: (row: T) => void;
onEdit?: (row: T, children?: ReactNode) => void | ReactNode;
onDelete?: (row: T) => void;
onDelete?: (row: T, children?: ReactNode) => void | ReactNode;
onView?: (row: T) => void;
selectable?: boolean;
onSelectionChange?: (selectedRows: T[]) => void;
@ -1497,26 +1498,45 @@ export function DataTable<T extends Record<string, any>>({
)}
{/* Delete action (always last) */}
{onDelete && (
<DropdownMenuItem
disabled={
typeof options?.disableDel === "function"
? options.disableDel(row) // Gọi hàm với `row` là dữ liệu hiện tại
: options?.disableDel ?? false
{onDelete &&
(() => {
const baseItem = (
<DropdownMenuItem
onSelect={(e) => {
e.preventDefault();
e.stopPropagation();
}}
className="cursor-pointer"
>
<div className="flex items-center gap-2 cursor-pointer text-destructive hover:!text-destructive">
<Trash2 className="mr-2 h-4 w-4 text-destructive" />
Xóa
</div>
</DropdownMenuItem>
);
// Gọi thử onDelete(row, baseItem)
const result = onDelete(row, baseItem);
// Nếu onDelete trả về element thì dùng luôn, nếu không thì quấn ConfirmAlert
if (isValidElement(result)) {
return cloneElement(result, {}, baseItem);
}
onSelect={(e) => {
e.preventDefault(); // Ngăn dropdown đóng lại
e.stopPropagation();
}}
>
<ConfirmAlert onConfirm={() => onDelete(row)}>
<div className="flex items-center gap-2 cursor-pointer text-destructive hover:!text-destructive">
<Trash2 className="mr-2 h-4 w-4 text-destructive" />
Xóa
</div>
</ConfirmAlert>
</DropdownMenuItem>
)}
return (
<DropdownMenuItem
onSelect={(e) => {
e.preventDefault();
e.stopPropagation();
}}
className="cursor-pointer"
>
<ConfirmAlert onConfirm={() => onDelete(row) as any}>
{baseItem.props.children}
</ConfirmAlert>
</DropdownMenuItem>
);
})()}
</DropdownMenuContent>
</DropdownMenu>
);

View File

@ -149,7 +149,9 @@ export default function ProductModal({
await productApi.customAction(
response.data.id,
action,
response.data
response.data,
"POST",
{ toast_success: false }
);
}
} else {

View File

@ -11,7 +11,7 @@ import {
UploadCloud,
X,
} from "lucide-react";
import { useEffect, useMemo, useRef, useState } from "react";
import { useEffect, useMemo, useRef, useState, type ReactNode } from "react";
import { useSearchParams } from "react-router";
import { toast } from "sonner";
import { productApi } from "~/api/products-api.service";
@ -40,6 +40,8 @@ import { useAppSelector } from "~/hooks/use-app-dispatch";
import type { RootState } from "~/store";
import { HistoryModal } from "./components/modals/history-modal";
import ProductModal from "./components/modals/product-modal";
import { ConfirmAlert } from "~/components/btn/confirm-alert";
import { ConfirmDeleteAlert } from "~/components/btn/confirm-delete-alert";
export default function List() {
// const { setEdit } = useUserModal();
@ -61,8 +63,8 @@ export default function List() {
const { user } = useAppSelector((state: RootState) => state.app);
const handleDelete = async (data: IProduct) => {
if (data.status) {
const handleDelete = async (unlist: boolean, data: IProduct) => {
if (data.status && unlist) {
const response = await productApi.customAction(
data.id || 0,
"unlist",
@ -397,7 +399,16 @@ export default function List() {
} as TableState<IProduct>)
);
}}
onDelete={handleDelete}
onDelete={(data: IProduct, children) => {
return (
<ConfirmDeleteAlert
data={data}
onConfirm={(unlist) => handleDelete(unlist, data)}
>
{children}
</ConfirmDeleteAlert>
);
}}
onEdit={(data: IProduct, children) => {
return (
<ProductModal onSubmit={refetch} data={data}>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 518 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 281 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB