mirror of
https://github.com/LHY0125/PathEditor.git
synced 2026-07-01 11:45:54 +08:00
9453006310
从 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 测试全部通过
120 lines
4.0 KiB
TypeScript
120 lines
4.0 KiB
TypeScript
import { useState, useEffect, useRef, useMemo } from 'react';
|
||
import { invoke } from '@tauri-apps/api/core';
|
||
import type { PathEntry } from '@/core/path-entry';
|
||
|
||
export type ValidationState = 'valid' | 'invalid' | 'unknown';
|
||
|
||
/**
|
||
* 异步验证路径目录是否真实存在 + 展开环境变量
|
||
* 缓存结果避免重复 IPC 调用。
|
||
* setState 仅在异步 .then() 回调中调用(符合 React 规则),
|
||
* 不存在路径的缓存清理通过 useMemo 派生。
|
||
*/
|
||
export function usePathValidation(paths: readonly PathEntry[]) {
|
||
const validatedRef = useRef<Set<string>>(new Set());
|
||
const expandedRef = useRef<Set<string>>(new Set());
|
||
const [validationCache, setValidationCache] = useState<Map<string, ValidationState>>(new Map());
|
||
const [expandedCache, setExpandedCache] = useState<Map<string, string>>(new Map());
|
||
|
||
// 仅保留当前 paths 中存在的条目(派生 state,不在 effect 中同步 setState)
|
||
const currentKeys = useMemo(() => new Set(paths.map((p) => p.path)), [paths]);
|
||
const cleanedValidationCache = useMemo(() => {
|
||
const next = new Map(validationCache);
|
||
let changed = false;
|
||
for (const key of next.keys()) {
|
||
if (!currentKeys.has(key)) {
|
||
next.delete(key);
|
||
changed = true;
|
||
}
|
||
}
|
||
return changed ? next : validationCache;
|
||
}, [validationCache, currentKeys]);
|
||
|
||
const cleanedExpandedCache = useMemo(() => {
|
||
const next = new Map(expandedCache);
|
||
let changed = false;
|
||
for (const key of next.keys()) {
|
||
if (!currentKeys.has(key)) {
|
||
next.delete(key);
|
||
changed = true;
|
||
}
|
||
}
|
||
return changed ? next : expandedCache;
|
||
}, [expandedCache, currentKeys]);
|
||
|
||
// 同步清理 ref(ref 不能在 render 期间修改,放在 effect 中不 setState 是安全的)
|
||
useEffect(() => {
|
||
for (const key of validatedRef.current) {
|
||
if (!currentKeys.has(key)) validatedRef.current.delete(key);
|
||
}
|
||
for (const key of expandedRef.current) {
|
||
if (!currentKeys.has(key)) expandedRef.current.delete(key);
|
||
}
|
||
}, [currentKeys]);
|
||
|
||
// 异步验证路径(setState 在 .then() 回调中,符合 React 规则)
|
||
useEffect(() => {
|
||
let cancelled = false;
|
||
const toValidate = paths.filter((p) => !validatedRef.current.has(p.path));
|
||
if (toValidate.length === 0) return;
|
||
|
||
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'];
|
||
}
|
||
}),
|
||
).then((results) => {
|
||
if (cancelled) return;
|
||
for (const [p] of results) validatedRef.current.add(p);
|
||
setValidationCache((prev) => {
|
||
const next = new Map(prev);
|
||
for (const [p, v] of results) next.set(p, v);
|
||
return next;
|
||
});
|
||
});
|
||
|
||
return () => {
|
||
cancelled = true;
|
||
};
|
||
}, [paths]);
|
||
|
||
// 异步展开环境变量(setState 在 .then() 回调中)
|
||
useEffect(() => {
|
||
let cancelled = false;
|
||
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, ''];
|
||
}
|
||
}),
|
||
).then((results) => {
|
||
if (cancelled) return;
|
||
for (const [p] of results) expandedRef.current.add(p);
|
||
setExpandedCache((prev) => {
|
||
const next = new Map(prev);
|
||
for (const [p, v] of results) next.set(p, v);
|
||
return next;
|
||
});
|
||
});
|
||
|
||
return () => {
|
||
cancelled = true;
|
||
};
|
||
}, [paths]);
|
||
|
||
return { validationCache: cleanedValidationCache, expandedCache: cleanedExpandedCache };
|
||
}
|