You've already forked DataMate
feat: Enhance DatasetDetail component with delete functionality and improved download handling
feat: Add automatic data refresh and improved user feedback in DatasetManagementPage fix: Update dataset API to streamline download functionality and improve error handling
This commit is contained in:
115
frontend/src/components/ActionDropdown.tsx
Normal file
115
frontend/src/components/ActionDropdown.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import { Dropdown, Popconfirm, Button, Space } from "antd";
|
||||
import { EllipsisOutlined } from "@ant-design/icons";
|
||||
import { useState } from "react";
|
||||
|
||||
interface ActionItem {
|
||||
key: string;
|
||||
label: string;
|
||||
icon?: React.ReactNode;
|
||||
danger?: boolean;
|
||||
confirm?: {
|
||||
title: string;
|
||||
description?: string;
|
||||
okText?: string;
|
||||
cancelText?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface ActionDropdownProps {
|
||||
actions?: ActionItem[];
|
||||
onAction?: (key: string, action: ActionItem) => void;
|
||||
placement?:
|
||||
| "bottomRight"
|
||||
| "topLeft"
|
||||
| "topCenter"
|
||||
| "topRight"
|
||||
| "bottomLeft"
|
||||
| "bottomCenter"
|
||||
| "top"
|
||||
| "bottom";
|
||||
}
|
||||
|
||||
const ActionDropdown = ({
|
||||
actions = [],
|
||||
onAction,
|
||||
placement = "bottomRight",
|
||||
}: ActionDropdownProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const handleActionClick = (action: ActionItem) => {
|
||||
if (action.confirm) {
|
||||
// 如果有确认框,不立即执行,等待确认
|
||||
return;
|
||||
}
|
||||
// 执行操作
|
||||
onAction?.(action.key, action);
|
||||
// 如果没有确认框,则立即关闭 Dropdown
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const dropdownContent = (
|
||||
<div className="bg-white p-2 rounded shadow-md">
|
||||
<Space direction="vertical" className="w-full">
|
||||
{actions.map((action) => {
|
||||
if (action.confirm) {
|
||||
return (
|
||||
<Popconfirm
|
||||
key={action.key}
|
||||
title={action.confirm.title}
|
||||
description={action.confirm.description}
|
||||
onConfirm={() => {
|
||||
onAction?.(action.key, action);
|
||||
setOpen(false);
|
||||
}}
|
||||
okText={action.confirm.okText || "确定"}
|
||||
cancelText={action.confirm.cancelText || "取消"}
|
||||
okType={action.danger ? "danger" : "primary"}
|
||||
styles={{ root: { zIndex: 9999 } }}
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
className="w-full text-left"
|
||||
danger={action.danger}
|
||||
icon={action.icon}
|
||||
>
|
||||
{action.label}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
key={action.key}
|
||||
className="w-full"
|
||||
size="small"
|
||||
type="text"
|
||||
danger={action.danger}
|
||||
icon={action.icon}
|
||||
onClick={() => handleActionClick(action)}
|
||||
>
|
||||
{action.label}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
overlay={dropdownContent}
|
||||
trigger={["click"]}
|
||||
placement={placement}
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<EllipsisOutlined style={{ fontSize: 24 }} />}
|
||||
/>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActionDropdown;
|
||||
@@ -64,6 +64,7 @@ export default function AddTagPopover({
|
||||
open={showPopover}
|
||||
trigger="click"
|
||||
placement="bottom"
|
||||
onOpenChange={setShowPopover}
|
||||
content={
|
||||
<div className="space-y-4 w-[300px]">
|
||||
<h4 className="font-medium border-b pb-2 border-gray-100">
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { Tag, Pagination, Dropdown, Tooltip, Empty, Popover } from "antd";
|
||||
import {
|
||||
EllipsisOutlined,
|
||||
ClockCircleOutlined,
|
||||
StarFilled,
|
||||
} from "@ant-design/icons";
|
||||
Tag,
|
||||
Pagination,
|
||||
Tooltip,
|
||||
Empty,
|
||||
Popover,
|
||||
Menu,
|
||||
Popconfirm,
|
||||
} from "antd";
|
||||
import { ClockCircleOutlined, StarFilled } from "@ant-design/icons";
|
||||
import type { ItemType } from "antd/es/menu/interface";
|
||||
import { formatDateTime } from "@/utils/unit";
|
||||
import ActionDropdown from "./ActionDropdown";
|
||||
|
||||
interface BaseCardDataType {
|
||||
id: string | number;
|
||||
@@ -168,6 +173,48 @@ function CardView<T extends BaseCardDataType>(props: CardViewProps<T>) {
|
||||
|
||||
const ops = (item) =>
|
||||
typeof operations === "function" ? operations(item) : operations;
|
||||
|
||||
const menu = (item) => {
|
||||
const ops =
|
||||
typeof operations === "function" ? operations(item) : operations;
|
||||
<Menu>
|
||||
{ops.map((op) => {
|
||||
if (op?.danger) {
|
||||
return (
|
||||
<Menu.Item key={op?.key} disabled icon={op?.icon}>
|
||||
<Popconfirm
|
||||
title="确定删除吗?"
|
||||
description="此操作不可撤销"
|
||||
onConfirm={op.onClick ? () => op.onClick(item) : undefined}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
// 阻止事件冒泡,避免 Dropdown 关闭
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "block",
|
||||
width: "100%",
|
||||
color: "inherit",
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{op.icon}
|
||||
{op.label}
|
||||
</div>
|
||||
</Popconfirm>
|
||||
</Menu.Item>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Menu.Item key={op?.key} onClick={op?.onClick} icon={op?.icon}>
|
||||
{op?.label}
|
||||
</Menu.Item>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</Menu>;
|
||||
};
|
||||
return (
|
||||
<div className="flex-overflow-hidden">
|
||||
<div className="overflow-auto grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 gap-4">
|
||||
@@ -261,24 +308,15 @@ function CardView<T extends BaseCardDataType>(props: CardViewProps<T>) {
|
||||
</div>
|
||||
</div>
|
||||
{operations && (
|
||||
<Dropdown
|
||||
trigger={["click"]}
|
||||
menu={{
|
||||
items: ops(item),
|
||||
onClick: ({ key }) => {
|
||||
const operation = ops(item).find(
|
||||
(op) => op.key === key
|
||||
);
|
||||
if (operation?.onClick) {
|
||||
operation.onClick(item);
|
||||
}
|
||||
},
|
||||
<ActionDropdown
|
||||
actions={ops(item)}
|
||||
onAction={(key) => {
|
||||
const operation = ops(item).find((op) => op.key === key);
|
||||
if (operation?.onClick) {
|
||||
operation.onClick(item);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="cursor-pointer">
|
||||
<EllipsisOutlined style={{ fontSize: 24 }} />
|
||||
</div>
|
||||
</Dropdown>
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import React from "react";
|
||||
import { Database } from "lucide-react";
|
||||
import { Card, Dropdown, Button, Tag, Tooltip } from "antd";
|
||||
import { Card, Button, Tag, Tooltip, Popconfirm } from "antd";
|
||||
import type { ItemType } from "antd/es/menu/interface";
|
||||
import AddTagPopover from "./AddTagPopover";
|
||||
import ActionDropdown from "./ActionDropdown";
|
||||
|
||||
interface StatisticItem {
|
||||
icon: React.ReactNode;
|
||||
@@ -100,22 +101,39 @@ function DetailHeader<T>({
|
||||
{operations.map((op) => {
|
||||
if (op.isDropdown) {
|
||||
return (
|
||||
<Dropdown
|
||||
key={op.key}
|
||||
menu={{
|
||||
items: op?.items as ItemType[],
|
||||
onClick: op?.onMenuClick,
|
||||
}}
|
||||
>
|
||||
<Tooltip title={op.label}>
|
||||
<Button icon={op.icon} />
|
||||
</Tooltip>
|
||||
</Dropdown>
|
||||
<ActionDropdown
|
||||
actions={op?.items}
|
||||
onAction={op?.onMenuClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (op.confirm) {
|
||||
return (
|
||||
<Tooltip key={op.key} title={op.label}>
|
||||
<Popconfirm
|
||||
key={op.key}
|
||||
title={op.confirm.title}
|
||||
description={op.confirm.description}
|
||||
onConfirm={() => {
|
||||
op?.onClick();
|
||||
}}
|
||||
okText={op.confirm.okText || "确定"}
|
||||
cancelText={op.confirm.cancelText || "取消"}
|
||||
okType={op.danger ? "danger" : "primary"}
|
||||
overlayStyle={{ zIndex: 9999 }}
|
||||
>
|
||||
<Button icon={op.icon} danger={op.danger} />
|
||||
</Popconfirm>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Tooltip key={op.key} title={op.label}>
|
||||
<Button {...op} />
|
||||
<Button
|
||||
icon={op.icon}
|
||||
danger={op.danger}
|
||||
onClick={op.onClick}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
|
||||
Reference in New Issue
Block a user