edit and re publist
|
|
@ -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 (30–40 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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -149,7 +149,9 @@ export default function ProductModal({
|
|||
await productApi.customAction(
|
||||
response.data.id,
|
||||
action,
|
||||
response.data
|
||||
response.data,
|
||||
"POST",
|
||||
{ toast_success: false }
|
||||
);
|
||||
}
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 518 KiB |
|
Before Width: | Height: | Size: 281 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |