mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-21 09:42:09 +08:00
refactor: structure config validation outcomes
- Replace boolean/string validation results with structured ValidationOutcome values across config validation, profile switching, DNS validation, and script validation. Callers can now distinguish valid, invalid, skipped, and busy states, with invalid results carrying an error kind and message. - Route core config updates through structured outcomes and checked forced updates. Generation and validation failures now discard runtime drafts and surface validation failures instead of relying on empty success messages. - Preserve existing frontend ergonomics by keeping boolean service wrappers where callers only need success, while direct DNS and script validation consumers receive ValidationOutcome. Failed profile editor saves reload the backend-restored file content and synchronize Monaco models so reopened editors show the restored state.
This commit is contained in:
@@ -70,6 +70,13 @@ export const EditorViewer = ({
|
||||
const resolvedTitle = title ?? t('profiles.components.menu.editFile')
|
||||
const disableSave = loading || saveDisabled || dirty === false
|
||||
|
||||
const syncEditorValue = useCallback(() => {
|
||||
const model = editorRef.current?.getModel()
|
||||
if (model && model.getValue() !== value) {
|
||||
model.setValue(value)
|
||||
}
|
||||
}, [value])
|
||||
|
||||
const syncMaximizedState = useCallback(async () => {
|
||||
try {
|
||||
setIsMaximized(await appWindow.isMaximized())
|
||||
@@ -149,6 +156,11 @@ export const EditorViewer = ({
|
||||
void syncMaximizedState()
|
||||
}, [open, syncMaximizedState])
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || loading) return
|
||||
syncEditorValue()
|
||||
}, [loading, open, syncEditorValue])
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
|
||||
@@ -209,6 +221,7 @@ export const EditorViewer = ({
|
||||
beforeMount={beforeEditorMount}
|
||||
onMount={(editorInstance) => {
|
||||
editorRef.current = editorInstance
|
||||
syncEditorValue()
|
||||
}}
|
||||
onChange={(nextValue) => onChange?.(nextValue ?? '')}
|
||||
onValidate={onValidate}
|
||||
|
||||
@@ -508,7 +508,11 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
setCurrData(nextData)
|
||||
}
|
||||
|
||||
await saveProfileFile(property, nextData)
|
||||
if (!(await saveProfileFile(property, nextData))) {
|
||||
await fetchContent()
|
||||
onClose()
|
||||
return
|
||||
}
|
||||
showNotice.success('shared.feedback.notifications.saved')
|
||||
setPrevData(nextData)
|
||||
onSave?.(prevData, nextData)
|
||||
|
||||
@@ -630,7 +630,10 @@ export const ProfileItem = (props: Props) => {
|
||||
|
||||
const handleSaveProfileDocument = useLockFn(async () => {
|
||||
const currentValue = profileDocument.value
|
||||
await saveProfileFile(uid, currentValue)
|
||||
if (!(await saveProfileFile(uid, currentValue))) {
|
||||
await profileDocument.reload()
|
||||
return
|
||||
}
|
||||
onSave?.(profileDocument.savedValue, currentValue)
|
||||
profileDocument.markSaved(currentValue)
|
||||
})
|
||||
@@ -638,7 +641,10 @@ export const ProfileItem = (props: Props) => {
|
||||
const handleSaveMergeDocument = useLockFn(async () => {
|
||||
const mergeUid = option?.merge ?? ''
|
||||
const currentValue = mergeDocument.value
|
||||
await saveProfileFile(mergeUid, currentValue)
|
||||
if (!(await saveProfileFile(mergeUid, currentValue))) {
|
||||
await mergeDocument.reload()
|
||||
return
|
||||
}
|
||||
onSave?.(mergeDocument.savedValue, currentValue)
|
||||
mergeDocument.markSaved(currentValue)
|
||||
})
|
||||
@@ -646,7 +652,10 @@ export const ProfileItem = (props: Props) => {
|
||||
const handleSaveScriptDocument = useLockFn(async () => {
|
||||
const scriptUid = option?.script ?? ''
|
||||
const currentValue = scriptDocument.value
|
||||
await saveProfileFile(scriptUid, currentValue)
|
||||
if (!(await saveProfileFile(scriptUid, currentValue))) {
|
||||
await scriptDocument.reload()
|
||||
return
|
||||
}
|
||||
onSave?.(scriptDocument.savedValue, currentValue)
|
||||
scriptDocument.markSaved(currentValue)
|
||||
})
|
||||
|
||||
@@ -86,7 +86,10 @@ export const ProfileMore = (props: Props) => {
|
||||
|
||||
const handleSave = useLockFn(async () => {
|
||||
const currentValue = document.value
|
||||
await saveProfileFile(id, currentValue)
|
||||
if (!(await saveProfileFile(id, currentValue))) {
|
||||
await document.reload()
|
||||
return
|
||||
}
|
||||
onSave?.(document.savedValue, currentValue)
|
||||
document.markSaved(currentValue)
|
||||
})
|
||||
|
||||
@@ -355,7 +355,11 @@ export const ProxiesEditorViewer = (props: Props) => {
|
||||
|
||||
const handleSave = useLockFn(async () => {
|
||||
try {
|
||||
await saveProfileFile(property, currData)
|
||||
if (!(await saveProfileFile(property, currData))) {
|
||||
await fetchContent()
|
||||
onClose()
|
||||
return
|
||||
}
|
||||
showNotice.success('shared.feedback.notifications.saved')
|
||||
onSave?.(prevData, currData)
|
||||
onClose()
|
||||
|
||||
@@ -565,7 +565,11 @@ export const RulesEditorViewer = (props: Props) => {
|
||||
|
||||
const handleSave = useLockFn(async () => {
|
||||
try {
|
||||
await saveProfileFile(property, currData)
|
||||
if (!(await saveProfileFile(property, currData))) {
|
||||
await fetchContent()
|
||||
onClose()
|
||||
return
|
||||
}
|
||||
showNotice.success('shared.feedback.notifications.saved')
|
||||
onSave?.(prevData, currData)
|
||||
onClose()
|
||||
|
||||
@@ -528,12 +528,16 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
await invoke('save_dns_config', { dnsConfig: config })
|
||||
|
||||
// 验证配置
|
||||
const [isValid, errorMsg] = await invoke<[boolean, string]>(
|
||||
const validation = await invoke<ValidationOutcome>(
|
||||
'validate_dns_config',
|
||||
{},
|
||||
)
|
||||
|
||||
if (!isValid) {
|
||||
if (validation.status !== 'valid') {
|
||||
const errorMsg =
|
||||
validation.status === 'invalid'
|
||||
? validation.message
|
||||
: 'Configuration validation skipped'
|
||||
let cleanErrorMsg = errorMsg
|
||||
|
||||
// 提取关键错误信息
|
||||
|
||||
@@ -20,6 +20,28 @@ export const useEditorDocument = ({ open, load }: UseEditorDocumentOptions) => {
|
||||
setLoading(true)
|
||||
}, [])
|
||||
|
||||
const applyLoadedValue = useCallback(
|
||||
(nextValue: string | null | undefined) => {
|
||||
const normalized = nextValue ?? ''
|
||||
setValue(normalized)
|
||||
setSavedValue(normalized)
|
||||
return normalized
|
||||
},
|
||||
[],
|
||||
)
|
||||
|
||||
const reload = useCallback(async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
return applyLoadedValue(await load())
|
||||
} catch (error) {
|
||||
showNotice.error(error)
|
||||
throw error
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [applyLoadedValue, load])
|
||||
|
||||
useEffect(
|
||||
(signal) => {
|
||||
resetDocumentState()
|
||||
@@ -29,10 +51,7 @@ export const useEditorDocument = ({ open, load }: UseEditorDocumentOptions) => {
|
||||
load()
|
||||
.then((nextValue) => {
|
||||
if (signal.aborted) return
|
||||
|
||||
const normalized = nextValue ?? ''
|
||||
setValue(normalized)
|
||||
setSavedValue(normalized)
|
||||
applyLoadedValue(nextValue)
|
||||
})
|
||||
.catch((error) => {
|
||||
if (!signal.aborted) showNotice.error(error)
|
||||
@@ -41,7 +60,7 @@ export const useEditorDocument = ({ open, load }: UseEditorDocumentOptions) => {
|
||||
if (!signal.aborted) setLoading(false)
|
||||
})
|
||||
},
|
||||
[load, open, resetDocumentState],
|
||||
[applyLoadedValue, load, open, resetDocumentState],
|
||||
)
|
||||
|
||||
const markSaved = useCallback((nextValue: string) => {
|
||||
@@ -57,5 +76,6 @@ export const useEditorDocument = ({ open, load }: UseEditorDocumentOptions) => {
|
||||
loading,
|
||||
dirty,
|
||||
markSaved,
|
||||
reload,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -584,7 +584,7 @@ const ProfilePage = () => {
|
||||
setActivatings((prev) => [...new Set([...prev, ...currentProfiles])])
|
||||
|
||||
try {
|
||||
await enhanceProfiles()
|
||||
if (!(await enhanceProfiles())) return
|
||||
mutateLogs()
|
||||
if (notifySuccess) {
|
||||
showNotice.success(
|
||||
|
||||
+16
-4
@@ -14,11 +14,16 @@ export async function getProfiles() {
|
||||
}
|
||||
|
||||
export async function enhanceProfiles() {
|
||||
return invoke<void>('enhance_profiles')
|
||||
return (
|
||||
(await invoke<ValidationOutcome>('enhance_profiles')).status === 'valid'
|
||||
)
|
||||
}
|
||||
|
||||
export async function patchProfilesConfig(profiles: IProfilesConfig) {
|
||||
return invoke<boolean>('patch_profiles_config', { profiles })
|
||||
return (
|
||||
(await invoke<ValidationOutcome>('patch_profiles_config', { profiles }))
|
||||
.status === 'valid'
|
||||
)
|
||||
}
|
||||
|
||||
export async function createProfile(
|
||||
@@ -37,7 +42,14 @@ export async function readProfileFile(index: string) {
|
||||
}
|
||||
|
||||
export async function saveProfileFile(index: string, fileData: string) {
|
||||
return invoke<void>('save_profile_file', { index, fileData })
|
||||
return (
|
||||
(
|
||||
await invoke<ValidationOutcome>('save_profile_file', {
|
||||
index,
|
||||
fileData,
|
||||
})
|
||||
).status === 'valid'
|
||||
)
|
||||
}
|
||||
|
||||
export async function importProfile(url: string, option?: IProfileOption) {
|
||||
@@ -492,7 +504,7 @@ export async function scriptValidateNotice(status: string, msg: string) {
|
||||
}
|
||||
|
||||
export async function validateScriptFile(filePath: string) {
|
||||
return invoke<boolean>('validate_script_file', { filePath })
|
||||
return invoke<ValidationOutcome>('validate_script_file', { filePath })
|
||||
}
|
||||
|
||||
// 获取当前运行模式
|
||||
|
||||
Vendored
+5
@@ -16,6 +16,11 @@ type Platform =
|
||||
*/
|
||||
declare const OS_PLATFORM: Platform
|
||||
|
||||
type ValidationOutcome =
|
||||
| { status: 'valid' | 'busy' }
|
||||
| { status: 'invalid'; kind: string; message: string }
|
||||
| { status: 'skipped'; reason: string }
|
||||
|
||||
/**
|
||||
* Some interface for clash api
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user