fix UI freeze problem
This commit is contained in:
parent
3b1865c21d
commit
451eed4c65
81
src/components/buttons/delete-button.tsx
Normal file
81
src/components/buttons/delete-button.tsx
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import { Button } from "@/components/ui/button";
|
||||
import { Trash2 } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
|
||||
interface DeleteButtonProps {
|
||||
onClick: () => void | Promise<void>;
|
||||
loading?: boolean;
|
||||
disabled?: boolean;
|
||||
label?: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export function DeleteButton({
|
||||
onClick,
|
||||
loading = false,
|
||||
disabled = false,
|
||||
label = "Xóa",
|
||||
title = "Xác nhận xóa",
|
||||
description = "Bạn có chắc chắn muốn xóa các mục này không?",
|
||||
}: DeleteButtonProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [isConfirming, setIsConfirming] = useState(false);
|
||||
|
||||
const handleConfirm = async () => {
|
||||
setIsConfirming(true);
|
||||
try {
|
||||
await onClick();
|
||||
} finally {
|
||||
setIsConfirming(false);
|
||||
setOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={() => setOpen(true)}
|
||||
disabled={loading || disabled}
|
||||
className="gap-2"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
{label}
|
||||
</Button>
|
||||
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogDescription>{description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setOpen(false)}
|
||||
disabled={isConfirming}
|
||||
>
|
||||
Hủy
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={handleConfirm}
|
||||
disabled={isConfirming || loading}
|
||||
>
|
||||
{isConfirming ? "Đang xóa..." : "Xóa"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -17,6 +17,13 @@ export function UploadVersionForm({ onSubmit, closeDialog }: UploadVersionFormPr
|
|||
const [isUploading, setIsUploading] = useState(false);
|
||||
const [isDone, setIsDone] = useState(false);
|
||||
|
||||
// Match server allowed extensions
|
||||
const ALLOWED_EXTENSIONS = [".exe", ".apk", ".conf", ".json", ".xml", ".setting", ".lnk", ".url", ".seb"];
|
||||
const isFileValid = (file: File) => {
|
||||
const fileName = file.name.toLowerCase();
|
||||
return ALLOWED_EXTENSIONS.some((ext) => fileName.endsWith(ext));
|
||||
};
|
||||
|
||||
const form = useForm({
|
||||
defaultValues: { files: new DataTransfer().files, newVersion: "" },
|
||||
onSubmit: async ({ value }) => {
|
||||
|
|
@ -25,6 +32,15 @@ export function UploadVersionForm({ onSubmit, closeDialog }: UploadVersionFormPr
|
|||
return;
|
||||
}
|
||||
|
||||
// Validate file types
|
||||
const invalidFiles = Array.from(value.files).filter((f) => !isFileValid(f));
|
||||
if (invalidFiles.length > 0) {
|
||||
toast.error(
|
||||
`File không hợp lệ: ${invalidFiles.map((f) => f.name).join(", ")}. Chỉ chấp nhận ${ALLOWED_EXTENSIONS.join(", ")}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsUploading(true);
|
||||
setUploadPercent(0);
|
||||
|
|
@ -81,9 +97,13 @@ export function UploadVersionForm({ onSubmit, closeDialog }: UploadVersionFormPr
|
|||
<Label>File</Label>
|
||||
<Input
|
||||
type="file"
|
||||
accept=".exe,.apk,.conf,.json,.xml,.setting,.lnk,.url,.seb"
|
||||
onChange={(e) => e.target.files && field.handleChange(e.target.files)}
|
||||
disabled={isUploading || isDone}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Chỉ chấp nhận file: .exe, .apk, .conf, .json, .xml, .setting, .lnk, .url, .seb
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</form.Field>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,11 @@ interface RequestUpdateMenuProps {
|
|||
onUpdateRoom: () => void;
|
||||
onUpdateAll: () => void;
|
||||
loading?: boolean;
|
||||
label?: string;
|
||||
deviceLabel?: string;
|
||||
roomLabel?: string;
|
||||
allLabel?: string;
|
||||
icon?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function RequestUpdateMenu({
|
||||
|
|
@ -21,6 +26,11 @@ export function RequestUpdateMenu({
|
|||
onUpdateRoom,
|
||||
onUpdateAll,
|
||||
loading,
|
||||
label = "Cập nhật",
|
||||
deviceLabel = "Thiết bị cụ thể",
|
||||
roomLabel = "Theo phòng",
|
||||
allLabel = "Tất cả thiết bị",
|
||||
icon,
|
||||
}: RequestUpdateMenuProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
|
|
@ -58,11 +68,13 @@ export function RequestUpdateMenu({
|
|||
<div className="flex items-center gap-2">
|
||||
{loading ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin text-gray-600" />
|
||||
) : icon ? (
|
||||
<div className="h-4 w-4 text-gray-600">{icon}</div>
|
||||
) : (
|
||||
<RefreshCw className="h-4 w-4 text-gray-600 transition-transform duration-300 group-hover:rotate-180" />
|
||||
)}
|
||||
<span className="text-sm font-semibold">
|
||||
{loading ? "Đang gửi..." : "Cập nhật"}
|
||||
{loading ? "Đang gửi..." : label}
|
||||
</span>
|
||||
<ChevronDown className="h-4 w-4 text-gray-600 transition-transform duration-200 group-data-[state=open]:rotate-180" />
|
||||
</div>
|
||||
|
|
@ -73,17 +85,17 @@ export function RequestUpdateMenu({
|
|||
<DropdownMenuContent align="start" className="w-56">
|
||||
<DropdownMenuItem onClick={handleUpdateDevice} disabled={loading}>
|
||||
<RefreshCw className="h-4 w-4 mr-2" />
|
||||
<span>Cập nhật thiết bị cụ thể</span>
|
||||
<span>{deviceLabel}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={handleUpdateRoom} disabled={loading}>
|
||||
<RefreshCw className="h-4 w-4 mr-2" />
|
||||
<span>Cập nhật theo phòng</span>
|
||||
<span>{roomLabel}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={handleUpdateAll} disabled={loading}>
|
||||
<RefreshCw className="h-4 w-4 mr-2" />
|
||||
<span>Cập nhật tất cả thiết bị</span>
|
||||
<span>{allLabel}</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ interface VersionTableProps<TData> {
|
|||
data: TData[];
|
||||
columns: ColumnDef<TData, any>[];
|
||||
isLoading: boolean;
|
||||
onTableInit?: (table: any) => void; // <-- thêm
|
||||
onTableInit?: (table: any) => void;
|
||||
}
|
||||
|
||||
export function VersionTable<TData>({
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ export const API_ENDPOINTS = {
|
|||
ADD_BLACKLIST: `${BASE_URL}/AppVersion/blacklist/add`,
|
||||
DELETE_BLACKLIST: (appId: number) => `${BASE_URL}/AppVersion/blacklist/remove/${appId}`,
|
||||
UPDATE_BLACKLIST: (appId: string) => `${BASE_URL}/AppVersion/blacklist/update/${appId}`,
|
||||
DELETE_FILES: `${BASE_URL}/AppVersion/delete`,
|
||||
REQUEST_UPDATE_BLACKLIST: `${BASE_URL}/AppVersion/blacklist/request-update`,
|
||||
},
|
||||
DEVICE_COMM: {
|
||||
|
|
|
|||
|
|
@ -72,6 +72,17 @@ function AppsComponent() {
|
|||
},
|
||||
});
|
||||
|
||||
const deleteMutation = useMutationData<{ MsiFileIds: number[] }>({
|
||||
url: BASE_URL + API_ENDPOINTS.APP_VERSION.DELETE_FILES,
|
||||
method: "DELETE",
|
||||
invalidate: [["software-version"]],
|
||||
onSuccess: () => toast.success("Xóa phần mềm thành công!"),
|
||||
onError: (error) => {
|
||||
console.error("Delete error:", error);
|
||||
toast.error("Xóa phần mềm thất bại!");
|
||||
},
|
||||
});
|
||||
|
||||
// Cột bảng
|
||||
const columns: ColumnDef<Version>[] = [
|
||||
{ accessorKey: "version", header: "Phiên bản" },
|
||||
|
|
@ -171,6 +182,30 @@ function AppsComponent() {
|
|||
}
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!table) {
|
||||
toast.error("Không thể lấy thông tin bảng!");
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedRows = table.getSelectedRowModel().rows;
|
||||
if (selectedRows.length === 0) {
|
||||
toast.error("Vui lòng chọn ít nhất một file để xóa!");
|
||||
return;
|
||||
}
|
||||
|
||||
const MsiFileIds = selectedRows.map((row: any) => row.original.id);
|
||||
|
||||
try {
|
||||
await deleteMutation.mutateAsync({
|
||||
data: { MsiFileIds },
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Delete error:", e);
|
||||
toast.error("Có lỗi xảy ra khi xóa!");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AppManagerTemplate<Version>
|
||||
title="Quản lý phần mềm"
|
||||
|
|
@ -181,8 +216,10 @@ function AppsComponent() {
|
|||
onUpload={handleUpload}
|
||||
onUpdate={handleInstall}
|
||||
onDownload={handleDonwload}
|
||||
onDelete={handleDelete}
|
||||
updateLoading={installMutation.isPending}
|
||||
downloadLoading={downloadMutation.isPending}
|
||||
deleteLoading={deleteMutation.isPending}
|
||||
onTableInit={setTable}
|
||||
rooms={roomData}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -7,10 +7,11 @@ import {
|
|||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { FileText, Building2 } from "lucide-react";
|
||||
import { FileText, Building2, Download } from "lucide-react";
|
||||
import { FormDialog } from "@/components/dialogs/form-dialog";
|
||||
import { VersionTable } from "@/components/tables/version-table";
|
||||
import { RequestUpdateMenu } from "@/components/menu/request-update-menu";
|
||||
import { DeleteButton } from "@/components/buttons/delete-button";
|
||||
import type { AxiosProgressEvent } from "axios";
|
||||
import { useState } from "react";
|
||||
import { SelectDialog } from "@/components/dialogs/select-dialog";
|
||||
|
|
@ -34,6 +35,8 @@ interface AppManagerTemplateProps<TData> {
|
|||
updateLoading?: boolean;
|
||||
onDownload?: (targetNames: string[]) => Promise<void> | void;
|
||||
downloadLoading?: boolean;
|
||||
onDelete?: () => Promise<void> | void;
|
||||
deleteLoading?: boolean;
|
||||
onTableInit?: (table: any) => void;
|
||||
rooms?: Room[];
|
||||
devices?: string[];
|
||||
|
|
@ -50,6 +53,8 @@ export function AppManagerTemplate<TData>({
|
|||
updateLoading,
|
||||
onDownload,
|
||||
downloadLoading,
|
||||
onDelete,
|
||||
deleteLoading,
|
||||
onTableInit,
|
||||
rooms = [],
|
||||
devices = [],
|
||||
|
|
@ -133,13 +138,18 @@ export function AppManagerTemplate<TData>({
|
|||
/>
|
||||
</CardContent>
|
||||
|
||||
{onUpdate && (
|
||||
<CardFooter className="gap-2">
|
||||
{(onUpdate || onDelete) && (
|
||||
<CardFooter className="flex items-center justify-between gap-4">
|
||||
<div className="flex gap-2">
|
||||
<RequestUpdateMenu
|
||||
onUpdateDevice={openDeviceDialog}
|
||||
onUpdateRoom={openRoomDialog}
|
||||
onUpdateAll={handleUpdateAll}
|
||||
loading={updateLoading}
|
||||
label="Cài đặt"
|
||||
deviceLabel="Cài đặt thiết bị cụ thể"
|
||||
roomLabel="Cài đặt theo phòng"
|
||||
allLabel="Cài đặt tất cả thiết bị"
|
||||
/>
|
||||
{onDownload && (
|
||||
<RequestUpdateMenu
|
||||
|
|
@ -154,6 +164,19 @@ export function AppManagerTemplate<TData>({
|
|||
onDownload(allTargets);
|
||||
}}
|
||||
loading={downloadLoading}
|
||||
label="Tải xuống"
|
||||
deviceLabel="Tải xuống thiết bị cụ thể"
|
||||
roomLabel="Tải xuống theo phòng"
|
||||
allLabel="Tải xuống tất cả thiết bị"
|
||||
icon={<Download className="h-4 w-4" />}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{onDelete && (
|
||||
<DeleteButton
|
||||
onClick={onDelete}
|
||||
loading={deleteLoading}
|
||||
disabled={false}
|
||||
/>
|
||||
)}
|
||||
</CardFooter>
|
||||
|
|
@ -167,6 +190,7 @@ export function AppManagerTemplate<TData>({
|
|||
onClose={() => {
|
||||
setDialogOpen(false);
|
||||
setDialogType(null);
|
||||
setTimeout(() => window.location.reload(), 500);
|
||||
}}
|
||||
title="Chọn phòng"
|
||||
description="Chọn các phòng cần cập nhật"
|
||||
|
|
@ -194,6 +218,7 @@ export function AppManagerTemplate<TData>({
|
|||
onClose={() => {
|
||||
setDialogOpen(false);
|
||||
setDialogType(null);
|
||||
setTimeout(() => window.location.reload(), 500);
|
||||
}}
|
||||
rooms={rooms}
|
||||
fetchDevices={fetchDevicesFromRoom}
|
||||
|
|
@ -223,6 +248,7 @@ export function AppManagerTemplate<TData>({
|
|||
onClose={() => {
|
||||
setDialogOpen(false);
|
||||
setDialogType(null);
|
||||
setTimeout(() => window.location.reload(), 500);
|
||||
}}
|
||||
title="Chọn phòng"
|
||||
description="Chọn các phòng để tải file xuống"
|
||||
|
|
@ -250,6 +276,7 @@ export function AppManagerTemplate<TData>({
|
|||
onClose={() => {
|
||||
setDialogOpen(false);
|
||||
setDialogType(null);
|
||||
setTimeout(() => window.location.reload(), 500);
|
||||
}}
|
||||
rooms={rooms}
|
||||
fetchDevices={fetchDevicesFromRoom}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user