导出 hook 换项目时整套重置并丢弃过时结果
view/project-development/export/state/useXhsMinitoolExport.ts:新增 projectPath 重置效果,把表单/判据/冲突/产物状态与全部本地基线清回初值;读内容、取指纹、保存、构建各处在 await 回来后用 projectPathRef 校验结果仍属于当前项目,过时就丢弃,不再写回新项目。 tests/xhsMinitoolExport.test.tsx:补换项目后表单与产物判据清空、并读回新项目内容的用例。
This commit is contained in:
+60
-4
@@ -134,6 +134,11 @@ export function useXhsMinitoolExport({
|
||||
/** 上一次抛进错误池的故障指纹:同一个只抛一次,见 [useXhsMinitoolExport.throwToErrorPool]。 */
|
||||
const reportedFailureRef = useRef('');
|
||||
const formRef = useRef<XHSMiniToolExportForm | null>(null);
|
||||
/**
|
||||
* 当前项目路径的即时值。异步结果(读内容、取指纹、保存、构建)回来时用它判断结果还属不属于
|
||||
* 当前项目:面板根跨项目不卸载,旧请求回来时绝不能把上一个项目的表单 / 基线写进新项目。
|
||||
*/
|
||||
const projectPathRef = useRef(projectPath);
|
||||
|
||||
useEffect(() => {
|
||||
formRef.current = form;
|
||||
@@ -143,6 +148,36 @@ export function useXhsMinitoolExport({
|
||||
adoptableRef.current = !dirty && !conflict && !fieldError;
|
||||
}, [dirty, conflict, fieldError]);
|
||||
|
||||
/**
|
||||
* 换项目时把整套状态与本地判据清回初值。
|
||||
*
|
||||
* 面板根跨项目不卸载,而这个 hook 的状态默认都属于「当前项目」:不清就会拿 A 的表单、写回基线
|
||||
* 与产物判据去操作 B。清完之后,下面依赖 `projectPath` 的轮询效果会立刻为新项目重读一次。
|
||||
*
|
||||
* `initialReadSettledRef` 必须一起复位:它决定 `loading` 与首次采纳,留着 true 会让新项目的
|
||||
* 第一次读取被当成「非首次轮询」而直接早返回,旧表单就会继续留在界面上。
|
||||
*/
|
||||
useEffect(() => {
|
||||
projectPathRef.current = projectPath;
|
||||
setForm(null);
|
||||
setHasScript(false);
|
||||
setFieldError(null);
|
||||
setConflict(null);
|
||||
setFailure(null);
|
||||
setDismissedFailure('');
|
||||
setLoading(false);
|
||||
setReadSettled(false);
|
||||
setRunning(false);
|
||||
setArtifactReady(false);
|
||||
setDirty(false);
|
||||
setAdaptPromptDismissed(false);
|
||||
baselineRef.current = '';
|
||||
hashPendingRef.current = false;
|
||||
initialReadSettledRef.current = false;
|
||||
reportedFailureRef.current = '';
|
||||
formRef.current = null;
|
||||
}, [projectPath]);
|
||||
|
||||
// 脚本出现之后就把提示重新武装:它要是又被删掉,用户该再看到一次阻断提示。
|
||||
useEffect(() => {
|
||||
if (hasScript) setAdaptPromptDismissed(false);
|
||||
@@ -192,6 +227,8 @@ export function useXhsMinitoolExport({
|
||||
invoke,
|
||||
projectPath,
|
||||
);
|
||||
// 读回来之前换了项目:这份内容属于上一个项目,不能写进新项目的状态。
|
||||
if (projectPathRef.current !== projectPath) return null;
|
||||
setHasScript(content.hasScript);
|
||||
// 内容读回来了,说明宿主与注册表现在读得动:读来源的失败(注册表坏了 / 宿主不可用)就地
|
||||
// 清掉,别让弹窗卡在已经好了的现场上。用函数式更新拿最新 `failure`,既不把 `failure` 加进
|
||||
@@ -221,19 +258,25 @@ export function useXhsMinitoolExport({
|
||||
setConflict(null);
|
||||
setFailure(null);
|
||||
try {
|
||||
baselineRef.current = await readXhsMinitoolExportHash(
|
||||
const contentHash = await readXhsMinitoolExportHash(
|
||||
invoke,
|
||||
projectPath,
|
||||
);
|
||||
// 基线属于项目:换过项目就不能拿这份指纹去当新项目的写回基线。
|
||||
if (projectPathRef.current !== projectPath) return null;
|
||||
baselineRef.current = contentHash;
|
||||
hashPendingRef.current = false;
|
||||
} catch (error) {
|
||||
// 指纹没取到:表单已经采纳、基线却还停在旧值。标成 pending,下一次轮询跳过上面的早返回
|
||||
// 把指纹补上;失败本身照旧交给外层 catch 报给用户。
|
||||
if (projectPathRef.current !== projectPath) return null;
|
||||
hashPendingRef.current = true;
|
||||
throw error;
|
||||
}
|
||||
return content.hasScript;
|
||||
} catch (error) {
|
||||
// 旧项目的失败不该记到新项目头上(也不该为新项目弹卡片)。
|
||||
if (projectPathRef.current !== projectPath) return null;
|
||||
const structured = asXhsMinitoolReadFailure(error);
|
||||
if (!structured) {
|
||||
// 认不出形状的拒绝(Tauri 传输失败 / 命令 panic / 参数序列化失败):现场原样抛给错误池
|
||||
@@ -249,7 +292,8 @@ export function useXhsMinitoolExport({
|
||||
setFailure(structured);
|
||||
return null;
|
||||
} finally {
|
||||
if (isInitialRead) {
|
||||
// 旧项目的收尾不翻新项目的「首次读取」判据:新项目那次 refresh 会自己收尾。
|
||||
if (isInitialRead && projectPathRef.current === projectPath) {
|
||||
initialReadSettledRef.current = true;
|
||||
setLoading(false);
|
||||
setReadSettled(true);
|
||||
@@ -281,12 +325,15 @@ export function useXhsMinitoolExport({
|
||||
const invoke = resolveTauriInvoke();
|
||||
if (!invoke) return false;
|
||||
try {
|
||||
baselineRef.current = await saveXhsMinitoolExportForm(invoke, {
|
||||
const nextBaseline = await saveXhsMinitoolExportForm(invoke, {
|
||||
projectPath,
|
||||
form: next,
|
||||
baseHash,
|
||||
});
|
||||
// 保存期间换了项目:这份返回值属于上一个项目,不能拿来当新项目的写回基线。
|
||||
if (projectPathRef.current !== projectPath) return false;
|
||||
// 保存命令的返回值就是当前表单的新指纹:这份表单的基线已经配套,不必再补读。
|
||||
baselineRef.current = nextBaseline;
|
||||
hashPendingRef.current = false;
|
||||
setFieldError(null);
|
||||
setConflict(null);
|
||||
@@ -294,6 +341,8 @@ export function useXhsMinitoolExport({
|
||||
setDirty(false);
|
||||
return true;
|
||||
} catch (error) {
|
||||
// 旧项目的失败不该记到新项目头上。
|
||||
if (projectPathRef.current !== projectPath) return false;
|
||||
// 三种结果都要停下重试,等用户或下一次输入推着走:字段不合法等用户改、冲突等用户选、
|
||||
// 其余按 typed 失败给建议。共同点是 `dirty` 归零,避免同一份输入反复打宿主。
|
||||
setDirty(false);
|
||||
@@ -358,8 +407,12 @@ export function useXhsMinitoolExport({
|
||||
setFailure(null);
|
||||
try {
|
||||
await runXhsMinitoolExportBuild(invoke, projectPath);
|
||||
// 构建期间换了项目:这次产物属于上一个项目,不能让新项目出现可下载的按钮。
|
||||
if (projectPathRef.current !== projectPath) return;
|
||||
setArtifactReady(true);
|
||||
} catch (error) {
|
||||
// 旧项目的构建失败不该记到新项目头上。
|
||||
if (projectPathRef.current !== projectPath) return;
|
||||
setArtifactReady(false);
|
||||
const structured = asXhsMinitoolBuildFailure(error);
|
||||
if (!structured) {
|
||||
@@ -393,6 +446,8 @@ export function useXhsMinitoolExport({
|
||||
setRunning(true);
|
||||
try {
|
||||
const hasScriptNow = await refresh();
|
||||
// 读期间换了项目:这次「有没有脚本」的结论属于上一个项目。
|
||||
if (projectPathRef.current !== projectPath) return;
|
||||
if (hasScriptNow !== true) {
|
||||
// 弹窗重新武装起来就是给用户的交代,状态行不再复述一遍。
|
||||
setAdaptPromptDismissed(false);
|
||||
@@ -407,7 +462,8 @@ export function useXhsMinitoolExport({
|
||||
if (conflict || fieldError) return;
|
||||
await buildOnce();
|
||||
} finally {
|
||||
setRunning(false);
|
||||
// 旧项目那一次打包的收尾不解新项目的锁。
|
||||
if (projectPathRef.current === projectPath) setRunning(false);
|
||||
}
|
||||
}, [
|
||||
projectPath,
|
||||
|
||||
@@ -798,6 +798,49 @@ describe('状态 hook', () => {
|
||||
expect(result.current.form).toEqual(FORM);
|
||||
});
|
||||
|
||||
it('换项目时清掉上一份表单与产物判据,并读回新项目的内容', async () => {
|
||||
const OTHER_PATH = '/tmp/export-panel-project-2';
|
||||
const OTHER_FORM = {
|
||||
name: '另一个小工具',
|
||||
introduction: '另一个简介',
|
||||
iconPath: 'assets/other.png',
|
||||
};
|
||||
const invoke = vi.fn(
|
||||
async (command: string, args?: { projectPath?: string }) => {
|
||||
if (command === 'read_xhs_minitool_export') {
|
||||
return args?.projectPath === PROJECT_PATH
|
||||
? { form: FORM, hasScript: true }
|
||||
: { form: OTHER_FORM, hasScript: false };
|
||||
}
|
||||
if (command === 'read_xhs_minitool_export_hash') return CONTENT_HASH;
|
||||
return { outputTail: 'zip 已生成', omittedCharacters: 0 };
|
||||
},
|
||||
);
|
||||
installTauriRuntime({ core: { invoke } });
|
||||
const { result, rerender } = renderHook(
|
||||
({ projectPath }: { projectPath: string }) =>
|
||||
useXhsMinitoolExport({
|
||||
enabled: true,
|
||||
projectPath,
|
||||
submitTurn: vi.fn(async () => true),
|
||||
}),
|
||||
{ initialProps: { projectPath: PROJECT_PATH } },
|
||||
);
|
||||
await waitFor(() => expect(result.current.form).toEqual(FORM));
|
||||
await act(async () => {
|
||||
await result.current.packageAll();
|
||||
});
|
||||
expect(result.current.artifactReady).toBe(true);
|
||||
|
||||
rerender({ projectPath: OTHER_PATH });
|
||||
// 新项目第一次读取落定之前,界面不能还显示上一个项目的表单与可下载产物。
|
||||
expect(result.current.form).toBeNull();
|
||||
expect(result.current.artifactReady).toBe(false);
|
||||
|
||||
await waitFor(() => expect(result.current.form).toEqual(OTHER_FORM));
|
||||
expect(result.current.hasScript).toBe(false);
|
||||
});
|
||||
|
||||
it('打包成功才有下载入口,失败会收回去', async () => {
|
||||
let fail = false;
|
||||
const invoke = vi.fn(async (command: string) => {
|
||||
|
||||
Reference in New Issue
Block a user