refactor: profile components

This commit is contained in:
Slinetrac
2025-10-14 23:45:12 +08:00
parent 5d114806f7
commit 4f2633a62b
11 changed files with 382 additions and 272 deletions

View File

@@ -20,7 +20,7 @@ import metaSchema from "meta-json-schema/schemas/meta-json-schema.json";
import * as monaco from "monaco-editor"; import * as monaco from "monaco-editor";
import { configureMonacoYaml } from "monaco-yaml"; import { configureMonacoYaml } from "monaco-yaml";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { ReactNode, useEffect, useRef, useState } from "react"; import { ReactNode, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import MonacoEditor from "react-monaco-editor"; import MonacoEditor from "react-monaco-editor";
import pac from "types-pac/pac.d.ts?raw"; import pac from "types-pac/pac.d.ts?raw";
@@ -63,13 +63,13 @@ const monacoInitialization = () => {
{ {
uri: "http://example.com/meta-json-schema.json", uri: "http://example.com/meta-json-schema.json",
fileMatch: ["**/*.clash.yaml"], fileMatch: ["**/*.clash.yaml"],
// @ts-ignore // @ts-expect-error -- meta schema JSON import does not satisfy JSONSchema7 at compile time
schema: metaSchema as JSONSchema7, schema: metaSchema as JSONSchema7,
}, },
{ {
uri: "http://example.com/clash-verge-merge-json-schema.json", uri: "http://example.com/clash-verge-merge-json-schema.json",
fileMatch: ["**/*.merge.yaml"], fileMatch: ["**/*.merge.yaml"],
// @ts-ignore // @ts-expect-error -- merge schema JSON import does not satisfy JSONSchema7 at compile time
schema: mergeSchema as JSONSchema7, schema: mergeSchema as JSONSchema7,
}, },
], ],
@@ -87,8 +87,8 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
const { const {
open = false, open = false,
title = t("Edit File"), title,
initialData = Promise.resolve(""), initialData,
readOnly = false, readOnly = false,
language = "yaml", language = "yaml",
schema, schema,
@@ -97,6 +97,12 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
onClose, onClose,
} = props; } = props;
const resolvedTitle = title ?? t("Edit File");
const resolvedInitialData = useMemo(
() => initialData ?? Promise.resolve(""),
[initialData],
);
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor>(undefined); const editorRef = useRef<monaco.editor.IStandaloneCodeEditor>(undefined);
const prevData = useRef<string | undefined>(""); const prevData = useRef<string | undefined>("");
const currData = useRef<string | undefined>(""); const currData = useRef<string | undefined>("");
@@ -111,7 +117,7 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
editorRef.current = editor; editorRef.current = editor;
// retrieve initial data // retrieve initial data
await initialData.then((data) => { await resolvedInitialData.then((data) => {
prevData.current = data; prevData.current = data;
currData.current = data; currData.current = data;
@@ -133,7 +139,9 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
const handleSave = useLockFn(async () => { const handleSave = useLockFn(async () => {
try { try {
!readOnly && onSave?.(prevData.current, currData.current); if (!readOnly) {
onSave?.(prevData.current, currData.current);
}
onClose(); onClose();
} catch (err: any) { } catch (err: any) {
showNotice("error", err.message || err.toString()); showNotice("error", err.message || err.toString());
@@ -148,10 +156,14 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
} }
}); });
const editorResize = debounce(() => { const editorResize = useMemo(
editorRef.current?.layout(); () =>
setTimeout(() => editorRef.current?.layout(), 500); debounce(() => {
}, 100); editorRef.current?.layout();
setTimeout(() => editorRef.current?.layout(), 500);
}, 100),
[],
);
useEffect(() => { useEffect(() => {
const onResized = debounce(() => { const onResized = debounce(() => {
@@ -167,11 +179,11 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
editorRef.current?.dispose(); editorRef.current?.dispose();
editorRef.current = undefined; editorRef.current = undefined;
}; };
}, []); }, [editorResize]);
return ( return (
<Dialog open={open} onClose={onClose} maxWidth="xl" fullWidth> <Dialog open={open} onClose={onClose} maxWidth="xl" fullWidth>
<DialogTitle>{title}</DialogTitle> <DialogTitle>{resolvedTitle}</DialogTitle>
<DialogContent <DialogContent
sx={{ sx={{

View File

@@ -24,37 +24,50 @@ export const GroupItem = (props: Props) => {
const sortable = type === "prepend" || type === "append"; const sortable = type === "prepend" || type === "append";
const { const {
attributes, attributes: sortableAttributes,
listeners, listeners: sortableListeners,
setNodeRef, setNodeRef: sortableSetNodeRef,
transform, transform,
transition, transition,
isDragging, isDragging,
} = sortable } = useSortable({
? useSortable({ id: group.name }) id: group.name,
: { disabled: !sortable,
attributes: {}, });
listeners: {}, const dragAttributes = sortable ? sortableAttributes : undefined;
setNodeRef: null, const dragListeners = sortable ? sortableListeners : undefined;
transform: null, const dragNodeRef = sortable ? sortableSetNodeRef : undefined;
transition: null,
isDragging: false,
};
const [iconCachePath, setIconCachePath] = useState(""); const [iconCachePath, setIconCachePath] = useState("");
useEffect(() => { useEffect(() => {
initIconCachePath(); let cancelled = false;
}, [group]); const initIconCachePath = async () => {
const icon = group.icon?.trim() ?? "";
if (icon.startsWith("http")) {
try {
const fileName =
group.name.replaceAll(" ", "") + "-" + getFileName(icon);
const iconPath = await downloadIconCache(icon, fileName);
if (!cancelled) {
setIconCachePath(convertFileSrc(iconPath));
}
} catch {
if (!cancelled) {
setIconCachePath("");
}
}
} else if (!cancelled) {
setIconCachePath("");
}
};
async function initIconCachePath() { void initIconCachePath();
if (group.icon && group.icon.trim().startsWith("http")) {
const fileName = return () => {
group.name.replaceAll(" ", "") + "-" + getFileName(group.icon); cancelled = true;
const iconPath = await downloadIconCache(group.icon, fileName); };
setIconCachePath(convertFileSrc(iconPath)); }, [group.icon, group.name]);
}
}
function getFileName(url: string) { function getFileName(url: string) {
return url.substring(url.lastIndexOf("/") + 1); return url.substring(url.lastIndexOf("/") + 1);
@@ -108,9 +121,9 @@ export const GroupItem = (props: Props) => {
/> />
)} )}
<ListItemText <ListItemText
{...attributes} {...(dragAttributes ?? {})}
{...listeners} {...(dragListeners ?? {})}
ref={setNodeRef} ref={dragNodeRef}
sx={{ cursor: sortable ? "move" : "" }} sx={{ cursor: sortable ? "move" : "" }}
primary={ primary={
<StyledPrimary <StyledPrimary
@@ -133,11 +146,13 @@ export const GroupItem = (props: Props) => {
</Box> </Box>
</ListItemTextChild> </ListItemTextChild>
} }
secondaryTypographyProps={{ slotProps={{
sx: { secondary: {
display: "flex", sx: {
alignItems: "center", display: "flex",
color: "#ccc", alignItems: "center",
color: "#ccc",
},
}, },
}} }}
/> />

View File

@@ -36,7 +36,13 @@ import {
cancelIdleCallback, cancelIdleCallback,
} from "foxact/request-idle-callback"; } from "foxact/request-idle-callback";
import yaml from "js-yaml"; import yaml from "js-yaml";
import { useEffect, useMemo, useState } from "react"; import {
startTransition,
useCallback,
useEffect,
useMemo,
useState,
} from "react";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import MonacoEditor from "react-monaco-editor"; import MonacoEditor from "react-monaco-editor";
@@ -160,7 +166,7 @@ export const GroupsEditorViewer = (props: Props) => {
} }
} }
}; };
const fetchContent = async () => { const fetchContent = useCallback(async () => {
const data = await readProfileFile(property); const data = await readProfileFile(property);
const obj = yaml.load(data) as ISeqProfileConfig | null; const obj = yaml.load(data) as ISeqProfileConfig | null;
@@ -170,21 +176,20 @@ export const GroupsEditorViewer = (props: Props) => {
setPrevData(data); setPrevData(data);
setCurrData(data); setCurrData(data);
}; }, [property]);
useEffect(() => { useEffect(() => {
if (currData === "") return; if (currData === "" || visualization !== true) {
if (visualization !== true) return; return;
}
const obj = yaml.load(currData) as { const obj = yaml.load(currData) as ISeqProfileConfig | null;
prepend: []; startTransition(() => {
append: []; setPrependSeq(obj?.prepend ?? []);
delete: []; setAppendSeq(obj?.append ?? []);
} | null; setDeleteSeq(obj?.delete ?? []);
setPrependSeq(obj?.prepend || []); });
setAppendSeq(obj?.append || []); }, [currData, visualization]);
setDeleteSeq(obj?.delete || []);
}, [visualization]);
// 优化异步处理大数据yaml.dump避免UI卡死 // 优化异步处理大数据yaml.dump避免UI卡死
useEffect(() => { useEffect(() => {
@@ -210,7 +215,7 @@ export const GroupsEditorViewer = (props: Props) => {
} }
}, [prependSeq, appendSeq, deleteSeq]); }, [prependSeq, appendSeq, deleteSeq]);
const fetchProxyPolicy = async () => { const fetchProxyPolicy = useCallback(async () => {
const data = await readProfileFile(profileUid); const data = await readProfileFile(profileUid);
const proxiesData = await readProfileFile(proxiesUid); const proxiesData = await readProfileFile(proxiesUid);
const originGroupsObj = yaml.load(data) as { const originGroupsObj = yaml.load(data) as {
@@ -246,8 +251,8 @@ export const GroupsEditorViewer = (props: Props) => {
proxies.map((proxy: any) => proxy.name), proxies.map((proxy: any) => proxy.name),
), ),
); );
}; }, [appendSeq, deleteSeq, prependSeq, profileUid, proxiesUid]);
const fetchProfile = async () => { const fetchProfile = useCallback(async () => {
const data = await readProfileFile(profileUid); const data = await readProfileFile(profileUid);
const mergeData = await readProfileFile(mergeUid); const mergeData = await readProfileFile(mergeUid);
const globalMergeData = await readProfileFile("Merge"); const globalMergeData = await readProfileFile("Merge");
@@ -257,17 +262,17 @@ export const GroupsEditorViewer = (props: Props) => {
} | null; } | null;
const originProviderObj = yaml.load(data) as { const originProviderObj = yaml.load(data) as {
"proxy-providers": {}; "proxy-providers": Record<string, unknown>;
} | null; } | null;
const originProvider = originProviderObj?.["proxy-providers"] || {}; const originProvider = originProviderObj?.["proxy-providers"] || {};
const moreProviderObj = yaml.load(mergeData) as { const moreProviderObj = yaml.load(mergeData) as {
"proxy-providers": {}; "proxy-providers": Record<string, unknown>;
} | null; } | null;
const moreProvider = moreProviderObj?.["proxy-providers"] || {}; const moreProvider = moreProviderObj?.["proxy-providers"] || {};
const globalProviderObj = yaml.load(globalMergeData) as { const globalProviderObj = yaml.load(globalMergeData) as {
"proxy-providers": {}; "proxy-providers": Record<string, unknown>;
} | null; } | null;
const globalProvider = globalProviderObj?.["proxy-providers"] || {}; const globalProvider = globalProviderObj?.["proxy-providers"] || {};
@@ -280,21 +285,27 @@ export const GroupsEditorViewer = (props: Props) => {
setProxyProviderList(Object.keys(provider)); setProxyProviderList(Object.keys(provider));
setGroupList(originGroupsObj?.["proxy-groups"] || []); setGroupList(originGroupsObj?.["proxy-groups"] || []);
}; }, [mergeUid, profileUid]);
const getInterfaceNameList = async () => { const getInterfaceNameList = useCallback(async () => {
const list = await getNetworkInterfaces(); const list = await getNetworkInterfaces();
setInterfaceNameList(list); setInterfaceNameList(list);
}; }, []);
useEffect(() => { useEffect(() => {
fetchProxyPolicy(); fetchProxyPolicy();
}, [prependSeq, appendSeq, deleteSeq]); }, [fetchProxyPolicy]);
useEffect(() => { useEffect(() => {
if (!open) return; if (!open) return;
fetchContent(); fetchContent();
fetchProxyPolicy(); fetchProxyPolicy();
fetchProfile(); fetchProfile();
getInterfaceNameList(); getInterfaceNameList();
}, [open]); }, [
fetchContent,
fetchProfile,
fetchProxyPolicy,
getInterfaceNameList,
open,
]);
const validateGroup = () => { const validateGroup = () => {
const group = formIns.getValues(); const group = formIns.getValues();
@@ -811,10 +822,10 @@ export const GroupsEditorViewer = (props: Props) => {
return x.name; return x.name;
})} })}
> >
{filteredPrependSeq.map((item, index) => { {filteredPrependSeq.map((item) => {
return ( return (
<GroupItem <GroupItem
key={`${item.name}-${index}`} key={item.name}
type="prepend" type="prepend"
group={item} group={item}
onDelete={() => { onDelete={() => {
@@ -834,7 +845,7 @@ export const GroupsEditorViewer = (props: Props) => {
const newIndex = index - shift; const newIndex = index - shift;
return ( return (
<GroupItem <GroupItem
key={`${filteredGroupList[newIndex].name}-${index}`} key={filteredGroupList[newIndex].name}
type={ type={
deleteSeq.includes(filteredGroupList[newIndex].name) deleteSeq.includes(filteredGroupList[newIndex].name)
? "delete" ? "delete"
@@ -871,10 +882,10 @@ export const GroupsEditorViewer = (props: Props) => {
return x.name; return x.name;
})} })}
> >
{filteredAppendSeq.map((item, index) => { {filteredAppendSeq.map((item) => {
return ( return (
<GroupItem <GroupItem
key={`${item.name}-${index}`} key={item.name}
type="append" type="append"
group={item} group={item}
onDelete={() => { onDelete={() => {

View File

@@ -37,8 +37,8 @@ export const LogViewer = (props: Props) => {
pb: 1, pb: 1,
}} }}
> >
{logInfo.map(([level, log], index) => ( {logInfo.map(([level, log]) => (
<Fragment key={index.toString()}> <Fragment key={`${level}-${log}`}>
<Typography color="text.secondary" component="div"> <Typography color="text.secondary" component="div">
<Chip <Chip
label={level} label={level}

View File

@@ -19,7 +19,7 @@ import {
import { open } from "@tauri-apps/plugin-shell"; import { open } from "@tauri-apps/plugin-shell";
import { useLockFn } from "ahooks"; import { useLockFn } from "ahooks";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { useEffect, useState } from "react"; import { useEffect, useReducer, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { mutate } from "swr"; import { mutate } from "swr";
@@ -61,6 +61,7 @@ interface Props {
export const ProfileItem = (props: Props) => { export const ProfileItem = (props: Props) => {
const { const {
id,
selected, selected,
activating, activating,
itemData, itemData,
@@ -80,11 +81,11 @@ export const ProfileItem = (props: Props) => {
transition, transition,
isDragging, isDragging,
} = useSortable({ } = useSortable({
id: props.id, id,
}); });
const { t } = useTranslation(); const { t } = useTranslation();
const [anchorEl, setAnchorEl] = useState<any>(null); const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
const [position, setPosition] = useState({ left: 0, top: 0 }); const [position, setPosition] = useState({ left: 0, top: 0 });
const loadingCache = useLoadingCache(); const loadingCache = useLoadingCache();
const setLoadingCache = useSetLoadingCache(); const setLoadingCache = useSetLoadingCache();
@@ -166,37 +167,44 @@ export const ProfileItem = (props: Props) => {
if (showNextUpdate) { if (showNextUpdate) {
fetchNextUpdateTime(); fetchNextUpdateTime();
} }
}, [showNextUpdate, itemData.option?.update_interval, updated]); }, [
fetchNextUpdateTime,
showNextUpdate,
itemData.option?.update_interval,
updated,
]);
// 订阅定时器更新事件 // 订阅定时器更新事件
useEffect(() => { useEffect(() => {
let refreshTimeout: ReturnType<typeof setTimeout> | undefined;
// 处理定时器更新事件 - 这个事件专门用于通知定时器变更 // 处理定时器更新事件 - 这个事件专门用于通知定时器变更
const handleTimerUpdate = (event: any) => { const handleTimerUpdate = (event: Event) => {
const updatedUid = event.payload as string; const source = event as CustomEvent<string> & { payload?: string };
const updatedUid = source.detail ?? source.payload;
// 只有当更新的是当前配置时才刷新显示 // 只有当更新的是当前配置时才刷新显示
if (updatedUid === itemData.uid && showNextUpdate) { if (updatedUid === itemData.uid && showNextUpdate) {
console.log(`收到定时器更新事件: uid=${updatedUid}`); console.log(`收到定时器更新事件: uid=${updatedUid}`);
setTimeout(() => { if (refreshTimeout) {
clearTimeout(refreshTimeout);
}
refreshTimeout = window.setTimeout(() => {
fetchNextUpdateTime(true); fetchNextUpdateTime(true);
}, 1000); }, 1000);
} }
}; };
// 只注册定时器更新事件监听 // 只注册定时器更新事件监听
window.addEventListener( window.addEventListener("verge://timer-updated", handleTimerUpdate);
"verge://timer-updated",
handleTimerUpdate as EventListener,
);
return () => { return () => {
if (refreshTimeout) {
clearTimeout(refreshTimeout);
}
// 清理事件监听 // 清理事件监听
window.removeEventListener( window.removeEventListener("verge://timer-updated", handleTimerUpdate);
"verge://timer-updated",
handleTimerUpdate as EventListener,
);
}; };
}, [showNextUpdate, itemData.uid]); }, [fetchNextUpdateTime, itemData.uid, showNextUpdate]);
// local file mode // local file mode
// remote file mode // remote file mode
@@ -217,11 +225,11 @@ export const ProfileItem = (props: Props) => {
const loading = loadingCache[itemData.uid] ?? false; const loading = loadingCache[itemData.uid] ?? false;
// interval update fromNow field // interval update fromNow field
const [, setRefresh] = useState({}); const [, forceRefresh] = useReducer((value: number) => value + 1, 0);
useEffect(() => { useEffect(() => {
if (!hasUrl) return; if (!hasUrl) return;
let timer: any = null; let timer: ReturnType<typeof setTimeout> | undefined;
const handler = () => { const handler = () => {
const now = Date.now(); const now = Date.now();
@@ -232,7 +240,7 @@ export const ProfileItem = (props: Props) => {
const wait = now - lastUpdate >= 36e5 ? 30e5 : 5e4; const wait = now - lastUpdate >= 36e5 ? 30e5 : 5e4;
timer = setTimeout(() => { timer = setTimeout(() => {
setRefresh({}); forceRefresh();
handler(); handler();
}, wait); }, wait);
}; };
@@ -240,9 +248,12 @@ export const ProfileItem = (props: Props) => {
handler(); handler();
return () => { return () => {
if (timer) clearTimeout(timer); if (timer) {
clearTimeout(timer);
timer = undefined;
}
}; };
}, [hasUrl, updated]); }, [forceRefresh, hasUrl, updated]);
const [fileOpen, setFileOpen] = useState(false); const [fileOpen, setFileOpen] = useState(false);
const [rulesOpen, setRulesOpen] = useState(false); const [rulesOpen, setRulesOpen] = useState(false);
@@ -382,7 +393,9 @@ export const ProfileItem = (props: Props) => {
setAnchorEl(null); setAnchorEl(null);
if (batchMode) { if (batchMode) {
// If in batch mode, just toggle selection instead of showing delete confirmation // If in batch mode, just toggle selection instead of showing delete confirmation
onSelectionChange && onSelectionChange(); if (onSelectionChange) {
onSelectionChange();
}
} else { } else {
setConfirmOpen(true); setConfirmOpen(true);
} }
@@ -426,7 +439,9 @@ export const ProfileItem = (props: Props) => {
setAnchorEl(null); setAnchorEl(null);
if (batchMode) { if (batchMode) {
// If in batch mode, just toggle selection instead of showing delete confirmation // If in batch mode, just toggle selection instead of showing delete confirmation
onSelectionChange && onSelectionChange(); if (onSelectionChange) {
onSelectionChange();
}
} else { } else {
setConfirmOpen(true); setConfirmOpen(true);
} }
@@ -444,14 +459,16 @@ export const ProfileItem = (props: Props) => {
// 监听自动更新事件 // 监听自动更新事件
useEffect(() => { useEffect(() => {
const handleUpdateStarted = (event: CustomEvent) => { const handleUpdateStarted = (event: Event) => {
if (event.detail.uid === itemData.uid) { const customEvent = event as CustomEvent<{ uid?: string }>;
if (customEvent.detail?.uid === itemData.uid) {
setLoadingCache((cache) => ({ ...cache, [itemData.uid]: true })); setLoadingCache((cache) => ({ ...cache, [itemData.uid]: true }));
} }
}; };
const handleUpdateCompleted = (event: CustomEvent) => { const handleUpdateCompleted = (event: Event) => {
if (event.detail.uid === itemData.uid) { const customEvent = event as CustomEvent<{ uid?: string }>;
if (customEvent.detail?.uid === itemData.uid) {
setLoadingCache((cache) => ({ ...cache, [itemData.uid]: false })); setLoadingCache((cache) => ({ ...cache, [itemData.uid]: false }));
// 更新完成后刷新显示 // 更新完成后刷新显示
if (showNextUpdate) { if (showNextUpdate) {
@@ -461,27 +478,18 @@ export const ProfileItem = (props: Props) => {
}; };
// 注册事件监听 // 注册事件监听
window.addEventListener( window.addEventListener("profile-update-started", handleUpdateStarted);
"profile-update-started", window.addEventListener("profile-update-completed", handleUpdateCompleted);
handleUpdateStarted as EventListener,
);
window.addEventListener(
"profile-update-completed",
handleUpdateCompleted as EventListener,
);
return () => { return () => {
// 清理事件监听 // 清理事件监听
window.removeEventListener( window.removeEventListener("profile-update-started", handleUpdateStarted);
"profile-update-started",
handleUpdateStarted as EventListener,
);
window.removeEventListener( window.removeEventListener(
"profile-update-completed", "profile-update-completed",
handleUpdateCompleted as EventListener, handleUpdateCompleted,
); );
}; };
}, [itemData.uid, showNextUpdate]); }, [fetchNextUpdateTime, itemData.uid, setLoadingCache, showNextUpdate]);
return ( return (
<Box <Box
@@ -506,7 +514,7 @@ export const ProfileItem = (props: Props) => {
onContextMenu={(event) => { onContextMenu={(event) => {
const { clientX, clientY } = event; const { clientX, clientY } = event;
setPosition({ top: clientY, left: clientX }); setPosition({ top: clientY, left: clientX });
setAnchorEl(event.currentTarget); setAnchorEl(event.currentTarget as HTMLElement);
event.preventDefault(); event.preventDefault();
}} }}
> >
@@ -543,7 +551,9 @@ export const ProfileItem = (props: Props) => {
sx={{ padding: "2px", marginRight: "4px", marginLeft: "-8px" }} sx={{ padding: "2px", marginRight: "4px", marginLeft: "-8px" }}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
onSelectionChange && onSelectionChange(); if (onSelectionChange) {
onSelectionChange();
}
}} }}
> >
{isSelected ? ( {isSelected ? (
@@ -737,7 +747,7 @@ export const ProfileItem = (props: Props) => {
schema="clash" schema="clash"
onSave={async (prev, curr) => { onSave={async (prev, curr) => {
await saveProfileFile(uid, curr ?? ""); await saveProfileFile(uid, curr ?? "");
onSave && onSave(prev, curr); onSave?.(prev, curr);
}} }}
onClose={() => setFileOpen(false)} onClose={() => setFileOpen(false)}
/> />
@@ -783,7 +793,7 @@ export const ProfileItem = (props: Props) => {
schema="clash" schema="clash"
onSave={async (prev, curr) => { onSave={async (prev, curr) => {
await saveProfileFile(option?.merge ?? "", curr ?? ""); await saveProfileFile(option?.merge ?? "", curr ?? "");
onSave && onSave(prev, curr); onSave?.(prev, curr);
}} }}
onClose={() => setMergeOpen(false)} onClose={() => setMergeOpen(false)}
/> />
@@ -795,7 +805,7 @@ export const ProfileItem = (props: Props) => {
language="javascript" language="javascript"
onSave={async (prev, curr) => { onSave={async (prev, curr) => {
await saveProfileFile(option?.script ?? "", curr ?? ""); await saveProfileFile(option?.script ?? "", curr ?? "");
onSave && onSave(prev, curr); onSave?.(prev, curr);
}} }}
onClose={() => setScriptOpen(false)} onClose={() => setScriptOpen(false)}
/> />

View File

@@ -25,12 +25,15 @@ interface Props {
onSave?: (prev?: string, curr?: string) => void; onSave?: (prev?: string, curr?: string) => void;
} }
const EMPTY_LOG_INFO: [string, string][] = [];
// profile enhanced item // profile enhanced item
export const ProfileMore = (props: Props) => { export const ProfileMore = (props: Props) => {
const { id, logInfo = [], onSave } = props; const { id, logInfo, onSave } = props;
const entries = logInfo ?? EMPTY_LOG_INFO;
const { t } = useTranslation(); const { t } = useTranslation();
const [anchorEl, setAnchorEl] = useState<any>(null); const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
const [position, setPosition] = useState({ left: 0, top: 0 }); const [position, setPosition] = useState({ left: 0, top: 0 });
const [fileOpen, setFileOpen] = useState(false); const [fileOpen, setFileOpen] = useState(false);
const [logOpen, setLogOpen] = useState(false); const [logOpen, setLogOpen] = useState(false);
@@ -49,7 +52,7 @@ export const ProfileMore = (props: Props) => {
} }
}); });
const hasError = !!logInfo.find((e) => e[0] === "exception"); const hasError = entries.some(([level]) => level === "exception");
const itemMenu = [ const itemMenu = [
{ label: "Edit File", handler: onEditFile }, { label: "Edit File", handler: onEditFile },
@@ -71,7 +74,7 @@ export const ProfileMore = (props: Props) => {
onContextMenu={(event) => { onContextMenu={(event) => {
const { clientX, clientY } = event; const { clientX, clientY } = event;
setPosition({ top: clientY, left: clientX }); setPosition({ top: clientY, left: clientX });
setAnchorEl(event.currentTarget); setAnchorEl(event.currentTarget as HTMLElement);
event.preventDefault(); event.preventDefault();
}} }}
> >
@@ -173,7 +176,7 @@ export const ProfileMore = (props: Props) => {
schema={id === "Merge" ? "clash" : undefined} schema={id === "Merge" ? "clash" : undefined}
onSave={async (prev, curr) => { onSave={async (prev, curr) => {
await saveProfileFile(id, curr ?? ""); await saveProfileFile(id, curr ?? "");
onSave && onSave(prev, curr); onSave?.(prev, curr);
}} }}
onClose={() => setFileOpen(false)} onClose={() => setFileOpen(false)}
/> />
@@ -181,7 +184,7 @@ export const ProfileMore = (props: Props) => {
{logOpen && ( {logOpen && (
<LogViewer <LogViewer
open={logOpen} open={logOpen}
logInfo={logInfo} logInfo={entries}
onClose={() => setLogOpen(false)} onClose={() => setLogOpen(false)}
/> />
)} )}

View File

@@ -45,23 +45,19 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
// file input // file input
const fileDataRef = useRef<string | null>(null); const fileDataRef = useRef<string | null>(null);
const { const { control, watch, setValue, reset, handleSubmit, getValues } =
control, useForm<IProfileItem>({
watch, defaultValues: {
register: _register, type: "remote",
...formIns name: "",
} = useForm<IProfileItem>({ desc: "",
defaultValues: { url: "",
type: "remote", option: {
name: "", with_proxy: false,
desc: "", self_proxy: false,
url: "", },
option: {
with_proxy: false,
self_proxy: false,
}, },
}, });
});
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
create: () => { create: () => {
@@ -71,7 +67,7 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
edit: (item: IProfileItem) => { edit: (item: IProfileItem) => {
if (item) { if (item) {
Object.entries(item).forEach(([key, value]) => { Object.entries(item).forEach(([key, value]) => {
formIns.setValue(key as any, value); setValue(key as any, value);
}); });
} }
setOpenType("edit"); setOpenType("edit");
@@ -83,15 +79,15 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
const withProxy = watch("option.with_proxy"); const withProxy = watch("option.with_proxy");
useEffect(() => { useEffect(() => {
if (selfProxy) formIns.setValue("option.with_proxy", false); if (selfProxy) setValue("option.with_proxy", false);
}, [selfProxy]); }, [selfProxy, setValue]);
useEffect(() => { useEffect(() => {
if (withProxy) formIns.setValue("option.self_proxy", false); if (withProxy) setValue("option.self_proxy", false);
}, [withProxy]); }, [setValue, withProxy]);
const handleOk = useLockFn( const handleOk = useLockFn(
formIns.handleSubmit(async (form) => { handleSubmit(async (form) => {
if (form.option?.timeout_seconds) { if (form.option?.timeout_seconds) {
form.option.timeout_seconds = +form.option.timeout_seconds; form.option.timeout_seconds = +form.option.timeout_seconds;
} }
@@ -183,7 +179,7 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
// 成功后的操作 // 成功后的操作
setOpen(false); setOpen(false);
setTimeout(() => formIns.reset(), 500); setTimeout(() => reset(), 500);
fileDataRef.current = null; fileDataRef.current = null;
// 优化UI先关闭异步通知父组件 // 优化UI先关闭异步通知父组件
@@ -202,7 +198,7 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
try { try {
setOpen(false); setOpen(false);
fileDataRef.current = null; fileDataRef.current = null;
setTimeout(() => formIns.reset(), 500); setTimeout(() => reset(), 500);
} catch (e) { } catch (e) {
console.warn("[ProfileViewer] handleClose error:", e); console.warn("[ProfileViewer] handleClose error:", e);
} }
@@ -341,7 +337,7 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
{isLocal && openType === "new" && ( {isLocal && openType === "new" && (
<FileInput <FileInput
onChange={(file, val) => { onChange={(file, val) => {
formIns.setValue("name", formIns.getValues("name") || file.name); setValue("name", getValues("name") || file.name);
fileDataRef.current = val; fileDataRef.current = val;
}} }}
/> />

View File

@@ -29,7 +29,13 @@ import {
} from "@mui/material"; } from "@mui/material";
import { useLockFn } from "ahooks"; import { useLockFn } from "ahooks";
import yaml from "js-yaml"; import yaml from "js-yaml";
import { useEffect, useMemo, useState } from "react"; import {
startTransition,
useCallback,
useEffect,
useMemo,
useState,
} from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import MonacoEditor from "react-monaco-editor"; import MonacoEditor from "react-monaco-editor";
import { Virtuoso } from "react-virtuoso"; import { Virtuoso } from "react-virtuoso";
@@ -145,7 +151,9 @@ export const ProxiesEditorViewer = (props: Props) => {
const lines = uris.trim().split("\n"); const lines = uris.trim().split("\n");
let idx = 0; let idx = 0;
const batchSize = 50; const batchSize = 50;
function parseBatch() { let parseTimer: ReturnType<typeof setTimeout> | undefined;
const parseBatch = () => {
const end = Math.min(idx + batchSize, lines.length); const end = Math.min(idx + batchSize, lines.length);
for (; idx < end; idx++) { for (; idx < end; idx++) {
const uri = lines[idx]; const uri = lines[idx];
@@ -165,14 +173,18 @@ export const ProxiesEditorViewer = (props: Props) => {
} }
} }
if (idx < lines.length) { if (idx < lines.length) {
setTimeout(parseBatch, 0); parseTimer = window.setTimeout(parseBatch, 0);
} else { } else {
if (parseTimer) {
clearTimeout(parseTimer);
parseTimer = undefined;
}
cb(proxies); cb(proxies);
} }
} };
parseBatch(); parseBatch();
}; };
const fetchProfile = async () => { const fetchProfile = useCallback(async () => {
const data = await readProfileFile(profileUid); const data = await readProfileFile(profileUid);
const originProxiesObj = yaml.load(data) as { const originProxiesObj = yaml.load(data) as {
@@ -180,9 +192,9 @@ export const ProxiesEditorViewer = (props: Props) => {
} | null; } | null;
setProxyList(originProxiesObj?.proxies || []); setProxyList(originProxiesObj?.proxies || []);
}; }, [profileUid]);
const fetchContent = async () => { const fetchContent = useCallback(async () => {
const data = await readProfileFile(property); const data = await readProfileFile(property);
const obj = yaml.load(data) as ISeqProfileConfig | null; const obj = yaml.load(data) as ISeqProfileConfig | null;
@@ -192,50 +204,61 @@ export const ProxiesEditorViewer = (props: Props) => {
setPrevData(data); setPrevData(data);
setCurrData(data); setCurrData(data);
}; }, [property]);
useEffect(() => { useEffect(() => {
if (currData === "") return; if (currData === "" || visualization !== true) {
if (visualization !== true) return; return;
const obj = yaml.load(currData) as {
prepend: [];
append: [];
delete: [];
} | null;
setPrependSeq(obj?.prepend || []);
setAppendSeq(obj?.append || []);
setDeleteSeq(obj?.delete || []);
}, [visualization]);
useEffect(() => {
if (prependSeq && appendSeq && deleteSeq) {
const serialize = () => {
try {
setCurrData(
yaml.dump(
{ prepend: prependSeq, append: appendSeq, delete: deleteSeq },
{ forceQuotes: true },
),
);
} catch (e) {
console.warn("[ProxiesEditorViewer] yaml.dump failed:", e);
// 防止异常导致UI卡死
}
};
if (window.requestIdleCallback) {
window.requestIdleCallback(serialize);
} else {
setTimeout(serialize, 0);
}
} }
const obj = yaml.load(currData) as ISeqProfileConfig | null;
startTransition(() => {
setPrependSeq(obj?.prepend ?? []);
setAppendSeq(obj?.append ?? []);
setDeleteSeq(obj?.delete ?? []);
});
}, [currData, visualization]);
useEffect(() => {
if (!(prependSeq && appendSeq && deleteSeq)) {
return;
}
const serialize = () => {
try {
setCurrData(
yaml.dump(
{ prepend: prependSeq, append: appendSeq, delete: deleteSeq },
{ forceQuotes: true },
),
);
} catch (e) {
console.warn("[ProxiesEditorViewer] yaml.dump failed:", e);
// 防止异常导致UI卡死
}
};
let idleId: number | undefined;
let timeoutId: ReturnType<typeof setTimeout> | undefined;
if (window.requestIdleCallback) {
idleId = window.requestIdleCallback(serialize);
} else {
timeoutId = window.setTimeout(serialize, 0);
}
return () => {
if (idleId !== undefined && window.cancelIdleCallback) {
window.cancelIdleCallback(idleId);
}
if (timeoutId) {
clearTimeout(timeoutId);
}
};
}, [prependSeq, appendSeq, deleteSeq]); }, [prependSeq, appendSeq, deleteSeq]);
useEffect(() => { useEffect(() => {
if (!open) return; if (!open) return;
fetchContent(); fetchContent();
fetchProfile(); fetchProfile();
}, [open]); }, [fetchContent, fetchProfile, open]);
const handleSave = useLockFn(async () => { const handleSave = useLockFn(async () => {
try { try {
@@ -357,10 +380,10 @@ export const ProxiesEditorViewer = (props: Props) => {
return x.name; return x.name;
})} })}
> >
{filteredPrependSeq.map((item, index) => { {filteredPrependSeq.map((item) => {
return ( return (
<ProxyItem <ProxyItem
key={`${item.name}-${index}`} key={item.name}
type="prepend" type="prepend"
proxy={item} proxy={item}
onDelete={() => { onDelete={() => {
@@ -380,7 +403,7 @@ export const ProxiesEditorViewer = (props: Props) => {
const newIndex = index - shift; const newIndex = index - shift;
return ( return (
<ProxyItem <ProxyItem
key={`${filteredProxyList[newIndex].name}-${index}`} key={filteredProxyList[newIndex].name}
type={ type={
deleteSeq.includes(filteredProxyList[newIndex].name) deleteSeq.includes(filteredProxyList[newIndex].name)
? "delete" ? "delete"
@@ -417,10 +440,10 @@ export const ProxiesEditorViewer = (props: Props) => {
return x.name; return x.name;
})} })}
> >
{filteredAppendSeq.map((item, index) => { {filteredAppendSeq.map((item) => {
return ( return (
<ProxyItem <ProxyItem
key={`${item.name}-${index}`} key={item.name}
type="append" type="append"
proxy={item} proxy={item}
onDelete={() => { onDelete={() => {

View File

@@ -21,22 +21,19 @@ export const ProxyItem = (props: Props) => {
const sortable = type === "prepend" || type === "append"; const sortable = type === "prepend" || type === "append";
const { const {
attributes, attributes: sortableAttributes,
listeners, listeners: sortableListeners,
setNodeRef, setNodeRef: sortableSetNodeRef,
transform, transform,
transition, transition,
isDragging, isDragging,
} = sortable } = useSortable({
? useSortable({ id: proxy.name }) id: proxy.name,
: { disabled: !sortable,
attributes: {}, });
listeners: {}, const dragAttributes = sortable ? sortableAttributes : undefined;
setNodeRef: null, const dragListeners = sortable ? sortableListeners : undefined;
transform: null, const dragNodeRef = sortable ? sortableSetNodeRef : undefined;
transition: null,
isDragging: false,
};
return ( return (
<ListItem <ListItem
@@ -60,9 +57,9 @@ export const ProxyItem = (props: Props) => {
})} })}
> >
<ListItemText <ListItemText
{...attributes} {...(dragAttributes ?? {})}
{...listeners} {...(dragListeners ?? {})}
ref={setNodeRef} ref={dragNodeRef}
sx={{ cursor: sortable ? "move" : "" }} sx={{ cursor: sortable ? "move" : "" }}
primary={ primary={
<StyledPrimary <StyledPrimary
@@ -86,11 +83,13 @@ export const ProxyItem = (props: Props) => {
</Box> </Box>
</ListItemTextChild> </ListItemTextChild>
} }
secondaryTypographyProps={{ slotProps={{
sx: { secondary: {
display: "flex", sx: {
alignItems: "center", display: "flex",
color: "#ccc", alignItems: "center",
color: "#ccc",
},
}, },
}} }}
/> />

View File

@@ -95,11 +95,13 @@ export const RuleItem = (props: Props) => {
</StyledSubtitle> </StyledSubtitle>
</ListItemTextChild> </ListItemTextChild>
} }
secondaryTypographyProps={{ slotProps={{
sx: { secondary: {
display: "flex", sx: {
alignItems: "center", display: "flex",
color: "#ccc", alignItems: "center",
color: "#ccc",
},
}, },
}} }}
/> />

View File

@@ -31,7 +31,13 @@ import {
} from "@mui/material"; } from "@mui/material";
import { useLockFn } from "ahooks"; import { useLockFn } from "ahooks";
import yaml from "js-yaml"; import yaml from "js-yaml";
import { useEffect, useMemo, useState } from "react"; import {
startTransition,
useCallback,
useEffect,
useMemo,
useState,
} from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import MonacoEditor from "react-monaco-editor"; import MonacoEditor from "react-monaco-editor";
import { Virtuoso } from "react-virtuoso"; import { Virtuoso } from "react-virtuoso";
@@ -305,7 +311,7 @@ export const RulesEditorViewer = (props: Props) => {
} }
} }
}; };
const fetchContent = async () => { const fetchContent = useCallback(async () => {
const data = await readProfileFile(property); const data = await readProfileFile(property);
const obj = yaml.load(data) as ISeqProfileConfig | null; const obj = yaml.load(data) as ISeqProfileConfig | null;
@@ -315,42 +321,57 @@ export const RulesEditorViewer = (props: Props) => {
setPrevData(data); setPrevData(data);
setCurrData(data); setCurrData(data);
}; }, [property]);
useEffect(() => { useEffect(() => {
if (currData === "") return; if (currData === "" || visualization !== true) {
if (visualization !== true) return; return;
}
const obj = yaml.load(currData) as ISeqProfileConfig | null; const obj = yaml.load(currData) as ISeqProfileConfig | null;
setPrependSeq(obj?.prepend || []); startTransition(() => {
setAppendSeq(obj?.append || []); setPrependSeq(obj?.prepend ?? []);
setDeleteSeq(obj?.delete || []); setAppendSeq(obj?.append ?? []);
}, [visualization]); setDeleteSeq(obj?.delete ?? []);
});
}, [currData, visualization]);
// 优化异步处理大数据yaml.dump避免UI卡死 // 优化异步处理大数据yaml.dump避免UI卡死
useEffect(() => { useEffect(() => {
if (prependSeq && appendSeq && deleteSeq) { if (!(prependSeq && appendSeq && deleteSeq)) {
const serialize = () => { return;
try {
setCurrData(
yaml.dump(
{ prepend: prependSeq, append: appendSeq, delete: deleteSeq },
{ forceQuotes: true },
),
);
} catch (e: any) {
showNotice("error", e?.message || e?.toString() || "YAML dump error");
}
};
if (window.requestIdleCallback) {
window.requestIdleCallback(serialize);
} else {
setTimeout(serialize, 0);
}
} }
const serialize = () => {
try {
setCurrData(
yaml.dump(
{ prepend: prependSeq, append: appendSeq, delete: deleteSeq },
{ forceQuotes: true },
),
);
} catch (e: any) {
showNotice("error", e?.message || e?.toString() || "YAML dump error");
}
};
let idleId: number | undefined;
let timeoutId: ReturnType<typeof setTimeout> | undefined;
if (window.requestIdleCallback) {
idleId = window.requestIdleCallback(serialize);
} else {
timeoutId = window.setTimeout(serialize, 0);
}
return () => {
if (idleId !== undefined && window.cancelIdleCallback) {
window.cancelIdleCallback(idleId);
}
if (timeoutId) {
clearTimeout(timeoutId);
}
};
}, [prependSeq, appendSeq, deleteSeq]); }, [prependSeq, appendSeq, deleteSeq]);
const fetchProfile = async () => { const fetchProfile = useCallback(async () => {
const data = await readProfileFile(profileUid); // 原配置文件 const data = await readProfileFile(profileUid); // 原配置文件
const groupsData = await readProfileFile(groupsUid); // groups配置文件 const groupsData = await readProfileFile(groupsUid); // groups配置文件
const mergeData = await readProfileFile(mergeUid); // merge配置文件 const mergeData = await readProfileFile(mergeUid); // merge配置文件
@@ -358,13 +379,25 @@ export const RulesEditorViewer = (props: Props) => {
const rulesObj = yaml.load(data) as { rules: [] } | null; const rulesObj = yaml.load(data) as { rules: [] } | null;
const originGroupsObj = yaml.load(data) as { "proxy-groups": [] } | null; const originGroupsObj = yaml.load(data) as {
"proxy-groups": IProxyGroupConfig[];
} | null;
const originGroups = originGroupsObj?.["proxy-groups"] || []; const originGroups = originGroupsObj?.["proxy-groups"] || [];
const moreGroupsObj = yaml.load(groupsData) as ISeqProfileConfig | null; const moreGroupsObj = yaml.load(groupsData) as ISeqProfileConfig | null;
const morePrependGroups = moreGroupsObj?.["prepend"] || []; const rawPrependGroups = moreGroupsObj?.["prepend"];
const moreAppendGroups = moreGroupsObj?.["append"] || []; const morePrependGroups = Array.isArray(rawPrependGroups)
const moreDeleteGroups = ? (rawPrependGroups as IProxyGroupConfig[])
moreGroupsObj?.["delete"] || ([] as string[] | { name: string }[]); : [];
const rawAppendGroups = moreGroupsObj?.["append"];
const moreAppendGroups = Array.isArray(rawAppendGroups)
? (rawAppendGroups as IProxyGroupConfig[])
: [];
const rawDeleteGroups = moreGroupsObj?.["delete"];
const moreDeleteGroups: Array<string | { name: string }> = Array.isArray(
rawDeleteGroups,
)
? (rawDeleteGroups as Array<string | { name: string }>)
: [];
const groups = morePrependGroups.concat( const groups = morePrependGroups.concat(
originGroups.filter((group: any) => { originGroups.filter((group: any) => {
if (group.name) { if (group.name) {
@@ -376,14 +409,16 @@ export const RulesEditorViewer = (props: Props) => {
moreAppendGroups, moreAppendGroups,
); );
const originRuleSetObj = yaml.load(data) as { "rule-providers": {} } | null; const originRuleSetObj = yaml.load(data) as {
"rule-providers": Record<string, unknown>;
} | null;
const originRuleSet = originRuleSetObj?.["rule-providers"] || {}; const originRuleSet = originRuleSetObj?.["rule-providers"] || {};
const moreRuleSetObj = yaml.load(mergeData) as { const moreRuleSetObj = yaml.load(mergeData) as {
"rule-providers": {}; "rule-providers": Record<string, unknown>;
} | null; } | null;
const moreRuleSet = moreRuleSetObj?.["rule-providers"] || {}; const moreRuleSet = moreRuleSetObj?.["rule-providers"] || {};
const globalRuleSetObj = yaml.load(globalMergeData) as { const globalRuleSetObj = yaml.load(globalMergeData) as {
"rule-providers": {}; "rule-providers": Record<string, unknown>;
} | null; } | null;
const globalRuleSet = globalRuleSetObj?.["rule-providers"] || {}; const globalRuleSet = globalRuleSetObj?.["rule-providers"] || {};
const ruleSet = Object.assign( const ruleSet = Object.assign(
@@ -393,12 +428,16 @@ export const RulesEditorViewer = (props: Props) => {
globalRuleSet, globalRuleSet,
); );
const originSubRuleObj = yaml.load(data) as { "sub-rules": {} } | null; const originSubRuleObj = yaml.load(data) as {
"sub-rules": Record<string, unknown>;
} | null;
const originSubRule = originSubRuleObj?.["sub-rules"] || {}; const originSubRule = originSubRuleObj?.["sub-rules"] || {};
const moreSubRuleObj = yaml.load(mergeData) as { "sub-rules": {} } | null; const moreSubRuleObj = yaml.load(mergeData) as {
"sub-rules": Record<string, unknown>;
} | null;
const moreSubRule = moreSubRuleObj?.["sub-rules"] || {}; const moreSubRule = moreSubRuleObj?.["sub-rules"] || {};
const globalSubRuleObj = yaml.load(globalMergeData) as { const globalSubRuleObj = yaml.load(globalMergeData) as {
"sub-rules": {}; "sub-rules": Record<string, unknown>;
} | null; } | null;
const globalSubRule = globalSubRuleObj?.["sub-rules"] || {}; const globalSubRule = globalSubRuleObj?.["sub-rules"] || {};
const subRule = Object.assign( const subRule = Object.assign(
@@ -413,13 +452,13 @@ export const RulesEditorViewer = (props: Props) => {
setRuleSetList(Object.keys(ruleSet)); setRuleSetList(Object.keys(ruleSet));
setSubRuleList(Object.keys(subRule)); setSubRuleList(Object.keys(subRule));
setRuleList(rulesObj?.rules || []); setRuleList(rulesObj?.rules || []);
}; }, [groupsUid, mergeUid, profileUid]);
useEffect(() => { useEffect(() => {
if (!open) return; if (!open) return;
fetchContent(); fetchContent();
fetchProfile(); fetchProfile();
}, [open]); }, [fetchContent, fetchProfile, open]);
const validateRule = () => { const validateRule = () => {
if ((ruleType.required ?? true) && !ruleContent) { if ((ruleType.required ?? true) && !ruleContent) {
@@ -626,10 +665,10 @@ export const RulesEditorViewer = (props: Props) => {
return x; return x;
})} })}
> >
{filteredPrependSeq.map((item, index) => { {filteredPrependSeq.map((item) => {
return ( return (
<RuleItem <RuleItem
key={`${item}-${index}`} key={item}
type="prepend" type="prepend"
ruleRaw={item} ruleRaw={item}
onDelete={() => { onDelete={() => {
@@ -647,7 +686,7 @@ export const RulesEditorViewer = (props: Props) => {
const newIndex = index - shift; const newIndex = index - shift;
return ( return (
<RuleItem <RuleItem
key={`${filteredRuleList[newIndex]}-${index}`} key={filteredRuleList[newIndex]}
type={ type={
deleteSeq.includes(filteredRuleList[newIndex]) deleteSeq.includes(filteredRuleList[newIndex])
? "delete" ? "delete"
@@ -682,10 +721,10 @@ export const RulesEditorViewer = (props: Props) => {
return x; return x;
})} })}
> >
{filteredAppendSeq.map((item, index) => { {filteredAppendSeq.map((item) => {
return ( return (
<RuleItem <RuleItem
key={`${item}-${index}`} key={item}
type="append" type="append"
ruleRaw={item} ruleRaw={item}
onDelete={() => { onDelete={() => {