New Interface (initial commit)
This commit is contained in:
@@ -1,44 +1,41 @@
|
||||
import { alpha, Box, styled } from "@mui/material";
|
||||
import * as React from "react";
|
||||
import { cn } from "@root/lib/utils"; // Утилита для объединения классов
|
||||
|
||||
export const TestBox = styled(Box)(({ theme, "aria-selected": selected }) => {
|
||||
const { mode, primary, text } = theme.palette;
|
||||
const key = `${mode}-${!!selected}`;
|
||||
// Определяем пропсы для нашего компонента.
|
||||
// Он принимает все стандартные атрибуты для div, а также `selected`.
|
||||
export interface TestBoxProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
selected?: boolean;
|
||||
}
|
||||
|
||||
const backgroundColor =
|
||||
mode === "light" ? alpha(primary.main, 0.05) : alpha(primary.main, 0.08);
|
||||
export const TestBox = React.forwardRef<HTMLDivElement, TestBoxProps>(
|
||||
({ className, selected, children, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
// Устанавливаем data-атрибут в зависимости от пропса `selected`
|
||||
data-selected={selected}
|
||||
// Объединяем классы для создания сложной стилизации
|
||||
className={cn(
|
||||
// --- Базовые стили ---
|
||||
"relative w-full cursor-pointer rounded-lg p-4 shadow-sm transition-all duration-200",
|
||||
|
||||
const color = {
|
||||
"light-true": text.secondary,
|
||||
"light-false": text.secondary,
|
||||
"dark-true": alpha(text.secondary, 0.65),
|
||||
"dark-false": alpha(text.secondary, 0.65),
|
||||
}[key]!;
|
||||
// --- Стили по умолчанию (не выбран) ---
|
||||
"bg-primary/5 text-muted-foreground",
|
||||
"hover:bg-primary/10 hover:shadow-md",
|
||||
|
||||
const h2color = {
|
||||
"light-true": primary.main,
|
||||
"light-false": text.primary,
|
||||
"dark-true": primary.main,
|
||||
"dark-false": text.primary,
|
||||
}[key]!;
|
||||
// --- Стили для ВЫБРАННОГО состояния ---
|
||||
// Используем data-атрибут для стилизации
|
||||
"data-[selected=true]:bg-primary/20 data-[selected=true]:text-primary data-[selected=true]:shadow-lg",
|
||||
|
||||
return {
|
||||
position: "relative",
|
||||
width: "100%",
|
||||
display: "block",
|
||||
cursor: "pointer",
|
||||
textAlign: "left",
|
||||
borderRadius: 8,
|
||||
boxShadow: theme.shadows[1],
|
||||
padding: "8px 16px",
|
||||
boxSizing: "border-box",
|
||||
backgroundColor,
|
||||
color,
|
||||
"& h2": { color: h2color },
|
||||
transition: "background-color 0.3s, box-shadow 0.3s",
|
||||
"&:hover": {
|
||||
backgroundColor:
|
||||
mode === "light" ? alpha(primary.main, 0.1) : alpha(primary.main, 0.15),
|
||||
boxShadow: theme.shadows[2],
|
||||
},
|
||||
};
|
||||
});
|
||||
// --- Дополнительные классы от пользователя ---
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
TestBox.displayName = "TestBox";
|
||||
|
||||
@@ -3,16 +3,28 @@ import { useLockFn } from "ahooks";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSortable } from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import { Box, Divider, MenuItem, Menu, styled, alpha } from "@mui/material";
|
||||
import { BaseLoading } from "@/components/base";
|
||||
import { LanguageRounded } from "@mui/icons-material";
|
||||
import { showNotice } from "@/services/noticeService";
|
||||
import { TestBox } from "./test-box";
|
||||
import delayManager from "@/services/delay";
|
||||
import { cmdTestDelay, downloadIconCache } from "@/services/cmds";
|
||||
import { UnlistenFn } from "@tauri-apps/api/event";
|
||||
import { convertFileSrc } from "@tauri-apps/api/core";
|
||||
|
||||
import { useListen } from "@/hooks/use-listen";
|
||||
import { showNotice } from "@/services/noticeService";
|
||||
import delayManager from "@/services/delay";
|
||||
import { cmdTestDelay, downloadIconCache } from "@/services/cmds";
|
||||
|
||||
// Новые импорты
|
||||
import { BaseLoading } from "@/components/base";
|
||||
import { TestBox } from "./test-box"; // Наш рефакторенный компонент
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger } from "@/components/ui/context-menu";
|
||||
import { Languages } from "lucide-react"; // Новая иконка
|
||||
|
||||
// Вспомогательная функция для цвета задержки
|
||||
const getDelayColorClass = (delay: number): string => {
|
||||
if (delay < 0 || delay >= 10000) return "text-destructive";
|
||||
if (delay >= 500) return "text-destructive";
|
||||
if (delay >= 200) return "text-yellow-500";
|
||||
return "text-green-500";
|
||||
};
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
@@ -23,34 +35,21 @@ interface Props {
|
||||
|
||||
export const TestItem = (props: Props) => {
|
||||
const { itemData, onEdit, onDelete: onDeleteItem } = props;
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({
|
||||
id: props.id,
|
||||
});
|
||||
|
||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: props.id });
|
||||
const { t } = useTranslation();
|
||||
const [anchorEl, setAnchorEl] = useState<any>(null);
|
||||
const [position, setPosition] = useState({ left: 0, top: 0 });
|
||||
|
||||
const [delay, setDelay] = useState(-1);
|
||||
const { uid, name, icon, url } = itemData;
|
||||
const [iconCachePath, setIconCachePath] = useState("");
|
||||
const { addListener } = useListen();
|
||||
|
||||
const onDelay = async () => {
|
||||
setDelay(-2);
|
||||
const onDelay = useLockFn(async () => {
|
||||
setDelay(-2); // Состояние загрузки
|
||||
const result = await cmdTestDelay(url);
|
||||
setDelay(result);
|
||||
};
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
initIconCachePath();
|
||||
}, [icon]);
|
||||
const getFileName = (url: string) => url.substring(url.lastIndexOf("/") + 1);
|
||||
|
||||
async function initIconCachePath() {
|
||||
if (icon && icon.trim().startsWith("http")) {
|
||||
@@ -60,17 +59,9 @@ export const TestItem = (props: Props) => {
|
||||
}
|
||||
}
|
||||
|
||||
function getFileName(url: string) {
|
||||
return url.substring(url.lastIndexOf("/") + 1);
|
||||
}
|
||||
|
||||
const onEditTest = () => {
|
||||
setAnchorEl(null);
|
||||
onEdit();
|
||||
};
|
||||
useEffect(() => { initIconCachePath(); }, [icon]);
|
||||
|
||||
const onDelete = useLockFn(async () => {
|
||||
setAnchorEl(null);
|
||||
try {
|
||||
onDeleteItem(uid);
|
||||
} catch (err: any) {
|
||||
@@ -79,167 +70,73 @@ export const TestItem = (props: Props) => {
|
||||
});
|
||||
|
||||
const menu = [
|
||||
{ label: "Edit", handler: onEditTest },
|
||||
{ label: "Delete", handler: onDelete },
|
||||
{ label: "Edit", handler: onEdit },
|
||||
{ label: "Delete", handler: onDelete, isDestructive: true },
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
let unlistenFn: UnlistenFn | null = null;
|
||||
|
||||
const setupListener = async () => {
|
||||
if (unlistenFn) {
|
||||
unlistenFn();
|
||||
}
|
||||
unlistenFn = await addListener("verge://test-all", () => {
|
||||
onDelay();
|
||||
});
|
||||
if (unlistenFn) unlistenFn();
|
||||
unlistenFn = await addListener("verge://test-all", onDelay);
|
||||
};
|
||||
|
||||
setupListener();
|
||||
return () => { unlistenFn?.(); };
|
||||
}, [url, addListener, onDelay]);
|
||||
|
||||
return () => {
|
||||
if (unlistenFn) {
|
||||
console.log(
|
||||
`TestItem for ${props.id} unmounting or url changed, cleaning up test-all listener.`,
|
||||
);
|
||||
unlistenFn();
|
||||
}
|
||||
};
|
||||
}, [url, addListener, onDelay, props.id]);
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
zIndex: isDragging ? 100 : undefined,
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
position: "relative",
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
zIndex: isDragging ? "calc(infinity)" : undefined,
|
||||
}}
|
||||
>
|
||||
<TestBox
|
||||
onContextMenu={(event) => {
|
||||
const { clientX, clientY } = event;
|
||||
setPosition({ top: clientY, left: clientX });
|
||||
setAnchorEl(event.currentTarget);
|
||||
event.preventDefault();
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
position="relative"
|
||||
sx={{ cursor: "move" }}
|
||||
ref={setNodeRef}
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
>
|
||||
{icon && icon.trim() !== "" ? (
|
||||
<Box sx={{ display: "flex", justifyContent: "center" }}>
|
||||
{icon.trim().startsWith("http") && (
|
||||
<img
|
||||
src={iconCachePath === "" ? icon : iconCachePath}
|
||||
height="40px"
|
||||
/>
|
||||
<div style={style} ref={setNodeRef} {...attributes}>
|
||||
<ContextMenu>
|
||||
<ContextMenuTrigger>
|
||||
<TestBox>
|
||||
{/* Мы применяем `listeners` к иконке, чтобы за нее можно было таскать */}
|
||||
<div {...listeners} className="flex h-12 cursor-move items-center justify-center">
|
||||
{icon ? (
|
||||
<img
|
||||
src={icon.startsWith('data') ? icon : icon.startsWith('<svg') ? `data:image/svg+xml;base64,${btoa(icon)}` : (iconCachePath || icon)}
|
||||
className="h-10"
|
||||
alt={name}
|
||||
/>
|
||||
) : (
|
||||
<Languages className="h-10 w-10 text-muted-foreground" />
|
||||
)}
|
||||
{icon.trim().startsWith("data") && (
|
||||
<img src={icon} height="40px" />
|
||||
)}
|
||||
{icon.trim().startsWith("<svg") && (
|
||||
<img
|
||||
src={`data:image/svg+xml;base64,${btoa(icon)}`}
|
||||
height="40px"
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
) : (
|
||||
<Box sx={{ display: "flex", justifyContent: "center" }}>
|
||||
<LanguageRounded sx={{ height: "40px" }} fontSize="large" />
|
||||
</Box>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Box sx={{ display: "flex", justifyContent: "center" }}>{name}</Box>
|
||||
</Box>
|
||||
<Divider sx={{ marginTop: "8px" }} />
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
marginTop: "8px",
|
||||
color: "primary.main",
|
||||
}}
|
||||
>
|
||||
{delay === -2 && (
|
||||
<Widget>
|
||||
<BaseLoading />
|
||||
</Widget>
|
||||
)}
|
||||
<p className="mt-1 text-center text-sm font-semibold truncate" title={name}>{name}</p>
|
||||
|
||||
{delay === -1 && (
|
||||
<Widget
|
||||
className="the-check"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onDelay();
|
||||
}}
|
||||
sx={({ palette }) => ({
|
||||
":hover": { bgcolor: alpha(palette.primary.main, 0.15) },
|
||||
})}
|
||||
<Separator className="my-2" />
|
||||
|
||||
<div
|
||||
className="flex h-6 items-center justify-center text-sm font-medium"
|
||||
onClick={(e) => { e.stopPropagation(); onDelay(); }}
|
||||
>
|
||||
{t("Test")}
|
||||
</Widget>
|
||||
)}
|
||||
{delay === -2 ? (
|
||||
<BaseLoading className="h-4 w-4" />
|
||||
) : delay === -1 ? (
|
||||
<span className="cursor-pointer rounded-md px-2 py-0.5 hover:bg-accent">{t("Test")}</span>
|
||||
) : (
|
||||
<span className={`cursor-pointer rounded-md px-2 py-0.5 hover:bg-accent ${getDelayColorClass(delay)}`}>
|
||||
{delayManager.formatDelay(delay)} ms
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</TestBox>
|
||||
</ContextMenuTrigger>
|
||||
|
||||
{delay >= 0 && (
|
||||
// 显示延迟
|
||||
<Widget
|
||||
className="the-delay"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onDelay();
|
||||
}}
|
||||
color={delayManager.formatDelayColor(delay)}
|
||||
sx={({ palette }) => ({
|
||||
":hover": {
|
||||
bgcolor: alpha(palette.primary.main, 0.15),
|
||||
},
|
||||
})}
|
||||
>
|
||||
{delayManager.formatDelay(delay)}
|
||||
</Widget>
|
||||
)}
|
||||
</Box>
|
||||
</TestBox>
|
||||
|
||||
<Menu
|
||||
open={!!anchorEl}
|
||||
anchorEl={anchorEl}
|
||||
onClose={() => setAnchorEl(null)}
|
||||
anchorPosition={position}
|
||||
anchorReference="anchorPosition"
|
||||
transitionDuration={225}
|
||||
MenuListProps={{ sx: { py: 0.5 } }}
|
||||
onContextMenu={(e) => {
|
||||
setAnchorEl(null);
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
{menu.map((item) => (
|
||||
<MenuItem
|
||||
key={item.label}
|
||||
onClick={item.handler}
|
||||
sx={{ minWidth: 120 }}
|
||||
dense
|
||||
>
|
||||
{t(item.label)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
</Box>
|
||||
<ContextMenuContent>
|
||||
{menu.map((item) => (
|
||||
<ContextMenuItem key={item.label} onClick={item.handler} className={item.isDestructive ? "text-destructive" : ""}>
|
||||
{t(item.label)}
|
||||
</ContextMenuItem>
|
||||
))}
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const Widget = styled(Box)(({ theme: { typography } }) => ({
|
||||
padding: "3px 6px",
|
||||
fontSize: 14,
|
||||
fontFamily: typography.fontFamily,
|
||||
borderRadius: "4px",
|
||||
}));
|
||||
|
||||
@@ -1,13 +1,33 @@
|
||||
import { forwardRef, useImperativeHandle, useState } from "react";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useForm, Controller } from "react-hook-form";
|
||||
import { TextField } from "@mui/material";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { BaseDialog } from "@/components/base";
|
||||
import { nanoid } from "nanoid";
|
||||
import { showNotice } from "@/services/noticeService";
|
||||
|
||||
// Новые импорты из shadcn/ui и lucide-react
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
DialogClose,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Loader2 } from "lucide-react";
|
||||
|
||||
interface Props {
|
||||
onChange: (uid: string, patch?: Partial<IVergeTestItem>) => void;
|
||||
}
|
||||
@@ -17,7 +37,6 @@ export interface TestViewerRef {
|
||||
edit: (item: IVergeTestItem) => void;
|
||||
}
|
||||
|
||||
// create or edit the test item
|
||||
export const TestViewer = forwardRef<TestViewerRef, Props>((props, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
@@ -25,146 +44,126 @@ export const TestViewer = forwardRef<TestViewerRef, Props>((props, ref) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { verge, patchVerge } = useVerge();
|
||||
const testList = verge?.test_list ?? [];
|
||||
const { control, watch, register, ...formIns } = useForm<IVergeTestItem>({
|
||||
defaultValues: {
|
||||
name: "",
|
||||
icon: "",
|
||||
url: "",
|
||||
},
|
||||
|
||||
const form = useForm<IVergeTestItem>({
|
||||
defaultValues: { name: "", icon: "", url: "" },
|
||||
});
|
||||
const { control, handleSubmit, reset, setValue } = form;
|
||||
|
||||
const patchTestList = async (uid: string, patch: Partial<IVergeTestItem>) => {
|
||||
const newList = testList.map((x) => {
|
||||
if (x.uid === uid) {
|
||||
return { ...x, ...patch };
|
||||
}
|
||||
return x;
|
||||
});
|
||||
const newList = testList.map((x) => (x.uid === uid ? { ...x, ...patch } : x));
|
||||
await patchVerge({ test_list: newList });
|
||||
};
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
create: () => {
|
||||
reset({ name: "", icon: "", url: "" });
|
||||
setOpenType("new");
|
||||
setOpen(true);
|
||||
},
|
||||
edit: (item) => {
|
||||
if (item) {
|
||||
Object.entries(item).forEach(([key, value]) => {
|
||||
formIns.setValue(key as any, value);
|
||||
});
|
||||
}
|
||||
reset(item);
|
||||
setOpenType("edit");
|
||||
setOpen(true);
|
||||
},
|
||||
}));
|
||||
|
||||
const handleOk = useLockFn(
|
||||
formIns.handleSubmit(async (form) => {
|
||||
handleSubmit(async (formData) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
if (!form.name) throw new Error("`Name` should not be null");
|
||||
if (!form.url) throw new Error("`Url` should not be null");
|
||||
if (!formData.name) throw new Error("`Name` should not be null");
|
||||
if (!formData.url) throw new Error("`Url` should not be null");
|
||||
|
||||
let newList;
|
||||
let uid;
|
||||
if (formData.icon && formData.icon.startsWith("<svg")) {
|
||||
// --- ИСПРАВЛЕНИЕ ЗДЕСЬ ---
|
||||
// Удаляем комментарии из SVG, используя правильное регулярное выражение
|
||||
formData.icon = formData.icon.replace(/<!--[\s\S]*?-->/g, "");
|
||||
// --- КОНЕЦ ИСПРАВЛЕНИЯ ---
|
||||
|
||||
if (form.icon && form.icon.startsWith("<svg")) {
|
||||
// 移除 icon 中的注释
|
||||
if (form.icon) {
|
||||
form.icon = form.icon.replace(/<!--[\s\S]*?-->/g, "");
|
||||
}
|
||||
const doc = new DOMParser().parseFromString(
|
||||
form.icon,
|
||||
"image/svg+xml",
|
||||
);
|
||||
const doc = new DOMParser().parseFromString(formData.icon, "image/svg+xml");
|
||||
if (doc.querySelector("parsererror")) {
|
||||
throw new Error("`Icon`svg format error");
|
||||
}
|
||||
}
|
||||
|
||||
if (openType === "new") {
|
||||
uid = nanoid();
|
||||
const item = { ...form, uid };
|
||||
newList = [...testList, item];
|
||||
const uid = nanoid();
|
||||
const item = { ...formData, uid };
|
||||
const newList = [...testList, item];
|
||||
await patchVerge({ test_list: newList });
|
||||
props.onChange(uid);
|
||||
} else {
|
||||
if (!form.uid) throw new Error("UID not found");
|
||||
uid = form.uid;
|
||||
|
||||
await patchTestList(uid, form);
|
||||
props.onChange(uid, form);
|
||||
if (!formData.uid) throw new Error("UID not found");
|
||||
await patchTestList(formData.uid, formData);
|
||||
props.onChange(formData.uid, formData);
|
||||
}
|
||||
|
||||
setOpen(false);
|
||||
setLoading(false);
|
||||
setTimeout(() => formIns.reset(), 500);
|
||||
} catch (err: any) {
|
||||
showNotice("error", err.message || err.toString());
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
const handleClose = () => {
|
||||
setOpen(false);
|
||||
setTimeout(() => formIns.reset(), 500);
|
||||
};
|
||||
|
||||
const text = {
|
||||
fullWidth: true,
|
||||
size: "small",
|
||||
margin: "normal",
|
||||
variant: "outlined",
|
||||
autoComplete: "off",
|
||||
autoCorrect: "off",
|
||||
} as const;
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
open={open}
|
||||
title={openType === "new" ? t("Create Test") : t("Edit Test")}
|
||||
contentSx={{ width: 375, pb: 0, maxHeight: "80%" }}
|
||||
okBtn={t("Save")}
|
||||
cancelBtn={t("Cancel")}
|
||||
onClose={handleClose}
|
||||
onCancel={handleClose}
|
||||
onOk={handleOk}
|
||||
loading={loading}
|
||||
>
|
||||
<Controller
|
||||
name="name"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextField {...text} {...field} label={t("Name")} />
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="icon"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...text}
|
||||
{...field}
|
||||
multiline
|
||||
maxRows={5}
|
||||
label={t("Icon")}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="url"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...text}
|
||||
{...field}
|
||||
multiline
|
||||
maxRows={3}
|
||||
label={t("Test URL")}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</BaseDialog>
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{openType === "new" ? t("Create Test") : t("Edit Test")}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<Form {...form}>
|
||||
<form onSubmit={handleOk} className="space-y-4">
|
||||
<FormField
|
||||
control={control}
|
||||
name="name"
|
||||
rules={{ required: true }}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("Name")}</FormLabel>
|
||||
<FormControl><Input {...field} /></FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={control}
|
||||
name="icon"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("Icon")}</FormLabel>
|
||||
<FormControl><Textarea {...field} rows={4} placeholder="<svg>...</svg> or http(s)://..." /></FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={control}
|
||||
name="url"
|
||||
rules={{ required: true }}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("Test URL")}</FormLabel>
|
||||
<FormControl><Textarea {...field} rows={3} placeholder="https://www.google.com" /></FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<button type="submit" className="hidden" />
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
<DialogFooter>
|
||||
<DialogClose asChild><Button type="button" variant="outline">{t("Cancel")}</Button></DialogClose>
|
||||
<Button type="button" onClick={handleOk} disabled={loading}>
|
||||
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
{t("Save")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user