fix(groups-editor): persist deletions/restorations and normalize YAML
Prevent state resets while editing groups so deletions/restorations persist instead of being overwritten.
Ensure YAML is normalized and the latest visual state is saved.
- Add `normalizeDeleteSeq` to handle legacy `{name: ...}` entries and `buildGroupsYaml` for consistent serialization.
- Guard reassigning `deleteSeq` unless normalized value changes to avoid effect loops.
- Normalize proxy deletions and deduplicate policy names without extra backend writes.
- Split “on open” effect from proxy-policy refresh; toggling delete no longer triggers `fetchContent()`.
- Write composed YAML in `handleSave`, keep `currData`/`prevData` aligned, and provide accurate payloads to `onSave`.
This commit is contained in:
@@ -73,6 +73,50 @@ interface Props {
|
|||||||
|
|
||||||
const builtinProxyPolicies = ["DIRECT", "REJECT", "REJECT-DROP", "PASS"];
|
const builtinProxyPolicies = ["DIRECT", "REJECT", "REJECT-DROP", "PASS"];
|
||||||
|
|
||||||
|
const normalizeDeleteSeq = (input?: unknown): string[] => {
|
||||||
|
if (!Array.isArray(input)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const names = input
|
||||||
|
.map((item) => {
|
||||||
|
if (typeof item === "string") {
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
item &&
|
||||||
|
typeof item === "object" &&
|
||||||
|
"name" in item &&
|
||||||
|
typeof (item as { name: unknown }).name === "string"
|
||||||
|
) {
|
||||||
|
return (item as { name: string }).name;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
})
|
||||||
|
.filter(
|
||||||
|
(name): name is string => typeof name === "string" && name.length > 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
return Array.from(new Set(names));
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildGroupsYaml = (
|
||||||
|
prepend: IProxyGroupConfig[],
|
||||||
|
append: IProxyGroupConfig[],
|
||||||
|
deleteList: string[],
|
||||||
|
) => {
|
||||||
|
return yaml.dump(
|
||||||
|
{
|
||||||
|
prepend,
|
||||||
|
append,
|
||||||
|
delete: deleteList,
|
||||||
|
},
|
||||||
|
{ forceQuotes: true },
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const GroupsEditorViewer = (props: Props) => {
|
export const GroupsEditorViewer = (props: Props) => {
|
||||||
const { mergeUid, proxiesUid, profileUid, property, open, onClose, onSave } =
|
const { mergeUid, proxiesUid, profileUid, property, open, onClose, onSave } =
|
||||||
props;
|
props;
|
||||||
@@ -172,7 +216,16 @@ export const GroupsEditorViewer = (props: Props) => {
|
|||||||
|
|
||||||
setPrependSeq(obj?.prepend || []);
|
setPrependSeq(obj?.prepend || []);
|
||||||
setAppendSeq(obj?.append || []);
|
setAppendSeq(obj?.append || []);
|
||||||
setDeleteSeq(obj?.delete || []);
|
setDeleteSeq((prev) => {
|
||||||
|
const normalized = normalizeDeleteSeq(obj?.delete);
|
||||||
|
if (
|
||||||
|
normalized.length === prev.length &&
|
||||||
|
normalized.every((item, index) => item === prev[index])
|
||||||
|
) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
});
|
||||||
|
|
||||||
setPrevData(data);
|
setPrevData(data);
|
||||||
setCurrData(data);
|
setCurrData(data);
|
||||||
@@ -187,7 +240,16 @@ export const GroupsEditorViewer = (props: Props) => {
|
|||||||
startTransition(() => {
|
startTransition(() => {
|
||||||
setPrependSeq(obj?.prepend ?? []);
|
setPrependSeq(obj?.prepend ?? []);
|
||||||
setAppendSeq(obj?.append ?? []);
|
setAppendSeq(obj?.append ?? []);
|
||||||
setDeleteSeq(obj?.delete ?? []);
|
setDeleteSeq((prev) => {
|
||||||
|
const normalized = normalizeDeleteSeq(obj?.delete);
|
||||||
|
if (
|
||||||
|
normalized.length === prev.length &&
|
||||||
|
normalized.every((item, index) => item === prev[index])
|
||||||
|
) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}, [currData, visualization]);
|
}, [currData, visualization]);
|
||||||
|
|
||||||
@@ -196,12 +258,7 @@ export const GroupsEditorViewer = (props: Props) => {
|
|||||||
if (prependSeq && appendSeq && deleteSeq) {
|
if (prependSeq && appendSeq && deleteSeq) {
|
||||||
const serialize = () => {
|
const serialize = () => {
|
||||||
try {
|
try {
|
||||||
setCurrData(
|
setCurrData(buildGroupsYaml(prependSeq, appendSeq, deleteSeq));
|
||||||
yaml.dump(
|
|
||||||
{ prepend: prependSeq, append: appendSeq, delete: deleteSeq },
|
|
||||||
{ forceQuotes: true },
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("[GroupsEditorViewer] yaml.dump failed:", e);
|
console.warn("[GroupsEditorViewer] yaml.dump failed:", e);
|
||||||
// 防止异常导致UI卡死
|
// 防止异常导致UI卡死
|
||||||
@@ -227,30 +284,37 @@ export const GroupsEditorViewer = (props: Props) => {
|
|||||||
const moreProxiesObj = yaml.load(proxiesData) as ISeqProfileConfig | null;
|
const moreProxiesObj = yaml.load(proxiesData) as ISeqProfileConfig | null;
|
||||||
const morePrependProxies = moreProxiesObj?.prepend || [];
|
const morePrependProxies = moreProxiesObj?.prepend || [];
|
||||||
const moreAppendProxies = moreProxiesObj?.append || [];
|
const moreAppendProxies = moreProxiesObj?.append || [];
|
||||||
const moreDeleteProxies =
|
const moreDeleteProxies = normalizeDeleteSeq(moreProxiesObj?.delete);
|
||||||
moreProxiesObj?.delete || ([] as string[] | { name: string }[]);
|
|
||||||
|
|
||||||
const proxies = morePrependProxies.concat(
|
const proxies = morePrependProxies.concat(
|
||||||
originProxies.filter((proxy: any) => {
|
originProxies.filter((proxy: any) => {
|
||||||
if (proxy.name) {
|
const proxyName =
|
||||||
return !moreDeleteProxies.includes(proxy.name);
|
typeof proxy === "string"
|
||||||
} else {
|
? proxy
|
||||||
return !moreDeleteProxies.includes(proxy);
|
: (proxy?.name as string | undefined);
|
||||||
}
|
return proxyName ? !moreDeleteProxies.includes(proxyName) : true;
|
||||||
}),
|
}),
|
||||||
moreAppendProxies,
|
moreAppendProxies,
|
||||||
);
|
);
|
||||||
|
|
||||||
setProxyPolicyList(
|
const proxyNames = proxies
|
||||||
builtinProxyPolicies.concat(
|
.map((proxy: any) =>
|
||||||
prependSeq.map((group: IProxyGroupConfig) => group.name),
|
typeof proxy === "string" ? proxy : (proxy?.name as string | undefined),
|
||||||
originGroupsObj?.["proxy-groups"]
|
)
|
||||||
.map((group: IProxyGroupConfig) => group.name)
|
.filter(
|
||||||
.filter((name) => !deleteSeq.includes(name)) || [],
|
(name): name is string => typeof name === "string" && name.length > 0,
|
||||||
appendSeq.map((group: IProxyGroupConfig) => group.name),
|
|
||||||
proxies.map((proxy: any) => proxy.name),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const computedPolicyList = builtinProxyPolicies.concat(
|
||||||
|
prependSeq.map((group: IProxyGroupConfig) => group.name),
|
||||||
|
(originGroupsObj?.["proxy-groups"] || [])
|
||||||
|
.map((group: IProxyGroupConfig) => group.name)
|
||||||
|
.filter((name) => !deleteSeq.includes(name)),
|
||||||
|
appendSeq.map((group: IProxyGroupConfig) => group.name),
|
||||||
|
proxyNames,
|
||||||
|
);
|
||||||
|
|
||||||
|
setProxyPolicyList(Array.from(new Set(computedPolicyList)));
|
||||||
}, [appendSeq, deleteSeq, prependSeq, profileUid, proxiesUid]);
|
}, [appendSeq, deleteSeq, prependSeq, profileUid, proxiesUid]);
|
||||||
const fetchProfile = useCallback(async () => {
|
const fetchProfile = useCallback(async () => {
|
||||||
const data = await readProfileFile(profileUid);
|
const data = await readProfileFile(profileUid);
|
||||||
@@ -291,21 +355,16 @@ export const GroupsEditorViewer = (props: Props) => {
|
|||||||
setInterfaceNameList(list);
|
setInterfaceNameList(list);
|
||||||
}, []);
|
}, []);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!open) return;
|
||||||
fetchProxyPolicy();
|
fetchProxyPolicy();
|
||||||
}, [fetchProxyPolicy]);
|
}, [fetchProxyPolicy, open]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) return;
|
if (!open) return;
|
||||||
fetchContent();
|
fetchContent();
|
||||||
fetchProxyPolicy();
|
|
||||||
fetchProfile();
|
fetchProfile();
|
||||||
getInterfaceNameList();
|
getInterfaceNameList();
|
||||||
}, [
|
}, [fetchContent, fetchProfile, getInterfaceNameList, open]);
|
||||||
fetchContent,
|
|
||||||
fetchProfile,
|
|
||||||
fetchProxyPolicy,
|
|
||||||
getInterfaceNameList,
|
|
||||||
open,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const validateGroup = () => {
|
const validateGroup = () => {
|
||||||
const group = formIns.getValues();
|
const group = formIns.getValues();
|
||||||
@@ -316,9 +375,18 @@ export const GroupsEditorViewer = (props: Props) => {
|
|||||||
|
|
||||||
const handleSave = useLockFn(async () => {
|
const handleSave = useLockFn(async () => {
|
||||||
try {
|
try {
|
||||||
await saveProfileFile(property, currData);
|
const nextData = visualization
|
||||||
|
? buildGroupsYaml(prependSeq, appendSeq, deleteSeq)
|
||||||
|
: currData;
|
||||||
|
|
||||||
|
if (visualization) {
|
||||||
|
setCurrData(nextData);
|
||||||
|
}
|
||||||
|
|
||||||
|
await saveProfileFile(property, nextData);
|
||||||
showNotice("success", t("Saved Successfully"));
|
showNotice("success", t("Saved Successfully"));
|
||||||
onSave?.(prevData, currData);
|
setPrevData(nextData);
|
||||||
|
onSave?.(prevData, nextData);
|
||||||
onClose();
|
onClose();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
showNotice("error", err.toString());
|
showNotice("error", err.toString());
|
||||||
|
|||||||
Reference in New Issue
Block a user