mirror of
https://github.com/LHY0125/PathEditor.git
synced 2026-06-30 02:25:55 +08:00
chore: 同步 v5.0 基础设施完善到 v5.1
从 v5.0 cherry-pick 的开源项目基础设施改进: 新增配置文件: - .editorconfig, .gitattributes, .prettierrc, .markdownlint.json - commitlint.config.js 新增 GitHub 社区文件: - .github/dependabot.yml — 依赖自动更新 - .github/CODEOWNERS — 自动 PR 审查分配 - .github/FUNDING.yml — 开源赞助入口 新增文档: - ROADMAP.md — 路线图 - SUPPORT.md — 帮助指南 - docs/screenshots/ — 应用截图 新增 Git Hooks: - .husky/pre-commit — lint-staged 自动格式化+修复 - .husky/commit-msg — commitlint 校验 CI 强化: - 新增 Prettier 格式检查 - 新增 Vitest 覆盖率 + Codecov 上报 - 保留 v5.1 已有的 rust-cache + jsdom 全局环境 修复: - index.html 标题 v4.0 → v5.1 - PathEditDialog set-state-in-effect 改用 useRef prevOpen 守卫 - merge-preview.test.tsx no-explicit-any 修复 - 所有 TS/TSX 文件 Prettier 格式化统一 v5.1 保留特性: - @tanstack/react-virtual 虚拟滚动 - jsdom 全局测试环境 - Swatinem/rust-cache CI 加速 - 105 测试全部通过
This commit is contained in:
@@ -3,7 +3,12 @@ import { useAppStore } from '@/store/app-store';
|
||||
import { TargetType } from '@/core/undo-redo';
|
||||
import { open } from '@tauri-apps/plugin-dialog';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { importFromContent, exportToJson, exportToCsv, flattenImportResult } from '@/core/import-export';
|
||||
import {
|
||||
importFromContent,
|
||||
exportToJson,
|
||||
exportToCsv,
|
||||
flattenImportResult,
|
||||
} from '@/core/import-export';
|
||||
import type { PathEntry } from '@/core/path-entry';
|
||||
import { is_valid_path_format } from '@/core/validation';
|
||||
import { useKeyboard } from './use-keyboard';
|
||||
@@ -38,9 +43,10 @@ export function useAppActions(activeTab: TabId, dialogs: DialogState) {
|
||||
const idx = useAppStore.getState().selectedIndices[0];
|
||||
if (idx === undefined) return;
|
||||
const target = activeTab === 'user' ? TargetType.USER : TargetType.SYSTEM;
|
||||
const list = target === TargetType.SYSTEM
|
||||
? useAppStore.getState().sysPaths
|
||||
: useAppStore.getState().userPaths;
|
||||
const list =
|
||||
target === TargetType.SYSTEM
|
||||
? useAppStore.getState().sysPaths
|
||||
: useAppStore.getState().userPaths;
|
||||
const entry = list[idx];
|
||||
if (entry) setEditDialog({ open: true, index: idx, value: entry.path, target });
|
||||
}, [activeTab, setEditDialog]);
|
||||
@@ -71,14 +77,9 @@ export function useAppActions(activeTab: TabId, dialogs: DialogState) {
|
||||
}, [getCurrentTarget]);
|
||||
|
||||
const handleClean = useCallback(() => {
|
||||
const removed = useAppStore.getState().cleanPaths(
|
||||
getCurrentTarget(),
|
||||
is_valid_path_format,
|
||||
);
|
||||
const removed = useAppStore.getState().cleanPaths(getCurrentTarget(), is_valid_path_format);
|
||||
if (removed.length > 0) {
|
||||
useAppStore.getState().setStatusMessage(
|
||||
i18n.t('status.deleted', { count: removed.length }),
|
||||
);
|
||||
useAppStore.getState().setStatusMessage(i18n.t('status.deleted', { count: removed.length }));
|
||||
}
|
||||
}, [getCurrentTarget]);
|
||||
|
||||
@@ -95,9 +96,15 @@ export function useAppActions(activeTab: TabId, dialogs: DialogState) {
|
||||
if (result.system.length > 0 && result.user.length > 0) {
|
||||
setImportDialog({ open: true, system: result.system, user: result.user });
|
||||
} else if (result.system.length > 0) {
|
||||
useAppStore.getState().replacePaths(TargetType.SYSTEM, result.system.map(e => e.path));
|
||||
useAppStore.getState().replacePaths(
|
||||
TargetType.SYSTEM,
|
||||
result.system.map((e) => e.path),
|
||||
);
|
||||
} else if (result.user.length > 0) {
|
||||
useAppStore.getState().replacePaths(TargetType.USER, result.user.map(e => e.path));
|
||||
useAppStore.getState().replacePaths(
|
||||
TargetType.USER,
|
||||
result.user.map((e) => e.path),
|
||||
);
|
||||
}
|
||||
}, [setImportDialog]);
|
||||
|
||||
@@ -122,7 +129,10 @@ export function useAppActions(activeTab: TabId, dialogs: DialogState) {
|
||||
if (result.kind === 'warning') {
|
||||
// 长度超限,需要用户确认
|
||||
const { ask } = await import('@tauri-apps/plugin-dialog');
|
||||
const confirmed = await ask(i18n.t('status.saveWarningLongPaths'), { title: i18n.t('dialog.backupTitle'), kind: 'warning' });
|
||||
const confirmed = await ask(i18n.t('status.saveWarningLongPaths'), {
|
||||
title: i18n.t('dialog.backupTitle'),
|
||||
kind: 'warning',
|
||||
});
|
||||
if (confirmed) {
|
||||
await useAppStore.getState().savePaths(true);
|
||||
}
|
||||
@@ -156,33 +166,62 @@ export function useAppActions(activeTab: TabId, dialogs: DialogState) {
|
||||
|
||||
// ── 弹窗确认 ──
|
||||
|
||||
const handleNewConfirm = useCallback((value: string) => {
|
||||
setNewDialog(false);
|
||||
if (value.trim()) useAppStore.getState().addPath(value.trim(), getCurrentTarget());
|
||||
}, [getCurrentTarget, setNewDialog]);
|
||||
const handleNewConfirm = useCallback(
|
||||
(value: string) => {
|
||||
setNewDialog(false);
|
||||
if (value.trim()) useAppStore.getState().addPath(value.trim(), getCurrentTarget());
|
||||
},
|
||||
[getCurrentTarget, setNewDialog],
|
||||
);
|
||||
|
||||
const handleEditConfirm = useCallback((value: string) => {
|
||||
const d = dialogs.editDialog;
|
||||
setEditDialog({ open: false, index: -1, value: '', target: TargetType.SYSTEM });
|
||||
if (value.trim()) useAppStore.getState().editPath(d.index, value.trim(), d.target);
|
||||
}, [dialogs.editDialog, setEditDialog]);
|
||||
const handleEditConfirm = useCallback(
|
||||
(value: string) => {
|
||||
const d = dialogs.editDialog;
|
||||
setEditDialog({ open: false, index: -1, value: '', target: TargetType.SYSTEM });
|
||||
if (value.trim()) useAppStore.getState().editPath(d.index, value.trim(), d.target);
|
||||
},
|
||||
[dialogs.editDialog, setEditDialog],
|
||||
);
|
||||
|
||||
const handleImportSelect = useCallback((target: 'system' | 'user' | 'both') => {
|
||||
const { system, user } = dialogs.importDialog;
|
||||
const flat = flattenImportResult({ system, user }, target);
|
||||
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]);
|
||||
const handleImportSelect = useCallback(
|
||||
(target: 'system' | 'user' | 'both') => {
|
||||
const { system, user } = dialogs.importDialog;
|
||||
const flat = flattenImportResult({ system, user }, target);
|
||||
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],
|
||||
);
|
||||
|
||||
return {
|
||||
handleNew, handleEdit, handleBrowse, handleDelete,
|
||||
handleMoveUp, handleMoveDown, handleClean,
|
||||
handleImport, handleExport, handleSave,
|
||||
handleNewConfirm, handleEditConfirm, handleImportSelect,
|
||||
handleNew,
|
||||
handleEdit,
|
||||
handleBrowse,
|
||||
handleDelete,
|
||||
handleMoveUp,
|
||||
handleMoveDown,
|
||||
handleClean,
|
||||
handleImport,
|
||||
handleExport,
|
||||
handleSave,
|
||||
handleNewConfirm,
|
||||
handleEditConfirm,
|
||||
handleImportSelect,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -60,17 +60,15 @@ export function usePathValidation(paths: readonly PathEntry[]) {
|
||||
|
||||
const batch = toValidate.slice(0, 20);
|
||||
Promise.all(
|
||||
batch.map(
|
||||
async (p): Promise<[string, ValidationState]> => {
|
||||
try {
|
||||
if (p.path.includes('%')) return [p.path, 'valid'];
|
||||
const valid: boolean = await invoke('validate_path', { path: p.path });
|
||||
return [p.path, valid ? 'valid' : 'invalid'];
|
||||
} catch {
|
||||
return [p.path, 'unknown'];
|
||||
}
|
||||
},
|
||||
),
|
||||
batch.map(async (p): Promise<[string, ValidationState]> => {
|
||||
try {
|
||||
if (p.path.includes('%')) return [p.path, 'valid'];
|
||||
const valid: boolean = await invoke('validate_path', { path: p.path });
|
||||
return [p.path, valid ? 'valid' : 'invalid'];
|
||||
} catch {
|
||||
return [p.path, 'unknown'];
|
||||
}
|
||||
}),
|
||||
).then((results) => {
|
||||
if (cancelled) return;
|
||||
for (const [p] of results) validatedRef.current.add(p);
|
||||
@@ -89,23 +87,19 @@ export function usePathValidation(paths: readonly PathEntry[]) {
|
||||
// 异步展开环境变量(setState 在 .then() 回调中)
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
const toExpand = paths.filter(
|
||||
(p) => p.path.includes('%') && !expandedRef.current.has(p.path),
|
||||
);
|
||||
const toExpand = paths.filter((p) => p.path.includes('%') && !expandedRef.current.has(p.path));
|
||||
if (toExpand.length === 0) return;
|
||||
|
||||
const batch = toExpand.slice(0, 20);
|
||||
Promise.all(
|
||||
batch.map(
|
||||
async (p): Promise<[string, string]> => {
|
||||
try {
|
||||
const expanded: string = await invoke('expand_env_vars', { path: p.path });
|
||||
return [p.path, expanded !== p.path ? expanded : ''];
|
||||
} catch {
|
||||
return [p.path, ''];
|
||||
}
|
||||
},
|
||||
),
|
||||
batch.map(async (p): Promise<[string, string]> => {
|
||||
try {
|
||||
const expanded: string = await invoke('expand_env_vars', { path: p.path });
|
||||
return [p.path, expanded !== p.path ? expanded : ''];
|
||||
} catch {
|
||||
return [p.path, ''];
|
||||
}
|
||||
}),
|
||||
).then((results) => {
|
||||
if (cancelled) return;
|
||||
for (const [p] of results) expandedRef.current.add(p);
|
||||
|
||||
Reference in New Issue
Block a user