mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-11-09 11:52:08 +08:00
fix(profile): project IPC inputs to consumed fields
This commit is contained in:
@@ -121,7 +121,8 @@ pub async fn reorder_profile(active_id: std::string::String, over_id: std::strin
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn create_profile(item: PrfItem, file_data: Option<std::string::String>) -> CmdResult {
|
||||
pub async fn create_profile(item: ProfileCreate, file_data: Option<std::string::String>) -> CmdResult {
|
||||
let item: PrfItem = item.into();
|
||||
let file_data = file_data.map(String::from);
|
||||
match profiles_append_item_with_filedata_safe(&item, file_data).await {
|
||||
Ok(_) => {
|
||||
@@ -129,9 +130,6 @@ pub async fn create_profile(item: PrfItem, file_data: Option<std::string::String
|
||||
.await
|
||||
.with_error_code("PROFILE_CREATE_FAILED")?;
|
||||
logging_error!(Type::Timer, Timer::global().refresh().await);
|
||||
if let Some(uid) = &item.uid {
|
||||
handle::Handle::notify_profile_changed(uid);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => Err(coded_error("PROFILE_CREATE_FAILED", err)),
|
||||
@@ -325,10 +323,82 @@ async fn perform_config_update(
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, serde::Deserialize, specta::Type)]
|
||||
#[serde(default)]
|
||||
pub struct ProfileSelection {
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub current: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Default, serde::Deserialize, specta::Type)]
|
||||
#[serde(default)]
|
||||
pub struct ProfileCreate {
|
||||
#[serde(rename = "type")]
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub itype: Option<String>,
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub name: Option<String>,
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub desc: Option<String>,
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub url: Option<String>,
|
||||
pub option: Option<PrfOption>,
|
||||
}
|
||||
|
||||
impl From<ProfileCreate> for PrfItem {
|
||||
fn from(item: ProfileCreate) -> Self {
|
||||
Self {
|
||||
itype: item.itype,
|
||||
name: item.name,
|
||||
desc: item.desc,
|
||||
url: item.url,
|
||||
option: item.option,
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, serde::Deserialize, specta::Type)]
|
||||
#[serde(default)]
|
||||
pub struct ProfilePatch {
|
||||
#[serde(rename = "type")]
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub itype: Option<String>,
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub name: Option<String>,
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub desc: Option<String>,
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub file: Option<String>,
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub url: Option<String>,
|
||||
pub selected: Option<Vec<crate::config::PrfSelected>>,
|
||||
pub extra: Option<crate::config::PrfExtra>,
|
||||
pub updated: Option<usize>,
|
||||
pub option: Option<PrfOption>,
|
||||
}
|
||||
|
||||
impl From<ProfilePatch> for PrfItem {
|
||||
fn from(patch: ProfilePatch) -> Self {
|
||||
Self {
|
||||
itype: patch.itype,
|
||||
name: patch.name,
|
||||
desc: patch.desc,
|
||||
file: patch.file,
|
||||
url: patch.url,
|
||||
selected: patch.selected,
|
||||
extra: patch.extra,
|
||||
updated: patch.updated,
|
||||
option: patch.option,
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[tracing::instrument(skip_all, level = "info", fields(target = ?profiles.current))]
|
||||
#[specta::specta]
|
||||
pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<ValidationOutcome> {
|
||||
pub async fn patch_profiles_config(profiles: ProfileSelection) -> CmdResult<ValidationOutcome> {
|
||||
if CURRENT_SWITCHING_PROFILE
|
||||
.compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
|
||||
.is_err()
|
||||
@@ -342,7 +412,12 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<ValidationO
|
||||
|
||||
let previous_profile = Config::profiles().await.data_arc().current.clone();
|
||||
|
||||
Config::profiles().await.edit_draft(|d| d.patch_config(&profiles));
|
||||
Config::profiles().await.edit_draft(|d| {
|
||||
d.patch_config(&IProfiles {
|
||||
current: profiles.current.clone(),
|
||||
items: None,
|
||||
});
|
||||
});
|
||||
|
||||
perform_config_update(target_profile, previous_profile.as_ref())
|
||||
.await
|
||||
@@ -350,16 +425,16 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<ValidationO
|
||||
}
|
||||
|
||||
pub async fn patch_profiles_config_by_profile_index(profile_index: String) -> CmdResult<ValidationOutcome> {
|
||||
let profiles = IProfiles {
|
||||
let profiles = ProfileSelection {
|
||||
current: Some(profile_index),
|
||||
items: None,
|
||||
};
|
||||
patch_profiles_config(profiles).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn patch_profile(index: std::string::String, profile: PrfItem) -> CmdResult {
|
||||
pub async fn patch_profile(index: std::string::String, profile: ProfilePatch) -> CmdResult {
|
||||
let profile: PrfItem = profile.into();
|
||||
let index: String = index.into();
|
||||
let profiles = Config::profiles().await;
|
||||
let should_refresh_timer = if let Ok(old_profile) = profiles.latest_arc().get_item(&index)
|
||||
|
||||
@@ -115,7 +115,13 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
||||
}
|
||||
|
||||
const name = form.name || `${form.type} file`
|
||||
const item = { ...form, name, option }
|
||||
const item = {
|
||||
type: form.type,
|
||||
name,
|
||||
desc: form.desc,
|
||||
option,
|
||||
...(form.type === 'remote' ? { url: form.url } : {}),
|
||||
}
|
||||
const isRemote = form.type === 'remote'
|
||||
const isUpdate = openType === 'edit'
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useCallback, useRef } from 'react'
|
||||
|
||||
import type { ProfilePatch, ProfileSelection } from '@/services/bindings'
|
||||
import { getProfiles, patchProfile, patchProfilesConfig } from '@/services/cmds'
|
||||
import { mutate } from '@/services/mutate'
|
||||
import { fetchCacheData, setCacheData, useQuery } from '@/services/query-client'
|
||||
@@ -41,7 +42,7 @@ export const useProfiles = () => {
|
||||
}, [])
|
||||
|
||||
const patchProfiles = useCallback(
|
||||
async (value: Partial<IProfilesConfig>) => {
|
||||
async (value: ProfileSelection) => {
|
||||
try {
|
||||
const result = await mutate(() => patchProfilesConfig(value), {
|
||||
id: 'patch-profiles-config',
|
||||
@@ -72,7 +73,7 @@ export const useProfiles = () => {
|
||||
)
|
||||
|
||||
const patchCurrent = useCallback(
|
||||
async (value: Partial<IProfileItem>) => {
|
||||
async (value: ProfilePatch) => {
|
||||
if (profiles?.current) {
|
||||
const uid = profiles.current
|
||||
await mutate(() => patchProfile(uid, value), {
|
||||
|
||||
@@ -140,13 +140,13 @@ export const commands = {
|
||||
__TAURI_INVOKE<NetworkInterfaceView[]>('get_network_interfaces_info'),
|
||||
getProfiles: () => __TAURI_INVOKE<IProfiles_Serialize>('get_profiles'),
|
||||
enhanceProfiles: () => __TAURI_INVOKE<ValidationOutcome>('enhance_profiles'),
|
||||
patchProfilesConfig: (profiles: IProfiles_Deserialize) =>
|
||||
patchProfilesConfig: (profiles: ProfileSelection) =>
|
||||
__TAURI_INVOKE<ValidationOutcome>('patch_profiles_config', { profiles }),
|
||||
viewProfile: (index: string) =>
|
||||
__TAURI_INVOKE<null>('view_profile', { index }),
|
||||
patchProfile: (index: string, profile: PrfItem_Deserialize) =>
|
||||
patchProfile: (index: string, profile: ProfilePatch_Deserialize) =>
|
||||
__TAURI_INVOKE<null>('patch_profile', { index, profile }),
|
||||
createProfile: (item: PrfItem_Deserialize, fileData: string | null) =>
|
||||
createProfile: (item: ProfileCreate_Deserialize, fileData: string | null) =>
|
||||
__TAURI_INVOKE<null>('create_profile', { item, fileData }),
|
||||
importProfile: (
|
||||
url: string,
|
||||
@@ -733,6 +733,24 @@ export type PrfSelected = {
|
||||
now?: string | null
|
||||
}
|
||||
|
||||
export type ProfileCreate = ProfileCreate_Serialize | ProfileCreate_Deserialize
|
||||
|
||||
export type ProfileCreate_Deserialize = {
|
||||
type?: string | null
|
||||
name?: string | null
|
||||
desc?: string | null
|
||||
url?: string | null
|
||||
option?: PrfOption_Deserialize | null
|
||||
}
|
||||
|
||||
export type ProfileCreate_Serialize = {
|
||||
type: string | null
|
||||
name: string | null
|
||||
desc: string | null
|
||||
url: string | null
|
||||
option: PrfOption_Serialize | null
|
||||
}
|
||||
|
||||
export type ProfileDnsSettings =
|
||||
| ProfileDnsSettings_Serialize
|
||||
| ProfileDnsSettings_Deserialize
|
||||
@@ -747,6 +765,36 @@ export type ProfileDnsSettings_Serialize = {
|
||||
confirmation?: string | null
|
||||
}
|
||||
|
||||
export type ProfilePatch = ProfilePatch_Serialize | ProfilePatch_Deserialize
|
||||
|
||||
export type ProfilePatch_Deserialize = {
|
||||
type?: string | null
|
||||
name?: string | null
|
||||
desc?: string | null
|
||||
file?: string | null
|
||||
url?: string | null
|
||||
selected?: PrfSelected[] | null
|
||||
extra?: PrfExtra | null
|
||||
updated?: number | null
|
||||
option?: PrfOption_Deserialize | null
|
||||
}
|
||||
|
||||
export type ProfilePatch_Serialize = {
|
||||
type: string | null
|
||||
name: string | null
|
||||
desc: string | null
|
||||
file: string | null
|
||||
url: string | null
|
||||
selected: PrfSelected[] | null
|
||||
extra: PrfExtra | null
|
||||
updated: number | null
|
||||
option: PrfOption_Serialize | null
|
||||
}
|
||||
|
||||
export type ProfileSelection = {
|
||||
current?: string | null
|
||||
}
|
||||
|
||||
export type ProfileUpdatePayload = {
|
||||
uid: string
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { commands } from './bindings'
|
||||
import { createProfile, patchProfile, patchProfilesConfig } from './cmds'
|
||||
|
||||
vi.mock('./bindings', () => ({
|
||||
commands: {
|
||||
createProfile: vi.fn(),
|
||||
patchProfile: vi.fn(),
|
||||
patchProfilesConfig: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
const wirePayload = (value: unknown) => JSON.parse(JSON.stringify(value))
|
||||
|
||||
beforeEach(() => vi.clearAllMocks())
|
||||
|
||||
describe('profile IPC projections', () => {
|
||||
it('sends only current for profile selection even with a full snapshot', async () => {
|
||||
const snapshot = { current: 'profile', items: [{ uid: 'profile' }] }
|
||||
await patchProfilesConfig(snapshot)
|
||||
expect(commands.patchProfilesConfig).toHaveBeenCalledWith({
|
||||
current: 'profile',
|
||||
})
|
||||
})
|
||||
|
||||
it('strips profile output fields while retaining explicit selections and options', async () => {
|
||||
const selected = [{ name: 'group', now: 'node' }]
|
||||
const option = {
|
||||
script: 'script',
|
||||
merge: 'merge',
|
||||
rules: 'rules',
|
||||
proxies: 'proxies',
|
||||
groups: 'groups',
|
||||
}
|
||||
const profile = {
|
||||
uid: 'profile',
|
||||
home: 'https://example.com',
|
||||
file_data: 'unused',
|
||||
name: 'renamed',
|
||||
selected,
|
||||
option,
|
||||
}
|
||||
await patchProfile('profile', profile)
|
||||
expect(
|
||||
wirePayload(vi.mocked(commands.patchProfile).mock.calls[0][1]),
|
||||
).toEqual({ name: 'renamed', selected, option })
|
||||
})
|
||||
|
||||
it('omits ignored creation fields and a local profile URL', async () => {
|
||||
const profile = {
|
||||
type: 'local',
|
||||
name: 'local',
|
||||
url: '',
|
||||
uid: 'old',
|
||||
selected: [{ name: 'group', now: 'node' }],
|
||||
}
|
||||
await createProfile(profile, 'content')
|
||||
const [payload, content] = vi.mocked(commands.createProfile).mock.calls[0]
|
||||
expect(wirePayload(payload)).toEqual({ type: 'local', name: 'local' })
|
||||
expect(content).toBe('content')
|
||||
})
|
||||
})
|
||||
+30
-11
@@ -12,8 +12,9 @@ import type {
|
||||
ProxyPortSettings,
|
||||
UnlockItem,
|
||||
ProxyViewV1_Serialize as ProxyViewV1,
|
||||
IProfiles_Deserialize,
|
||||
PrfItem_Deserialize,
|
||||
ProfileSelection,
|
||||
ProfilePatch,
|
||||
ProfileCreate,
|
||||
} from './bindings'
|
||||
|
||||
export type {
|
||||
@@ -41,15 +42,24 @@ export async function enhanceProfiles() {
|
||||
return (await commands.enhanceProfiles()).status === 'valid'
|
||||
}
|
||||
|
||||
export async function patchProfilesConfig(profiles: IProfiles_Deserialize) {
|
||||
return commands.patchProfilesConfig(profiles)
|
||||
export async function patchProfilesConfig(profiles: ProfileSelection) {
|
||||
return commands.patchProfilesConfig({ current: profiles.current })
|
||||
}
|
||||
|
||||
export async function createProfile(
|
||||
item: PrfItem_Deserialize,
|
||||
item: ProfileCreate,
|
||||
fileData?: string | null,
|
||||
) {
|
||||
return commands.createProfile(item, fileData ?? null)
|
||||
return commands.createProfile(
|
||||
{
|
||||
type: item.type,
|
||||
name: item.name,
|
||||
desc: item.desc,
|
||||
...(item.type === 'remote' ? { url: item.url } : {}),
|
||||
option: item.option,
|
||||
},
|
||||
fileData ?? null,
|
||||
)
|
||||
}
|
||||
|
||||
export async function viewProfile(index: string) {
|
||||
@@ -80,11 +90,20 @@ export async function deleteProfile(index: string) {
|
||||
return commands.deleteProfile(index)
|
||||
}
|
||||
|
||||
export async function patchProfile(
|
||||
index: string,
|
||||
profile: PrfItem_Deserialize,
|
||||
) {
|
||||
return commands.patchProfile(index, profile)
|
||||
export async function patchProfile(index: string, profile: ProfilePatch) {
|
||||
const { type, name, desc, file, url, selected, extra, updated, option } =
|
||||
profile
|
||||
return commands.patchProfile(index, {
|
||||
type,
|
||||
name,
|
||||
desc,
|
||||
file,
|
||||
url,
|
||||
selected,
|
||||
extra,
|
||||
updated,
|
||||
option,
|
||||
})
|
||||
}
|
||||
|
||||
export async function getClashInfo() {
|
||||
|
||||
Reference in New Issue
Block a user