mirror of
https://github.com/LHY0125/PathEditor.git
synced 2026-06-29 01:45:54 +08:00
fix: 修复 5 个 bug + 备份警告丢失
- BUG 1: undo/redo 后持久化 disabled 状态到 disabled.json - BUG 2: expand_env_vars 增加缓冲区不足检测(result > required) - BUG 3: E2E mock load_disabled_state 返回格式从对象改为数组 - BUG 4: 双 hive 保存失败时同时显示两个错误原因 - BUG 5: 导入 both 合并为单条 undo 记录(新增 IMPORT_BOTH 操作类型) - 备份失败后保存成功时显示"保存成功(备份失败)"而非覆盖警告 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+17
-1
@@ -5,7 +5,7 @@
|
||||
import type { PathEntry } from './path-entry';
|
||||
|
||||
export const OperationType = {
|
||||
ADD: 0, DELETE: 1, EDIT: 2, MOVE_UP: 3, MOVE_DOWN: 4, CLEAN: 5, CLEAR: 6, IMPORT: 7, TOGGLE: 8,
|
||||
ADD: 0, DELETE: 1, EDIT: 2, MOVE_UP: 3, MOVE_DOWN: 4, CLEAN: 5, CLEAR: 6, IMPORT: 7, TOGGLE: 8, IMPORT_BOTH: 9,
|
||||
} as const;
|
||||
export type OperationType = (typeof OperationType)[keyof typeof OperationType];
|
||||
|
||||
@@ -21,6 +21,10 @@ export interface OpRecord {
|
||||
newPaths: PathEntry[];
|
||||
/** DELETE 操作专用:被删除的各路径的原始 index(升序) */
|
||||
indices?: number[];
|
||||
/** IMPORT_BOTH 专用:用户 hive 的旧路径 */
|
||||
oldPathsOther?: PathEntry[];
|
||||
/** IMPORT_BOTH 专用:用户 hive 的新路径 */
|
||||
newPathsOther?: PathEntry[];
|
||||
}
|
||||
|
||||
const DEFAULT_MAX_SIZE = 50;
|
||||
@@ -88,6 +92,12 @@ export class UndoRedoManager {
|
||||
case OperationType.TOGGLE:
|
||||
target[rec.index] = rec.oldPaths[0];
|
||||
break;
|
||||
case OperationType.IMPORT_BOTH:
|
||||
sys.length = 0;
|
||||
sys.push(...rec.oldPaths);
|
||||
user.length = 0;
|
||||
user.push(...(rec.oldPathsOther || []));
|
||||
return [sys, user];
|
||||
}
|
||||
|
||||
return [sys, user];
|
||||
@@ -138,6 +148,12 @@ export class UndoRedoManager {
|
||||
case OperationType.TOGGLE:
|
||||
target[rec.index] = rec.newPaths[0];
|
||||
break;
|
||||
case OperationType.IMPORT_BOTH:
|
||||
sys.length = 0;
|
||||
sys.push(...rec.newPaths);
|
||||
user.length = 0;
|
||||
user.push(...(rec.newPathsOther || []));
|
||||
return [sys, user];
|
||||
}
|
||||
|
||||
return [sys, user];
|
||||
|
||||
@@ -160,8 +160,12 @@ export function useAppActions(activeTab: TabId, dialogs: DialogState) {
|
||||
const handleImportSelect = useCallback((target: 'system' | 'user' | 'both') => {
|
||||
const { system, user } = dialogs.importDialog;
|
||||
const flat = flattenImportResult({ system, user }, target);
|
||||
if (flat.system.length > 0) useAppStore.getState().replacePaths(TargetType.SYSTEM, flat.system.map(e => e.path));
|
||||
if (flat.user.length > 0) useAppStore.getState().replacePaths(TargetType.USER, flat.user.map(e => e.path));
|
||||
if (target === 'both' && flat.system.length > 0 && flat.user.length > 0) {
|
||||
useAppStore.getState().replaceBothPaths(flat.system.map(e => e.path), flat.user.map(e => e.path));
|
||||
} else {
|
||||
if (flat.system.length > 0) useAppStore.getState().replacePaths(TargetType.SYSTEM, flat.system.map(e => e.path));
|
||||
if (flat.user.length > 0) useAppStore.getState().replacePaths(TargetType.USER, flat.user.map(e => e.path));
|
||||
}
|
||||
setImportDialog({ open: false, system: [], user: [] });
|
||||
}, [dialogs.importDialog, setImportDialog]);
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"readonly": "Read-only mode — Administrator privileges required for editing",
|
||||
"saving": "Saving...",
|
||||
"saved": "Saved successfully",
|
||||
"saved_without_backup": "Saved (backup failed)",
|
||||
"error": "Operation failed",
|
||||
"warning_backup": "Backup creation failed, save will proceed without backup",
|
||||
"deleted": "Deleted {{count}} path(s)",
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"readonly": "只读模式 — 需要管理员权限才能编辑",
|
||||
"saving": "正在保存...",
|
||||
"saved": "保存成功",
|
||||
"saved_without_backup": "保存成功(备份失败)",
|
||||
"error": "操作失败",
|
||||
"warning_backup": "备份创建失败,保存将继续但不生成备份",
|
||||
"deleted": "已删除 {{count}} 个路径",
|
||||
|
||||
+34
-5
@@ -36,6 +36,7 @@ interface AppState {
|
||||
moveDown: (index: number, target: TargetType) => void;
|
||||
cleanPaths: (target: TargetType, validateFn: (p: string) => boolean) => string[];
|
||||
replacePaths: (target: TargetType, newPaths: string[]) => void;
|
||||
replaceBothPaths: (sysPaths: string[], userPaths: string[]) => void;
|
||||
clearPaths: (target: TargetType) => void;
|
||||
|
||||
togglePath: (index: number, target: TargetType) => void;
|
||||
@@ -195,6 +196,20 @@ export const useAppStore = create<AppState>((set, get) => {
|
||||
markDirty();
|
||||
},
|
||||
|
||||
replaceBothPaths: (sysPaths, userPaths) => {
|
||||
const state = get();
|
||||
const sysEntries: PathEntry[] = sysPaths.map(p => ({ path: p, enabled: true }));
|
||||
const usrEntries: PathEntry[] = userPaths.map(p => ({ path: p, enabled: true }));
|
||||
state.undoRedo.push({
|
||||
type: OperationType.IMPORT_BOTH, target: TargetType.SYSTEM, index: 0,
|
||||
count: sysEntries.length + usrEntries.length,
|
||||
oldPaths: [...state.sysPaths], newPaths: [...sysEntries],
|
||||
oldPathsOther: [...state.userPaths], newPathsOther: [...usrEntries],
|
||||
});
|
||||
set({ sysPaths: [...sysEntries], userPaths: [...usrEntries], selectedIndices: [] });
|
||||
markDirty();
|
||||
},
|
||||
|
||||
clearPaths: (target) => {
|
||||
const state = get();
|
||||
const list = target === TargetType.SYSTEM ? state.sysPaths : state.userPaths;
|
||||
@@ -245,6 +260,11 @@ export const useAppStore = create<AppState>((set, get) => {
|
||||
// 内联计算 isModified 而非调用 markDirty(),避免两次 set() 导致额外渲染
|
||||
isModified: !(arraysEqual(result[0], _savedSys) && arraysEqual(result[1], _savedUser)),
|
||||
});
|
||||
// 同步持久化 disabled 状态,与 togglePath 保持一致
|
||||
invoke('save_disabled_state', {
|
||||
system: result[0].filter(e => !e.enabled).map(e => e.path),
|
||||
user: result[1].filter(e => !e.enabled).map(e => e.path),
|
||||
}).catch(() => {});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -257,6 +277,11 @@ export const useAppStore = create<AppState>((set, get) => {
|
||||
// 内联计算 isModified 而非调用 markDirty(),避免两次 set() 导致额外渲染
|
||||
isModified: !(arraysEqual(result[0], _savedSys) && arraysEqual(result[1], _savedUser)),
|
||||
});
|
||||
// 同步持久化 disabled 状态,与 togglePath 保持一致
|
||||
invoke('save_disabled_state', {
|
||||
system: result[0].filter(e => !e.enabled).map(e => e.path),
|
||||
user: result[1].filter(e => !e.enabled).map(e => e.path),
|
||||
}).catch(() => {});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -314,8 +339,9 @@ export const useAppStore = create<AppState>((set, get) => {
|
||||
}
|
||||
|
||||
// 备份当前注册表(保存前备份旧值,失败仅警告不中断)
|
||||
let backupFailed = false;
|
||||
await invoke('backup_registry', { customDir: null })
|
||||
.catch(() => set({ statusMessage: i18n.t('status.warning_backup') }));
|
||||
.catch(() => { backupFailed = true; });
|
||||
|
||||
const [sysResult, userResult] = await Promise.allSettled([
|
||||
invoke('save_system_paths', { paths: sysPaths }),
|
||||
@@ -328,11 +354,14 @@ export const useAppStore = create<AppState>((set, get) => {
|
||||
if (sysOk && userOk) {
|
||||
invoke('broadcast_env_change').catch(() => {});
|
||||
const savedSys = [...state.sysPaths], savedUser = [...state.userPaths];
|
||||
set({ isModified: false, isSaving: false, statusMessage: i18n.t('status.saved'), _savedSys: savedSys, _savedUser: savedUser });
|
||||
set({ isModified: false, isSaving: false,
|
||||
statusMessage: backupFailed ? i18n.t('status.saved_without_backup') : i18n.t('status.saved'),
|
||||
_savedSys: savedSys, _savedUser: savedUser });
|
||||
} else {
|
||||
const reason = (!sysOk && sysResult.status === 'rejected') ? String(sysResult.reason) :
|
||||
(!userOk && userResult.status === 'rejected') ? String(userResult.reason) : '';
|
||||
const msg = sysOk ? '用户 PATH 保存失败' : userOk ? '系统 PATH 保存失败' : `保存失败: ${reason}`;
|
||||
const sysErr = (!sysOk && sysResult.status === 'rejected') ? String(sysResult.reason) : '';
|
||||
const usrErr = (!userOk && userResult.status === 'rejected') ? String(userResult.reason) : '';
|
||||
const parts = [sysErr, usrErr].filter(Boolean);
|
||||
const msg = sysOk ? '用户 PATH 保存失败' : userOk ? '系统 PATH 保存失败' : `保存失败: ${parts.join('; ')}`;
|
||||
set({ isSaving: false, statusMessage: msg });
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user