refactor: profile components
This commit is contained in:
@@ -20,7 +20,7 @@ import metaSchema from "meta-json-schema/schemas/meta-json-schema.json";
|
||||
import * as monaco from "monaco-editor";
|
||||
import { configureMonacoYaml } from "monaco-yaml";
|
||||
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 MonacoEditor from "react-monaco-editor";
|
||||
import pac from "types-pac/pac.d.ts?raw";
|
||||
@@ -63,13 +63,13 @@ const monacoInitialization = () => {
|
||||
{
|
||||
uri: "http://example.com/meta-json-schema.json",
|
||||
fileMatch: ["**/*.clash.yaml"],
|
||||
// @ts-ignore
|
||||
// @ts-expect-error -- meta schema JSON import does not satisfy JSONSchema7 at compile time
|
||||
schema: metaSchema as JSONSchema7,
|
||||
},
|
||||
{
|
||||
uri: "http://example.com/clash-verge-merge-json-schema.json",
|
||||
fileMatch: ["**/*.merge.yaml"],
|
||||
// @ts-ignore
|
||||
// @ts-expect-error -- merge schema JSON import does not satisfy JSONSchema7 at compile time
|
||||
schema: mergeSchema as JSONSchema7,
|
||||
},
|
||||
],
|
||||
@@ -87,8 +87,8 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
|
||||
|
||||
const {
|
||||
open = false,
|
||||
title = t("Edit File"),
|
||||
initialData = Promise.resolve(""),
|
||||
title,
|
||||
initialData,
|
||||
readOnly = false,
|
||||
language = "yaml",
|
||||
schema,
|
||||
@@ -97,6 +97,12 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
|
||||
onClose,
|
||||
} = props;
|
||||
|
||||
const resolvedTitle = title ?? t("Edit File");
|
||||
const resolvedInitialData = useMemo(
|
||||
() => initialData ?? Promise.resolve(""),
|
||||
[initialData],
|
||||
);
|
||||
|
||||
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor>(undefined);
|
||||
const prevData = useRef<string | undefined>("");
|
||||
const currData = useRef<string | undefined>("");
|
||||
@@ -111,7 +117,7 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
|
||||
editorRef.current = editor;
|
||||
|
||||
// retrieve initial data
|
||||
await initialData.then((data) => {
|
||||
await resolvedInitialData.then((data) => {
|
||||
prevData.current = data;
|
||||
currData.current = data;
|
||||
|
||||
@@ -133,7 +139,9 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
|
||||
|
||||
const handleSave = useLockFn(async () => {
|
||||
try {
|
||||
!readOnly && onSave?.(prevData.current, currData.current);
|
||||
if (!readOnly) {
|
||||
onSave?.(prevData.current, currData.current);
|
||||
}
|
||||
onClose();
|
||||
} catch (err: any) {
|
||||
showNotice("error", err.message || err.toString());
|
||||
@@ -148,10 +156,14 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
|
||||
}
|
||||
});
|
||||
|
||||
const editorResize = debounce(() => {
|
||||
editorRef.current?.layout();
|
||||
setTimeout(() => editorRef.current?.layout(), 500);
|
||||
}, 100);
|
||||
const editorResize = useMemo(
|
||||
() =>
|
||||
debounce(() => {
|
||||
editorRef.current?.layout();
|
||||
setTimeout(() => editorRef.current?.layout(), 500);
|
||||
}, 100),
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const onResized = debounce(() => {
|
||||
@@ -167,11 +179,11 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
|
||||
editorRef.current?.dispose();
|
||||
editorRef.current = undefined;
|
||||
};
|
||||
}, []);
|
||||
}, [editorResize]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} maxWidth="xl" fullWidth>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogTitle>{resolvedTitle}</DialogTitle>
|
||||
|
||||
<DialogContent
|
||||
sx={{
|
||||
|
||||
@@ -24,37 +24,50 @@ export const GroupItem = (props: Props) => {
|
||||
const sortable = type === "prepend" || type === "append";
|
||||
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
attributes: sortableAttributes,
|
||||
listeners: sortableListeners,
|
||||
setNodeRef: sortableSetNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = sortable
|
||||
? useSortable({ id: group.name })
|
||||
: {
|
||||
attributes: {},
|
||||
listeners: {},
|
||||
setNodeRef: null,
|
||||
transform: null,
|
||||
transition: null,
|
||||
isDragging: false,
|
||||
};
|
||||
} = useSortable({
|
||||
id: group.name,
|
||||
disabled: !sortable,
|
||||
});
|
||||
const dragAttributes = sortable ? sortableAttributes : undefined;
|
||||
const dragListeners = sortable ? sortableListeners : undefined;
|
||||
const dragNodeRef = sortable ? sortableSetNodeRef : undefined;
|
||||
|
||||
const [iconCachePath, setIconCachePath] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
initIconCachePath();
|
||||
}, [group]);
|
||||
let cancelled = false;
|
||||
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() {
|
||||
if (group.icon && group.icon.trim().startsWith("http")) {
|
||||
const fileName =
|
||||
group.name.replaceAll(" ", "") + "-" + getFileName(group.icon);
|
||||
const iconPath = await downloadIconCache(group.icon, fileName);
|
||||
setIconCachePath(convertFileSrc(iconPath));
|
||||
}
|
||||
}
|
||||
void initIconCachePath();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [group.icon, group.name]);
|
||||
|
||||
function getFileName(url: string) {
|
||||
return url.substring(url.lastIndexOf("/") + 1);
|
||||
@@ -108,9 +121,9 @@ export const GroupItem = (props: Props) => {
|
||||
/>
|
||||
)}
|
||||
<ListItemText
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
ref={setNodeRef}
|
||||
{...(dragAttributes ?? {})}
|
||||
{...(dragListeners ?? {})}
|
||||
ref={dragNodeRef}
|
||||
sx={{ cursor: sortable ? "move" : "" }}
|
||||
primary={
|
||||
<StyledPrimary
|
||||
@@ -133,11 +146,13 @@ export const GroupItem = (props: Props) => {
|
||||
</Box>
|
||||
</ListItemTextChild>
|
||||
}
|
||||
secondaryTypographyProps={{
|
||||
sx: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
color: "#ccc",
|
||||
slotProps={{
|
||||
secondary: {
|
||||
sx: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
color: "#ccc",
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -36,7 +36,13 @@ import {
|
||||
cancelIdleCallback,
|
||||
} from "foxact/request-idle-callback";
|
||||
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 { useTranslation } from "react-i18next";
|
||||
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 obj = yaml.load(data) as ISeqProfileConfig | null;
|
||||
|
||||
@@ -170,21 +176,20 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
|
||||
setPrevData(data);
|
||||
setCurrData(data);
|
||||
};
|
||||
}, [property]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currData === "") return;
|
||||
if (visualization !== true) return;
|
||||
if (currData === "" || visualization !== true) {
|
||||
return;
|
||||
}
|
||||
|
||||
const obj = yaml.load(currData) as {
|
||||
prepend: [];
|
||||
append: [];
|
||||
delete: [];
|
||||
} | null;
|
||||
setPrependSeq(obj?.prepend || []);
|
||||
setAppendSeq(obj?.append || []);
|
||||
setDeleteSeq(obj?.delete || []);
|
||||
}, [visualization]);
|
||||
const obj = yaml.load(currData) as ISeqProfileConfig | null;
|
||||
startTransition(() => {
|
||||
setPrependSeq(obj?.prepend ?? []);
|
||||
setAppendSeq(obj?.append ?? []);
|
||||
setDeleteSeq(obj?.delete ?? []);
|
||||
});
|
||||
}, [currData, visualization]);
|
||||
|
||||
// 优化:异步处理大数据yaml.dump,避免UI卡死
|
||||
useEffect(() => {
|
||||
@@ -210,7 +215,7 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
}
|
||||
}, [prependSeq, appendSeq, deleteSeq]);
|
||||
|
||||
const fetchProxyPolicy = async () => {
|
||||
const fetchProxyPolicy = useCallback(async () => {
|
||||
const data = await readProfileFile(profileUid);
|
||||
const proxiesData = await readProfileFile(proxiesUid);
|
||||
const originGroupsObj = yaml.load(data) as {
|
||||
@@ -246,8 +251,8 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
proxies.map((proxy: any) => proxy.name),
|
||||
),
|
||||
);
|
||||
};
|
||||
const fetchProfile = async () => {
|
||||
}, [appendSeq, deleteSeq, prependSeq, profileUid, proxiesUid]);
|
||||
const fetchProfile = useCallback(async () => {
|
||||
const data = await readProfileFile(profileUid);
|
||||
const mergeData = await readProfileFile(mergeUid);
|
||||
const globalMergeData = await readProfileFile("Merge");
|
||||
@@ -257,17 +262,17 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
} | null;
|
||||
|
||||
const originProviderObj = yaml.load(data) as {
|
||||
"proxy-providers": {};
|
||||
"proxy-providers": Record<string, unknown>;
|
||||
} | null;
|
||||
const originProvider = originProviderObj?.["proxy-providers"] || {};
|
||||
|
||||
const moreProviderObj = yaml.load(mergeData) as {
|
||||
"proxy-providers": {};
|
||||
"proxy-providers": Record<string, unknown>;
|
||||
} | null;
|
||||
const moreProvider = moreProviderObj?.["proxy-providers"] || {};
|
||||
|
||||
const globalProviderObj = yaml.load(globalMergeData) as {
|
||||
"proxy-providers": {};
|
||||
"proxy-providers": Record<string, unknown>;
|
||||
} | null;
|
||||
const globalProvider = globalProviderObj?.["proxy-providers"] || {};
|
||||
|
||||
@@ -280,21 +285,27 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
|
||||
setProxyProviderList(Object.keys(provider));
|
||||
setGroupList(originGroupsObj?.["proxy-groups"] || []);
|
||||
};
|
||||
const getInterfaceNameList = async () => {
|
||||
}, [mergeUid, profileUid]);
|
||||
const getInterfaceNameList = useCallback(async () => {
|
||||
const list = await getNetworkInterfaces();
|
||||
setInterfaceNameList(list);
|
||||
};
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
fetchProxyPolicy();
|
||||
}, [prependSeq, appendSeq, deleteSeq]);
|
||||
}, [fetchProxyPolicy]);
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
fetchContent();
|
||||
fetchProxyPolicy();
|
||||
fetchProfile();
|
||||
getInterfaceNameList();
|
||||
}, [open]);
|
||||
}, [
|
||||
fetchContent,
|
||||
fetchProfile,
|
||||
fetchProxyPolicy,
|
||||
getInterfaceNameList,
|
||||
open,
|
||||
]);
|
||||
|
||||
const validateGroup = () => {
|
||||
const group = formIns.getValues();
|
||||
@@ -811,10 +822,10 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
return x.name;
|
||||
})}
|
||||
>
|
||||
{filteredPrependSeq.map((item, index) => {
|
||||
{filteredPrependSeq.map((item) => {
|
||||
return (
|
||||
<GroupItem
|
||||
key={`${item.name}-${index}`}
|
||||
key={item.name}
|
||||
type="prepend"
|
||||
group={item}
|
||||
onDelete={() => {
|
||||
@@ -834,7 +845,7 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
const newIndex = index - shift;
|
||||
return (
|
||||
<GroupItem
|
||||
key={`${filteredGroupList[newIndex].name}-${index}`}
|
||||
key={filteredGroupList[newIndex].name}
|
||||
type={
|
||||
deleteSeq.includes(filteredGroupList[newIndex].name)
|
||||
? "delete"
|
||||
@@ -871,10 +882,10 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
return x.name;
|
||||
})}
|
||||
>
|
||||
{filteredAppendSeq.map((item, index) => {
|
||||
{filteredAppendSeq.map((item) => {
|
||||
return (
|
||||
<GroupItem
|
||||
key={`${item.name}-${index}`}
|
||||
key={item.name}
|
||||
type="append"
|
||||
group={item}
|
||||
onDelete={() => {
|
||||
|
||||
@@ -37,8 +37,8 @@ export const LogViewer = (props: Props) => {
|
||||
pb: 1,
|
||||
}}
|
||||
>
|
||||
{logInfo.map(([level, log], index) => (
|
||||
<Fragment key={index.toString()}>
|
||||
{logInfo.map(([level, log]) => (
|
||||
<Fragment key={`${level}-${log}`}>
|
||||
<Typography color="text.secondary" component="div">
|
||||
<Chip
|
||||
label={level}
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
import { open } from "@tauri-apps/plugin-shell";
|
||||
import { useLockFn } from "ahooks";
|
||||
import dayjs from "dayjs";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useReducer, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { mutate } from "swr";
|
||||
|
||||
@@ -61,6 +61,7 @@ interface Props {
|
||||
|
||||
export const ProfileItem = (props: Props) => {
|
||||
const {
|
||||
id,
|
||||
selected,
|
||||
activating,
|
||||
itemData,
|
||||
@@ -80,11 +81,11 @@ export const ProfileItem = (props: Props) => {
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({
|
||||
id: props.id,
|
||||
id,
|
||||
});
|
||||
|
||||
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 loadingCache = useLoadingCache();
|
||||
const setLoadingCache = useSetLoadingCache();
|
||||
@@ -166,37 +167,44 @@ export const ProfileItem = (props: Props) => {
|
||||
if (showNextUpdate) {
|
||||
fetchNextUpdateTime();
|
||||
}
|
||||
}, [showNextUpdate, itemData.option?.update_interval, updated]);
|
||||
}, [
|
||||
fetchNextUpdateTime,
|
||||
showNextUpdate,
|
||||
itemData.option?.update_interval,
|
||||
updated,
|
||||
]);
|
||||
|
||||
// 订阅定时器更新事件
|
||||
useEffect(() => {
|
||||
let refreshTimeout: ReturnType<typeof setTimeout> | undefined;
|
||||
// 处理定时器更新事件 - 这个事件专门用于通知定时器变更
|
||||
const handleTimerUpdate = (event: any) => {
|
||||
const updatedUid = event.payload as string;
|
||||
const handleTimerUpdate = (event: Event) => {
|
||||
const source = event as CustomEvent<string> & { payload?: string };
|
||||
const updatedUid = source.detail ?? source.payload;
|
||||
|
||||
// 只有当更新的是当前配置时才刷新显示
|
||||
if (updatedUid === itemData.uid && showNextUpdate) {
|
||||
console.log(`收到定时器更新事件: uid=${updatedUid}`);
|
||||
setTimeout(() => {
|
||||
if (refreshTimeout) {
|
||||
clearTimeout(refreshTimeout);
|
||||
}
|
||||
refreshTimeout = window.setTimeout(() => {
|
||||
fetchNextUpdateTime(true);
|
||||
}, 1000);
|
||||
}
|
||||
};
|
||||
|
||||
// 只注册定时器更新事件监听
|
||||
window.addEventListener(
|
||||
"verge://timer-updated",
|
||||
handleTimerUpdate as EventListener,
|
||||
);
|
||||
window.addEventListener("verge://timer-updated", handleTimerUpdate);
|
||||
|
||||
return () => {
|
||||
if (refreshTimeout) {
|
||||
clearTimeout(refreshTimeout);
|
||||
}
|
||||
// 清理事件监听
|
||||
window.removeEventListener(
|
||||
"verge://timer-updated",
|
||||
handleTimerUpdate as EventListener,
|
||||
);
|
||||
window.removeEventListener("verge://timer-updated", handleTimerUpdate);
|
||||
};
|
||||
}, [showNextUpdate, itemData.uid]);
|
||||
}, [fetchNextUpdateTime, itemData.uid, showNextUpdate]);
|
||||
|
||||
// local file mode
|
||||
// remote file mode
|
||||
@@ -217,11 +225,11 @@ export const ProfileItem = (props: Props) => {
|
||||
const loading = loadingCache[itemData.uid] ?? false;
|
||||
|
||||
// interval update fromNow field
|
||||
const [, setRefresh] = useState({});
|
||||
const [, forceRefresh] = useReducer((value: number) => value + 1, 0);
|
||||
useEffect(() => {
|
||||
if (!hasUrl) return;
|
||||
|
||||
let timer: any = null;
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
const handler = () => {
|
||||
const now = Date.now();
|
||||
@@ -232,7 +240,7 @@ export const ProfileItem = (props: Props) => {
|
||||
const wait = now - lastUpdate >= 36e5 ? 30e5 : 5e4;
|
||||
|
||||
timer = setTimeout(() => {
|
||||
setRefresh({});
|
||||
forceRefresh();
|
||||
handler();
|
||||
}, wait);
|
||||
};
|
||||
@@ -240,9 +248,12 @@ export const ProfileItem = (props: Props) => {
|
||||
handler();
|
||||
|
||||
return () => {
|
||||
if (timer) clearTimeout(timer);
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
timer = undefined;
|
||||
}
|
||||
};
|
||||
}, [hasUrl, updated]);
|
||||
}, [forceRefresh, hasUrl, updated]);
|
||||
|
||||
const [fileOpen, setFileOpen] = useState(false);
|
||||
const [rulesOpen, setRulesOpen] = useState(false);
|
||||
@@ -382,7 +393,9 @@ export const ProfileItem = (props: Props) => {
|
||||
setAnchorEl(null);
|
||||
if (batchMode) {
|
||||
// If in batch mode, just toggle selection instead of showing delete confirmation
|
||||
onSelectionChange && onSelectionChange();
|
||||
if (onSelectionChange) {
|
||||
onSelectionChange();
|
||||
}
|
||||
} else {
|
||||
setConfirmOpen(true);
|
||||
}
|
||||
@@ -426,7 +439,9 @@ export const ProfileItem = (props: Props) => {
|
||||
setAnchorEl(null);
|
||||
if (batchMode) {
|
||||
// If in batch mode, just toggle selection instead of showing delete confirmation
|
||||
onSelectionChange && onSelectionChange();
|
||||
if (onSelectionChange) {
|
||||
onSelectionChange();
|
||||
}
|
||||
} else {
|
||||
setConfirmOpen(true);
|
||||
}
|
||||
@@ -444,14 +459,16 @@ export const ProfileItem = (props: Props) => {
|
||||
|
||||
// 监听自动更新事件
|
||||
useEffect(() => {
|
||||
const handleUpdateStarted = (event: CustomEvent) => {
|
||||
if (event.detail.uid === itemData.uid) {
|
||||
const handleUpdateStarted = (event: Event) => {
|
||||
const customEvent = event as CustomEvent<{ uid?: string }>;
|
||||
if (customEvent.detail?.uid === itemData.uid) {
|
||||
setLoadingCache((cache) => ({ ...cache, [itemData.uid]: true }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateCompleted = (event: CustomEvent) => {
|
||||
if (event.detail.uid === itemData.uid) {
|
||||
const handleUpdateCompleted = (event: Event) => {
|
||||
const customEvent = event as CustomEvent<{ uid?: string }>;
|
||||
if (customEvent.detail?.uid === itemData.uid) {
|
||||
setLoadingCache((cache) => ({ ...cache, [itemData.uid]: false }));
|
||||
// 更新完成后刷新显示
|
||||
if (showNextUpdate) {
|
||||
@@ -461,27 +478,18 @@ export const ProfileItem = (props: Props) => {
|
||||
};
|
||||
|
||||
// 注册事件监听
|
||||
window.addEventListener(
|
||||
"profile-update-started",
|
||||
handleUpdateStarted as EventListener,
|
||||
);
|
||||
window.addEventListener(
|
||||
"profile-update-completed",
|
||||
handleUpdateCompleted as EventListener,
|
||||
);
|
||||
window.addEventListener("profile-update-started", handleUpdateStarted);
|
||||
window.addEventListener("profile-update-completed", handleUpdateCompleted);
|
||||
|
||||
return () => {
|
||||
// 清理事件监听
|
||||
window.removeEventListener(
|
||||
"profile-update-started",
|
||||
handleUpdateStarted as EventListener,
|
||||
);
|
||||
window.removeEventListener("profile-update-started", handleUpdateStarted);
|
||||
window.removeEventListener(
|
||||
"profile-update-completed",
|
||||
handleUpdateCompleted as EventListener,
|
||||
handleUpdateCompleted,
|
||||
);
|
||||
};
|
||||
}, [itemData.uid, showNextUpdate]);
|
||||
}, [fetchNextUpdateTime, itemData.uid, setLoadingCache, showNextUpdate]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
@@ -506,7 +514,7 @@ export const ProfileItem = (props: Props) => {
|
||||
onContextMenu={(event) => {
|
||||
const { clientX, clientY } = event;
|
||||
setPosition({ top: clientY, left: clientX });
|
||||
setAnchorEl(event.currentTarget);
|
||||
setAnchorEl(event.currentTarget as HTMLElement);
|
||||
event.preventDefault();
|
||||
}}
|
||||
>
|
||||
@@ -543,7 +551,9 @@ export const ProfileItem = (props: Props) => {
|
||||
sx={{ padding: "2px", marginRight: "4px", marginLeft: "-8px" }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onSelectionChange && onSelectionChange();
|
||||
if (onSelectionChange) {
|
||||
onSelectionChange();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isSelected ? (
|
||||
@@ -737,7 +747,7 @@ export const ProfileItem = (props: Props) => {
|
||||
schema="clash"
|
||||
onSave={async (prev, curr) => {
|
||||
await saveProfileFile(uid, curr ?? "");
|
||||
onSave && onSave(prev, curr);
|
||||
onSave?.(prev, curr);
|
||||
}}
|
||||
onClose={() => setFileOpen(false)}
|
||||
/>
|
||||
@@ -783,7 +793,7 @@ export const ProfileItem = (props: Props) => {
|
||||
schema="clash"
|
||||
onSave={async (prev, curr) => {
|
||||
await saveProfileFile(option?.merge ?? "", curr ?? "");
|
||||
onSave && onSave(prev, curr);
|
||||
onSave?.(prev, curr);
|
||||
}}
|
||||
onClose={() => setMergeOpen(false)}
|
||||
/>
|
||||
@@ -795,7 +805,7 @@ export const ProfileItem = (props: Props) => {
|
||||
language="javascript"
|
||||
onSave={async (prev, curr) => {
|
||||
await saveProfileFile(option?.script ?? "", curr ?? "");
|
||||
onSave && onSave(prev, curr);
|
||||
onSave?.(prev, curr);
|
||||
}}
|
||||
onClose={() => setScriptOpen(false)}
|
||||
/>
|
||||
|
||||
@@ -25,12 +25,15 @@ interface Props {
|
||||
onSave?: (prev?: string, curr?: string) => void;
|
||||
}
|
||||
|
||||
const EMPTY_LOG_INFO: [string, string][] = [];
|
||||
|
||||
// profile enhanced item
|
||||
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 [anchorEl, setAnchorEl] = useState<any>(null);
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
|
||||
const [position, setPosition] = useState({ left: 0, top: 0 });
|
||||
const [fileOpen, setFileOpen] = 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 = [
|
||||
{ label: "Edit File", handler: onEditFile },
|
||||
@@ -71,7 +74,7 @@ export const ProfileMore = (props: Props) => {
|
||||
onContextMenu={(event) => {
|
||||
const { clientX, clientY } = event;
|
||||
setPosition({ top: clientY, left: clientX });
|
||||
setAnchorEl(event.currentTarget);
|
||||
setAnchorEl(event.currentTarget as HTMLElement);
|
||||
event.preventDefault();
|
||||
}}
|
||||
>
|
||||
@@ -173,7 +176,7 @@ export const ProfileMore = (props: Props) => {
|
||||
schema={id === "Merge" ? "clash" : undefined}
|
||||
onSave={async (prev, curr) => {
|
||||
await saveProfileFile(id, curr ?? "");
|
||||
onSave && onSave(prev, curr);
|
||||
onSave?.(prev, curr);
|
||||
}}
|
||||
onClose={() => setFileOpen(false)}
|
||||
/>
|
||||
@@ -181,7 +184,7 @@ export const ProfileMore = (props: Props) => {
|
||||
{logOpen && (
|
||||
<LogViewer
|
||||
open={logOpen}
|
||||
logInfo={logInfo}
|
||||
logInfo={entries}
|
||||
onClose={() => setLogOpen(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -45,23 +45,19 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
||||
// file input
|
||||
const fileDataRef = useRef<string | null>(null);
|
||||
|
||||
const {
|
||||
control,
|
||||
watch,
|
||||
register: _register,
|
||||
...formIns
|
||||
} = useForm<IProfileItem>({
|
||||
defaultValues: {
|
||||
type: "remote",
|
||||
name: "",
|
||||
desc: "",
|
||||
url: "",
|
||||
option: {
|
||||
with_proxy: false,
|
||||
self_proxy: false,
|
||||
const { control, watch, setValue, reset, handleSubmit, getValues } =
|
||||
useForm<IProfileItem>({
|
||||
defaultValues: {
|
||||
type: "remote",
|
||||
name: "",
|
||||
desc: "",
|
||||
url: "",
|
||||
option: {
|
||||
with_proxy: false,
|
||||
self_proxy: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
create: () => {
|
||||
@@ -71,7 +67,7 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
||||
edit: (item: IProfileItem) => {
|
||||
if (item) {
|
||||
Object.entries(item).forEach(([key, value]) => {
|
||||
formIns.setValue(key as any, value);
|
||||
setValue(key as any, value);
|
||||
});
|
||||
}
|
||||
setOpenType("edit");
|
||||
@@ -83,15 +79,15 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
||||
const withProxy = watch("option.with_proxy");
|
||||
|
||||
useEffect(() => {
|
||||
if (selfProxy) formIns.setValue("option.with_proxy", false);
|
||||
}, [selfProxy]);
|
||||
if (selfProxy) setValue("option.with_proxy", false);
|
||||
}, [selfProxy, setValue]);
|
||||
|
||||
useEffect(() => {
|
||||
if (withProxy) formIns.setValue("option.self_proxy", false);
|
||||
}, [withProxy]);
|
||||
if (withProxy) setValue("option.self_proxy", false);
|
||||
}, [setValue, withProxy]);
|
||||
|
||||
const handleOk = useLockFn(
|
||||
formIns.handleSubmit(async (form) => {
|
||||
handleSubmit(async (form) => {
|
||||
if (form.option?.timeout_seconds) {
|
||||
form.option.timeout_seconds = +form.option.timeout_seconds;
|
||||
}
|
||||
@@ -183,7 +179,7 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
||||
|
||||
// 成功后的操作
|
||||
setOpen(false);
|
||||
setTimeout(() => formIns.reset(), 500);
|
||||
setTimeout(() => reset(), 500);
|
||||
fileDataRef.current = null;
|
||||
|
||||
// 优化:UI先关闭,异步通知父组件
|
||||
@@ -202,7 +198,7 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
||||
try {
|
||||
setOpen(false);
|
||||
fileDataRef.current = null;
|
||||
setTimeout(() => formIns.reset(), 500);
|
||||
setTimeout(() => reset(), 500);
|
||||
} catch (e) {
|
||||
console.warn("[ProfileViewer] handleClose error:", e);
|
||||
}
|
||||
@@ -341,7 +337,7 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
||||
{isLocal && openType === "new" && (
|
||||
<FileInput
|
||||
onChange={(file, val) => {
|
||||
formIns.setValue("name", formIns.getValues("name") || file.name);
|
||||
setValue("name", getValues("name") || file.name);
|
||||
fileDataRef.current = val;
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -29,7 +29,13 @@ import {
|
||||
} from "@mui/material";
|
||||
import { useLockFn } from "ahooks";
|
||||
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 MonacoEditor from "react-monaco-editor";
|
||||
import { Virtuoso } from "react-virtuoso";
|
||||
@@ -145,7 +151,9 @@ export const ProxiesEditorViewer = (props: Props) => {
|
||||
const lines = uris.trim().split("\n");
|
||||
let idx = 0;
|
||||
const batchSize = 50;
|
||||
function parseBatch() {
|
||||
let parseTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
const parseBatch = () => {
|
||||
const end = Math.min(idx + batchSize, lines.length);
|
||||
for (; idx < end; idx++) {
|
||||
const uri = lines[idx];
|
||||
@@ -165,14 +173,18 @@ export const ProxiesEditorViewer = (props: Props) => {
|
||||
}
|
||||
}
|
||||
if (idx < lines.length) {
|
||||
setTimeout(parseBatch, 0);
|
||||
parseTimer = window.setTimeout(parseBatch, 0);
|
||||
} else {
|
||||
if (parseTimer) {
|
||||
clearTimeout(parseTimer);
|
||||
parseTimer = undefined;
|
||||
}
|
||||
cb(proxies);
|
||||
}
|
||||
}
|
||||
};
|
||||
parseBatch();
|
||||
};
|
||||
const fetchProfile = async () => {
|
||||
const fetchProfile = useCallback(async () => {
|
||||
const data = await readProfileFile(profileUid);
|
||||
|
||||
const originProxiesObj = yaml.load(data) as {
|
||||
@@ -180,9 +192,9 @@ export const ProxiesEditorViewer = (props: Props) => {
|
||||
} | null;
|
||||
|
||||
setProxyList(originProxiesObj?.proxies || []);
|
||||
};
|
||||
}, [profileUid]);
|
||||
|
||||
const fetchContent = async () => {
|
||||
const fetchContent = useCallback(async () => {
|
||||
const data = await readProfileFile(property);
|
||||
const obj = yaml.load(data) as ISeqProfileConfig | null;
|
||||
|
||||
@@ -192,50 +204,61 @@ export const ProxiesEditorViewer = (props: Props) => {
|
||||
|
||||
setPrevData(data);
|
||||
setCurrData(data);
|
||||
};
|
||||
}, [property]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currData === "") return;
|
||||
if (visualization !== true) 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);
|
||||
}
|
||||
if (currData === "" || visualization !== true) {
|
||||
return;
|
||||
}
|
||||
|
||||
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]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
fetchContent();
|
||||
fetchProfile();
|
||||
}, [open]);
|
||||
}, [fetchContent, fetchProfile, open]);
|
||||
|
||||
const handleSave = useLockFn(async () => {
|
||||
try {
|
||||
@@ -357,10 +380,10 @@ export const ProxiesEditorViewer = (props: Props) => {
|
||||
return x.name;
|
||||
})}
|
||||
>
|
||||
{filteredPrependSeq.map((item, index) => {
|
||||
{filteredPrependSeq.map((item) => {
|
||||
return (
|
||||
<ProxyItem
|
||||
key={`${item.name}-${index}`}
|
||||
key={item.name}
|
||||
type="prepend"
|
||||
proxy={item}
|
||||
onDelete={() => {
|
||||
@@ -380,7 +403,7 @@ export const ProxiesEditorViewer = (props: Props) => {
|
||||
const newIndex = index - shift;
|
||||
return (
|
||||
<ProxyItem
|
||||
key={`${filteredProxyList[newIndex].name}-${index}`}
|
||||
key={filteredProxyList[newIndex].name}
|
||||
type={
|
||||
deleteSeq.includes(filteredProxyList[newIndex].name)
|
||||
? "delete"
|
||||
@@ -417,10 +440,10 @@ export const ProxiesEditorViewer = (props: Props) => {
|
||||
return x.name;
|
||||
})}
|
||||
>
|
||||
{filteredAppendSeq.map((item, index) => {
|
||||
{filteredAppendSeq.map((item) => {
|
||||
return (
|
||||
<ProxyItem
|
||||
key={`${item.name}-${index}`}
|
||||
key={item.name}
|
||||
type="append"
|
||||
proxy={item}
|
||||
onDelete={() => {
|
||||
|
||||
@@ -21,22 +21,19 @@ export const ProxyItem = (props: Props) => {
|
||||
const sortable = type === "prepend" || type === "append";
|
||||
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
attributes: sortableAttributes,
|
||||
listeners: sortableListeners,
|
||||
setNodeRef: sortableSetNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = sortable
|
||||
? useSortable({ id: proxy.name })
|
||||
: {
|
||||
attributes: {},
|
||||
listeners: {},
|
||||
setNodeRef: null,
|
||||
transform: null,
|
||||
transition: null,
|
||||
isDragging: false,
|
||||
};
|
||||
} = useSortable({
|
||||
id: proxy.name,
|
||||
disabled: !sortable,
|
||||
});
|
||||
const dragAttributes = sortable ? sortableAttributes : undefined;
|
||||
const dragListeners = sortable ? sortableListeners : undefined;
|
||||
const dragNodeRef = sortable ? sortableSetNodeRef : undefined;
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
@@ -60,9 +57,9 @@ export const ProxyItem = (props: Props) => {
|
||||
})}
|
||||
>
|
||||
<ListItemText
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
ref={setNodeRef}
|
||||
{...(dragAttributes ?? {})}
|
||||
{...(dragListeners ?? {})}
|
||||
ref={dragNodeRef}
|
||||
sx={{ cursor: sortable ? "move" : "" }}
|
||||
primary={
|
||||
<StyledPrimary
|
||||
@@ -86,11 +83,13 @@ export const ProxyItem = (props: Props) => {
|
||||
</Box>
|
||||
</ListItemTextChild>
|
||||
}
|
||||
secondaryTypographyProps={{
|
||||
sx: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
color: "#ccc",
|
||||
slotProps={{
|
||||
secondary: {
|
||||
sx: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
color: "#ccc",
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -95,11 +95,13 @@ export const RuleItem = (props: Props) => {
|
||||
</StyledSubtitle>
|
||||
</ListItemTextChild>
|
||||
}
|
||||
secondaryTypographyProps={{
|
||||
sx: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
color: "#ccc",
|
||||
slotProps={{
|
||||
secondary: {
|
||||
sx: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
color: "#ccc",
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -31,7 +31,13 @@ import {
|
||||
} from "@mui/material";
|
||||
import { useLockFn } from "ahooks";
|
||||
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 MonacoEditor from "react-monaco-editor";
|
||||
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 obj = yaml.load(data) as ISeqProfileConfig | null;
|
||||
|
||||
@@ -315,42 +321,57 @@ export const RulesEditorViewer = (props: Props) => {
|
||||
|
||||
setPrevData(data);
|
||||
setCurrData(data);
|
||||
};
|
||||
}, [property]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currData === "") return;
|
||||
if (visualization !== true) return;
|
||||
if (currData === "" || visualization !== true) {
|
||||
return;
|
||||
}
|
||||
|
||||
const obj = yaml.load(currData) as ISeqProfileConfig | null;
|
||||
setPrependSeq(obj?.prepend || []);
|
||||
setAppendSeq(obj?.append || []);
|
||||
setDeleteSeq(obj?.delete || []);
|
||||
}, [visualization]);
|
||||
startTransition(() => {
|
||||
setPrependSeq(obj?.prepend ?? []);
|
||||
setAppendSeq(obj?.append ?? []);
|
||||
setDeleteSeq(obj?.delete ?? []);
|
||||
});
|
||||
}, [currData, visualization]);
|
||||
|
||||
// 优化:异步处理大数据yaml.dump,避免UI卡死
|
||||
useEffect(() => {
|
||||
if (prependSeq && appendSeq && deleteSeq) {
|
||||
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");
|
||||
}
|
||||
};
|
||||
if (window.requestIdleCallback) {
|
||||
window.requestIdleCallback(serialize);
|
||||
} else {
|
||||
setTimeout(serialize, 0);
|
||||
}
|
||||
if (!(prependSeq && appendSeq && deleteSeq)) {
|
||||
return;
|
||||
}
|
||||
|
||||
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]);
|
||||
|
||||
const fetchProfile = async () => {
|
||||
const fetchProfile = useCallback(async () => {
|
||||
const data = await readProfileFile(profileUid); // 原配置文件
|
||||
const groupsData = await readProfileFile(groupsUid); // groups配置文件
|
||||
const mergeData = await readProfileFile(mergeUid); // merge配置文件
|
||||
@@ -358,13 +379,25 @@ export const RulesEditorViewer = (props: Props) => {
|
||||
|
||||
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 moreGroupsObj = yaml.load(groupsData) as ISeqProfileConfig | null;
|
||||
const morePrependGroups = moreGroupsObj?.["prepend"] || [];
|
||||
const moreAppendGroups = moreGroupsObj?.["append"] || [];
|
||||
const moreDeleteGroups =
|
||||
moreGroupsObj?.["delete"] || ([] as string[] | { name: string }[]);
|
||||
const rawPrependGroups = moreGroupsObj?.["prepend"];
|
||||
const morePrependGroups = Array.isArray(rawPrependGroups)
|
||||
? (rawPrependGroups as IProxyGroupConfig[])
|
||||
: [];
|
||||
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(
|
||||
originGroups.filter((group: any) => {
|
||||
if (group.name) {
|
||||
@@ -376,14 +409,16 @@ export const RulesEditorViewer = (props: Props) => {
|
||||
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 moreRuleSetObj = yaml.load(mergeData) as {
|
||||
"rule-providers": {};
|
||||
"rule-providers": Record<string, unknown>;
|
||||
} | null;
|
||||
const moreRuleSet = moreRuleSetObj?.["rule-providers"] || {};
|
||||
const globalRuleSetObj = yaml.load(globalMergeData) as {
|
||||
"rule-providers": {};
|
||||
"rule-providers": Record<string, unknown>;
|
||||
} | null;
|
||||
const globalRuleSet = globalRuleSetObj?.["rule-providers"] || {};
|
||||
const ruleSet = Object.assign(
|
||||
@@ -393,12 +428,16 @@ export const RulesEditorViewer = (props: Props) => {
|
||||
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 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 globalSubRuleObj = yaml.load(globalMergeData) as {
|
||||
"sub-rules": {};
|
||||
"sub-rules": Record<string, unknown>;
|
||||
} | null;
|
||||
const globalSubRule = globalSubRuleObj?.["sub-rules"] || {};
|
||||
const subRule = Object.assign(
|
||||
@@ -413,13 +452,13 @@ export const RulesEditorViewer = (props: Props) => {
|
||||
setRuleSetList(Object.keys(ruleSet));
|
||||
setSubRuleList(Object.keys(subRule));
|
||||
setRuleList(rulesObj?.rules || []);
|
||||
};
|
||||
}, [groupsUid, mergeUid, profileUid]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
fetchContent();
|
||||
fetchProfile();
|
||||
}, [open]);
|
||||
}, [fetchContent, fetchProfile, open]);
|
||||
|
||||
const validateRule = () => {
|
||||
if ((ruleType.required ?? true) && !ruleContent) {
|
||||
@@ -626,10 +665,10 @@ export const RulesEditorViewer = (props: Props) => {
|
||||
return x;
|
||||
})}
|
||||
>
|
||||
{filteredPrependSeq.map((item, index) => {
|
||||
{filteredPrependSeq.map((item) => {
|
||||
return (
|
||||
<RuleItem
|
||||
key={`${item}-${index}`}
|
||||
key={item}
|
||||
type="prepend"
|
||||
ruleRaw={item}
|
||||
onDelete={() => {
|
||||
@@ -647,7 +686,7 @@ export const RulesEditorViewer = (props: Props) => {
|
||||
const newIndex = index - shift;
|
||||
return (
|
||||
<RuleItem
|
||||
key={`${filteredRuleList[newIndex]}-${index}`}
|
||||
key={filteredRuleList[newIndex]}
|
||||
type={
|
||||
deleteSeq.includes(filteredRuleList[newIndex])
|
||||
? "delete"
|
||||
@@ -682,10 +721,10 @@ export const RulesEditorViewer = (props: Props) => {
|
||||
return x;
|
||||
})}
|
||||
>
|
||||
{filteredAppendSeq.map((item, index) => {
|
||||
{filteredAppendSeq.map((item) => {
|
||||
return (
|
||||
<RuleItem
|
||||
key={`${item}-${index}`}
|
||||
key={item}
|
||||
type="append"
|
||||
ruleRaw={item}
|
||||
onDelete={() => {
|
||||
|
||||
Reference in New Issue
Block a user