fix: return draft results to shelf

This commit is contained in:
kdletters
2026-05-26 14:43:49 +08:00
parent 2d34004e64
commit 2a031bcd89
4 changed files with 164 additions and 49 deletions

View File

@@ -453,6 +453,7 @@ type PendingDraftShelfMap = Partial<
Record<string, PendingDraftShelfState>
>
>;
type CreationFlowReturnTarget = 'create' | 'draft-shelf';
type Match3DBackgroundCompileTask = {
session: Match3DAgentSessionSnapshot;
payload: CreateMatch3DSessionRequest;
@@ -3300,6 +3301,8 @@ export function PlatformEntryFlowShellImpl({
);
const handledInitialPublicWorkCodeRef = useRef<string | null>(null);
const selectionStageRef = useRef(selectionStage);
const creationFlowReturnTargetRef =
useRef<CreationFlowReturnTarget>('create');
const activeMatch3DGenerationSessionIdRef = useRef<string | null>(null);
const activePuzzleGenerationSessionIdRef = useRef<string | null>(null);
const [draftGenerationNotices, setDraftGenerationNotices] =
@@ -3545,11 +3548,34 @@ export function PlatformEntryFlowShellImpl({
const enterDraftTab = useCallback(() => {
setPlatformTab('saves');
}, [setPlatformTab]);
const markCreationFlowReturnToCreate = useCallback(() => {
creationFlowReturnTargetRef.current = 'create';
}, []);
const markCreationFlowReturnToDraftShelf = useCallback(() => {
creationFlowReturnTargetRef.current = 'draft-shelf';
}, []);
const returnToCreationFlowSource = useCallback(() => {
const returnTarget = creationFlowReturnTargetRef.current;
creationFlowReturnTargetRef.current = 'create';
clearCreationUrlState();
if (returnTarget === 'draft-shelf') {
enterDraftTab();
} else {
enterCreateTab();
}
selectionStageRef.current = 'platform';
setSelectionStage('platform');
}, [enterCreateTab, enterDraftTab, setSelectionStage]);
const shouldReturnToDraftShelf = useCallback(
() => creationFlowReturnTargetRef.current === 'draft-shelf',
[],
);
const returnToCreationCenterFromGeneration = useCallback(() => {
markCreationFlowReturnToCreate();
enterCreateTab();
selectionStageRef.current = 'platform';
setSelectionStage('platform');
}, [enterCreateTab, setSelectionStage]);
}, [enterCreateTab, markCreationFlowReturnToCreate, setSelectionStage]);
const isViewingMatch3DGeneration = useCallback((sessionId: string) => {
return (
selectionStageRef.current === 'match3d-generating' &&
@@ -4717,9 +4743,15 @@ export function PlatformEntryFlowShellImpl({
handleStartNewGame();
}
markCreationFlowReturnToCreate();
sessionController.setCreationTypeError(null);
return true;
}, [handleStartNewGame, hasSavedGame, sessionController]);
}, [
handleStartNewGame,
hasSavedGame,
markCreationFlowReturnToCreate,
sessionController,
]);
const openCreationTypePicker = useCallback(() => {
if (!prepareCreationLaunch()) {
@@ -5905,11 +5937,13 @@ export function PlatformEntryFlowShellImpl({
]);
const openBigFishAgentWorkspace = useCallback(async () => {
markCreationFlowReturnToCreate();
setBigFishRun(null);
await bigFishFlow.openWorkspace();
}, [bigFishFlow]);
}, [bigFishFlow, markCreationFlowReturnToCreate]);
const openSquareHoleAgentWorkspace = useCallback(async () => {
markCreationFlowReturnToCreate();
setSquareHoleSession(null);
setSquareHoleProfile(null);
setSquareHoleRun(null);
@@ -5927,9 +5961,11 @@ export function PlatformEntryFlowShellImpl({
setSquareHoleSession,
setStreamingSquareHoleReplyText,
squareHoleFlow,
markCreationFlowReturnToCreate,
]);
const openMatch3DWorkspace = useCallback(() => {
markCreationFlowReturnToCreate();
setMatch3DRun(null);
setMatch3DProfile(null);
setMatch3DRuntimeProfile(null);
@@ -5946,9 +5982,11 @@ export function PlatformEntryFlowShellImpl({
setMatch3DError,
setSelectionStage,
setStreamingMatch3DReplyText,
markCreationFlowReturnToCreate,
]);
const openJumpHopWorkspace = useCallback(() => {
markCreationFlowReturnToCreate();
setJumpHopError(null);
setJumpHopSession(null);
setJumpHopWork(null);
@@ -5957,9 +5995,10 @@ export function PlatformEntryFlowShellImpl({
enterCreateTab();
setShowCreationTypeModal(false);
setSelectionStage('jump-hop-workspace');
}, [enterCreateTab, setSelectionStage]);
}, [enterCreateTab, markCreationFlowReturnToCreate, setSelectionStage]);
const openWoodenFishWorkspace = useCallback(() => {
markCreationFlowReturnToCreate();
setWoodenFishError(null);
setWoodenFishSession(null);
setWoodenFishWork(null);
@@ -5968,9 +6007,10 @@ export function PlatformEntryFlowShellImpl({
enterCreateTab();
setShowCreationTypeModal(false);
setSelectionStage('wooden-fish-workspace');
}, [enterCreateTab, setSelectionStage]);
}, [enterCreateTab, markCreationFlowReturnToCreate, setSelectionStage]);
const openPuzzleWorkspace = useCallback(() => {
markCreationFlowReturnToCreate();
enterCreateTab();
setShowCreationTypeModal(false);
setPuzzleCreationError(null);
@@ -5981,9 +6021,11 @@ export function PlatformEntryFlowShellImpl({
setPuzzleCreationError,
setPuzzleError,
setSelectionStage,
markCreationFlowReturnToCreate,
]);
const openBarkBattleWorkspace = useCallback(() => {
markCreationFlowReturnToCreate();
setBarkBattleDraftConfig(null);
setBarkBattlePublishedConfig(null);
setBarkBattleRuntimeMode('draft');
@@ -5995,15 +6037,17 @@ export function PlatformEntryFlowShellImpl({
setShowCreationTypeModal(false);
selectionStageRef.current = 'bark-battle-workspace';
setSelectionStage('bark-battle-workspace');
}, [enterCreateTab, setSelectionStage]);
}, [enterCreateTab, markCreationFlowReturnToCreate, setSelectionStage]);
const openVisualNovelWorkspace = useCallback(() => {
markCreationFlowReturnToCreate();
enterCreateTab();
setShowCreationTypeModal(false);
setVisualNovelError(null);
setSelectionStage('visual-novel-agent-workspace');
}, [
enterCreateTab,
markCreationFlowReturnToCreate,
setSelectionStage,
setVisualNovelError,
]);
@@ -6014,6 +6058,7 @@ export function PlatformEntryFlowShellImpl({
return;
}
markCreationFlowReturnToCreate();
enterCreateTab();
setShowCreationTypeModal(false);
setBabyObjectMatchError(null);
@@ -6021,6 +6066,7 @@ export function PlatformEntryFlowShellImpl({
}, [
enterCreateTab,
isBabyObjectMatchVisible,
markCreationFlowReturnToCreate,
sessionController,
setBabyObjectMatchError,
setSelectionStage,
@@ -6039,9 +6085,15 @@ export function PlatformEntryFlowShellImpl({
setIsCreativeAgentStreaming(false);
setCreativeDraftEditError(null);
setIsCreativeDraftEditBusy(false);
markCreationFlowReturnToCreate();
enterCreateTab();
setSelectionStage('platform');
}, [creativeAgentSession, enterCreateTab, setSelectionStage]);
}, [
creativeAgentSession,
enterCreateTab,
markCreationFlowReturnToCreate,
setSelectionStage,
]);
const openCreativeAgentWorkspace = useCallback(async () => {
if (isCreativeAgentBusy || isCreativeAgentStreaming) {
@@ -6049,6 +6101,7 @@ export function PlatformEntryFlowShellImpl({
}
setPuzzleRun(null);
markCreationFlowReturnToCreate();
setPuzzleRuntimeAuthMode('default');
setPuzzleOperation(null);
setPuzzleGenerationState(null);
@@ -6077,6 +6130,7 @@ export function PlatformEntryFlowShellImpl({
enterCreateTab,
isCreativeAgentBusy,
isCreativeAgentStreaming,
markCreationFlowReturnToCreate,
resolvePuzzleErrorMessage,
setSelectionStage,
]);
@@ -6922,9 +6976,9 @@ export function PlatformEntryFlowShellImpl({
setBigFishRuntimeStartedAt(null);
setBigFishRuntimeReturnStage('platform');
setBigFishGenerationState(null);
clearCreationUrlState();
bigFishFlow.leaveFlow();
}, [bigFishFlow]);
returnToCreationFlowSource();
}, [bigFishFlow, returnToCreationFlowSource]);
const leaveMatch3DFlow = useCallback(() => {
setMatch3DRun(null);
@@ -6932,17 +6986,17 @@ export function PlatformEntryFlowShellImpl({
setMatch3DFormDraftPayload(null);
setMatch3DGenerationState(null);
setMatch3DRuntimeReturnStage('match3d-result');
clearCreationUrlState();
match3dFlow.leaveFlow();
}, [match3dFlow, setMatch3DFormDraftPayload]);
returnToCreationFlowSource();
}, [match3dFlow, returnToCreationFlowSource, setMatch3DFormDraftPayload]);
const leaveSquareHoleFlow = useCallback(() => {
setSquareHoleRun(null);
setSquareHoleRuntimeReturnStage('square-hole-result');
setSquareHoleGenerationState(null);
clearCreationUrlState();
squareHoleFlow.leaveFlow();
}, [squareHoleFlow]);
returnToCreationFlowSource();
}, [returnToCreationFlowSource, squareHoleFlow]);
const leaveJumpHopFlow = useCallback(() => {
setJumpHopRun(null);
@@ -6951,9 +7005,8 @@ export function PlatformEntryFlowShellImpl({
setJumpHopGenerationState(null);
setJumpHopSession(null);
setJumpHopError(null);
clearCreationUrlState();
setSelectionStage('platform');
}, [setSelectionStage]);
returnToCreationFlowSource();
}, [returnToCreationFlowSource]);
const leaveWoodenFishFlow = useCallback(() => {
setWoodenFishRun(null);
@@ -6962,9 +7015,8 @@ export function PlatformEntryFlowShellImpl({
setWoodenFishGenerationState(null);
setWoodenFishSession(null);
setWoodenFishError(null);
clearCreationUrlState();
setSelectionStage('platform');
}, [setSelectionStage]);
returnToCreationFlowSource();
}, [returnToCreationFlowSource]);
const createReadyJumpHopGenerationState = useCallback(
(state: MiniGameDraftGenerationState) =>
@@ -6992,10 +7044,8 @@ export function PlatformEntryFlowShellImpl({
setBarkBattleError(null);
setBarkBattleGenerationPartialFailed(false);
setIsBarkBattleBusy(false);
clearCreationUrlState();
selectionStageRef.current = 'platform';
setSelectionStage('platform');
}, [setSelectionStage]);
returnToCreationFlowSource();
}, [returnToCreationFlowSource]);
const createBarkBattleGeneratingDraft = useCallback(
async (payload: BarkBattleConfigEditorPayload) => {
@@ -7276,10 +7326,10 @@ export function PlatformEntryFlowShellImpl({
setActiveCreativeAgentSessionId(null);
setCreativeDraftEditError(null);
resetRecommendRuntimeSelection();
clearCreationUrlState();
clearPuzzleRuntimeUrlState();
puzzleFlow.leaveFlow();
}, [puzzleFlow, resetRecommendRuntimeSelection]);
returnToCreationFlowSource();
}, [puzzleFlow, resetRecommendRuntimeSelection, returnToCreationFlowSource]);
const leaveVisualNovelFlow = useCallback(() => {
setVisualNovelWork(null);
@@ -7288,9 +7338,9 @@ export function PlatformEntryFlowShellImpl({
setVisualNovelFormDraftPayload(null);
setVisualNovelGenerationStartedAtMs(null);
setVisualNovelGenerationPhase('generating');
clearCreationUrlState();
visualNovelFlow.leaveFlow();
}, [visualNovelFlow]);
returnToCreationFlowSource();
}, [returnToCreationFlowSource, visualNovelFlow]);
const leaveBabyObjectMatchFlow = useCallback(() => {
setBabyObjectMatchDraft(null);
@@ -7298,11 +7348,8 @@ export function PlatformEntryFlowShellImpl({
setBabyObjectMatchGenerationState(null);
setBabyObjectMatchGenerationPhase('generating');
setBabyObjectMatchError(null);
clearCreationUrlState();
enterCreateTab();
selectionStageRef.current = 'platform';
setSelectionStage('platform');
}, [enterCreateTab, setSelectionStage]);
returnToCreationFlowSource();
}, [returnToCreationFlowSource]);
const saveBabyObjectMatchResultDraft = useCallback(
async (draft: BabyObjectMatchDraft) => {
@@ -10079,7 +10126,6 @@ export function PlatformEntryFlowShellImpl({
}, [activeRecommendRuntimeKind, setPuzzleError]);
const leaveAgentWorkspace = useCallback(() => {
enterCreateTab();
sessionController.resetSessionViewState();
sessionController.setGeneratedCustomWorldProfile(null);
autosaveCoordinator.resetAutoSaveTrackingToIdle();
@@ -10087,12 +10133,11 @@ export function PlatformEntryFlowShellImpl({
sessionController.activeAgentSessionId,
null,
);
setSelectionStage('platform');
returnToCreationFlowSource();
}, [
autosaveCoordinator,
enterCreateTab,
returnToCreationFlowSource,
sessionController,
setSelectionStage,
]);
const leaveAgentDraftGeneration = useCallback(() => {
@@ -10112,13 +10157,11 @@ export function PlatformEntryFlowShellImpl({
autosaveCoordinator.resetAutoSaveTrackingToIdle();
sessionController.setCustomWorldGenerationViewSource(null);
sessionController.setCustomWorldResultViewSource(null);
enterCreateTab();
setSelectionStage('platform');
returnToCreationFlowSource();
}, [
autosaveCoordinator,
enterCreateTab,
returnToCreationFlowSource,
sessionController,
setSelectionStage,
]);
const leaveCustomWorldResult = useCallback(() => {
@@ -10127,12 +10170,18 @@ export function PlatformEntryFlowShellImpl({
autosaveCoordinator.resetAutoSaveTrackingToIdle();
sessionController.setCustomWorldGenerationViewSource(null);
sessionController.setCustomWorldResultViewSource(null);
if (shouldReturnToDraftShelf()) {
returnToCreationFlowSource();
return;
}
setSelectionStage(selectedDetailEntry ? 'detail' : 'platform');
}, [
autosaveCoordinator,
returnToCreationFlowSource,
selectedDetailEntry,
sessionController,
setSelectionStage,
shouldReturnToDraftShelf,
]);
const handleStartSelectedWorld = useCallback(() => {
@@ -14135,6 +14184,7 @@ export function PlatformEntryFlowShellImpl({
}}
onOpenDraft={(item) => {
runProtectedAction(() => {
markCreationFlowReturnToDraftShelf();
void detailNavigation.handleOpenCreationWork(item);
});
}}
@@ -14146,6 +14196,7 @@ export function PlatformEntryFlowShellImpl({
if (!matchedWork) {
return;
}
markCreationFlowReturnToDraftShelf();
void detailNavigation.handleOpenCreationWork(matchedWork);
});
}}
@@ -14160,6 +14211,7 @@ export function PlatformEntryFlowShellImpl({
isBigFishCreationVisible
? (item) => {
runProtectedAction(() => {
markCreationFlowReturnToDraftShelf();
void openBigFishDraft(item);
});
}
@@ -14169,6 +14221,7 @@ export function PlatformEntryFlowShellImpl({
isJumpHopCreationVisible
? (item) => {
runProtectedAction(() => {
markCreationFlowReturnToDraftShelf();
void openJumpHopDraft(item);
});
}
@@ -14185,6 +14238,7 @@ export function PlatformEntryFlowShellImpl({
match3dItems={match3dShelfItems}
onOpenMatch3DDetail={(item) => {
runProtectedAction(() => {
markCreationFlowReturnToDraftShelf();
void openMatch3DDraft(item);
});
}}
@@ -14198,6 +14252,7 @@ export function PlatformEntryFlowShellImpl({
isSquareHoleCreationVisible
? (item) => {
runProtectedAction(() => {
markCreationFlowReturnToDraftShelf();
void openSquareHoleDraft(item);
});
}
@@ -14213,6 +14268,7 @@ export function PlatformEntryFlowShellImpl({
puzzleItems={puzzleShelfItems}
onOpenPuzzleDetail={(item) => {
runProtectedAction(() => {
markCreationFlowReturnToDraftShelf();
void openPuzzleDraft(item);
});
}}
@@ -14228,6 +14284,7 @@ export function PlatformEntryFlowShellImpl({
}
onOpenBabyObjectMatchDetail={(item) => {
runProtectedAction(() => {
markCreationFlowReturnToDraftShelf();
openBabyObjectMatchDraft(item);
});
}}
@@ -14237,12 +14294,14 @@ export function PlatformEntryFlowShellImpl({
barkBattleItems={barkBattleShelfItems}
onOpenBarkBattleDetail={(item) => {
runProtectedAction(() => {
markCreationFlowReturnToDraftShelf();
openBarkBattleDraft(item);
});
}}
visualNovelItems={visualNovelShelfItems}
onOpenVisualNovelDetail={(item) => {
runProtectedAction(() => {
markCreationFlowReturnToDraftShelf();
void openVisualNovelDraft(item);
});
}}
@@ -14691,6 +14750,10 @@ export function PlatformEntryFlowShellImpl({
isBusy={isBigFishBusy}
error={bigFishError}
onBack={() => {
if (shouldReturnToDraftShelf()) {
leaveBigFishFlow();
return;
}
setSelectionStage('big-fish-agent-workspace');
}}
onDismissError={() => {
@@ -14840,6 +14903,10 @@ export function PlatformEntryFlowShellImpl({
isBusy={isMatch3DBusy}
error={match3dError}
onBack={() => {
if (shouldReturnToDraftShelf()) {
leaveMatch3DFlow();
return;
}
returnToCreationCenterFromGeneration();
}}
onSaved={(profile) => {
@@ -15081,6 +15148,10 @@ export function PlatformEntryFlowShellImpl({
isBusy={isBabyObjectMatchBusy}
error={babyObjectMatchError}
onBack={() => {
if (shouldReturnToDraftShelf()) {
leaveBabyObjectMatchFlow();
return;
}
setSelectionStage('baby-object-match-workspace');
}}
onSaveDraft={(draft) => {
@@ -15266,6 +15337,10 @@ export function PlatformEntryFlowShellImpl({
isBusy={isSquareHoleBusy}
error={squareHoleError}
onBack={() => {
if (shouldReturnToDraftShelf()) {
leaveSquareHoleFlow();
return;
}
setSelectionStage('square-hole-agent-workspace');
}}
onSaved={(profile) => {
@@ -15903,6 +15978,10 @@ export function PlatformEntryFlowShellImpl({
isBusy={isVisualNovelBusy}
error={visualNovelError}
onBack={() => {
if (shouldReturnToDraftShelf()) {
leaveVisualNovelFlow();
return;
}
setSelectionStage('visual-novel-agent-workspace');
}}
onSaveDraft={(draft) => {
@@ -16181,6 +16260,10 @@ export function PlatformEntryFlowShellImpl({
: barkBattleError
}
onBack={() => {
if (shouldReturnToDraftShelf()) {
leaveBarkBattleFlow();
return;
}
enterCreateTab();
selectionStageRef.current = 'platform';
setSelectionStage('platform');

View File

@@ -324,6 +324,13 @@ function getPlatformTabPanel(tab: string) {
return panel;
}
async function findPlatformTabPanel(tab: string) {
await waitFor(() => {
expect(document.getElementById(`platform-tab-panel-${tab}`)).toBeTruthy();
});
return getPlatformTabPanel(tab);
}
const testCreationEntryConfig = {
startCard: {
title: '新建作品',
@@ -6938,7 +6945,7 @@ test('match3d creation tab stays usable even when public galleries fail', async
expect(match3dCreationClient.createSession).not.toHaveBeenCalled();
});
test('puzzle draft result back button returns to creation hub', async () => {
test('puzzle draft result back button returns to draft hub when opened from shelf', async () => {
const user = userEvent.setup();
vi.mocked(listPuzzleWorks).mockResolvedValue({
@@ -6979,10 +6986,16 @@ test('puzzle draft result back button returns to creation hub', async () => {
await user.click(screen.getByRole('button', { name: '返回' }));
const draftPanel = await findPlatformTabPanel('saves');
await waitFor(() => {
expect(draftPanel.getAttribute('aria-hidden')).toBe('false');
});
expect(
await screen.findByRole('tablist', { name: '玩法模板分类' }),
within(draftPanel).getByRole('tablist', { name: '作品筛选' }),
).toBeTruthy();
expect(await screen.findByText('拼图工作区missing-session')).toBeTruthy();
expect(within(draftPanel).getByText('雨夜猫塔')).toBeTruthy();
expect(getPlatformTabPanel('create').getAttribute('aria-hidden')).toBe('true');
expect(screen.queryByText('拼图工作区missing-session')).toBeNull();
expect(
screen.queryByText('雨夜里有一只会发光的猫站在遗迹台阶上。'),
).toBeNull();
@@ -8996,7 +9009,7 @@ test('agent result view does not keep legacy publish blockers when preview uses
expect((actionButton as HTMLButtonElement).disabled).toBe(false);
});
test('agent draft result back button returns to creation hub without syncing result profile', async () => {
test('agent draft result back button returns to draft hub without syncing result profile', async () => {
const user = userEvent.setup();
const resultSession = {
@@ -9153,22 +9166,32 @@ test('agent draft result back button returns to creation hub without syncing res
},
{ timeout: 2500 },
);
const syncCallsBeforeBack = vi
.mocked(executeRpgCreationAction)
.mock.calls.filter(
([sessionId, payload]) =>
sessionId === 'custom-world-agent-session-1' &&
payload?.action === 'sync_result_profile',
).length;
await user.click(screen.getByRole('button', { name: /返回创作/u }));
const draftPanel = await findPlatformTabPanel('saves');
await waitFor(() => {
expect(screen.getByRole('tablist', { name: '玩法模板分类' })).toBeTruthy();
expect(draftPanel.getAttribute('aria-hidden')).toBe('false');
});
expect(within(draftPanel).getByRole('tablist', { name: '作品筛选' })).toBeTruthy();
expect(getPlatformTabPanel('create').getAttribute('aria-hidden')).toBe('true');
expect(
vi
.mocked(executeRpgCreationAction)
.mock.calls.some(
.mock.calls.filter(
([sessionId, payload]) =>
sessionId === 'custom-world-agent-session-1' &&
payload?.action === 'sync_result_profile',
),
).toBe(false);
).length,
).toBe(syncCallsBeforeBack);
expect(screen.queryByText('世界档案')).toBeNull();
});