fix: keep draft form on mud-point failure
This commit is contained in:
@@ -2715,6 +2715,10 @@ export function PlatformEntryFlowShellImpl({
|
||||
? 'platform-theme--dark'
|
||||
: 'platform-theme--light';
|
||||
const [showCreationTypeModal, setShowCreationTypeModal] = useState(false);
|
||||
const [draftGenerationPointNotice, setDraftGenerationPointNotice] = useState<{
|
||||
title: string;
|
||||
message: string;
|
||||
} | null>(null);
|
||||
const [selectedDetailEntry, setSelectedDetailEntry] =
|
||||
useState<CustomWorldLibraryEntry<CustomWorldProfile> | null>(null);
|
||||
const [selectedPublicWorkDetail, setSelectedPublicWorkDetail] =
|
||||
@@ -3299,7 +3303,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
[draftGenerationNotices],
|
||||
);
|
||||
const ensureEnoughDraftGenerationPointsFromServer = useCallback(
|
||||
async (pointsCost: number, setError: (message: string | null) => void) => {
|
||||
async (pointsCost: number) => {
|
||||
try {
|
||||
const latestDashboard = await getPlatformProfileDashboard(
|
||||
RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS,
|
||||
@@ -3307,25 +3311,26 @@ export function PlatformEntryFlowShellImpl({
|
||||
platformBootstrap.setProfileDashboard(latestDashboard);
|
||||
const walletBalance = resolveProfileWalletBalance(latestDashboard);
|
||||
if (walletBalance >= pointsCost) {
|
||||
setDraftGenerationPointNotice(null);
|
||||
return true;
|
||||
}
|
||||
|
||||
setError(
|
||||
`泥点不足,本次需要 ${pointsCost} 泥点,当前 ${walletBalance} 泥点。`,
|
||||
setDraftGenerationPointNotice(
|
||||
{
|
||||
title: '泥点不足',
|
||||
message: `本次需要 ${pointsCost} 泥点,当前 ${walletBalance} 泥点。`,
|
||||
},
|
||||
);
|
||||
enterCreateTab();
|
||||
selectionStageRef.current = 'platform';
|
||||
setSelectionStage('platform');
|
||||
return false;
|
||||
} catch {
|
||||
setError('读取泥点余额失败,请稍后重试。');
|
||||
enterCreateTab();
|
||||
selectionStageRef.current = 'platform';
|
||||
setSelectionStage('platform');
|
||||
setDraftGenerationPointNotice({
|
||||
title: '读取泥点余额失败',
|
||||
message: '请稍后重试。',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[enterCreateTab, platformBootstrap, setSelectionStage],
|
||||
[platformBootstrap],
|
||||
);
|
||||
|
||||
const resolveBigFishErrorMessage = useCallback(
|
||||
@@ -5274,30 +5279,27 @@ export function PlatformEntryFlowShellImpl({
|
||||
setPuzzleError(null);
|
||||
return ensureEnoughDraftGenerationPointsFromServer(
|
||||
PUZZLE_DRAFT_GENERATION_POINT_COST,
|
||||
(message) => {
|
||||
setPuzzleCreationError(message);
|
||||
setPuzzleError(message);
|
||||
},
|
||||
);
|
||||
}, [
|
||||
ensureEnoughDraftGenerationPointsFromServer,
|
||||
setPuzzleCreationError,
|
||||
setPuzzleError,
|
||||
]);
|
||||
const preflightMatch3DDraftGeneration = useCallback(async () => {
|
||||
setMatch3DError(null);
|
||||
return ensureEnoughDraftGenerationPointsFromServer(
|
||||
MATCH3D_DRAFT_GENERATION_POINT_COST,
|
||||
setMatch3DError,
|
||||
);
|
||||
}, [ensureEnoughDraftGenerationPointsFromServer, setMatch3DError]);
|
||||
}, [ensureEnoughDraftGenerationPointsFromServer]);
|
||||
const preflightBarkBattleDraftGeneration = useCallback(async () => {
|
||||
setBarkBattleError(null);
|
||||
return ensureEnoughDraftGenerationPointsFromServer(
|
||||
BARK_BATTLE_DRAFT_GENERATION_POINT_COST,
|
||||
setBarkBattleError,
|
||||
);
|
||||
}, [ensureEnoughDraftGenerationPointsFromServer]);
|
||||
const draftGenerationPointNoticeDescription = draftGenerationPointNotice
|
||||
? draftGenerationPointNotice.title === '读取泥点余额失败'
|
||||
? '当前表单不会丢失,关闭后可继续编辑,稍后再试。'
|
||||
: '当前表单不会丢失,关闭后可继续编辑或补足泥点再继续。'
|
||||
: undefined;
|
||||
const recoverCompletedPuzzleDraftGeneration = useCallback(
|
||||
async ({
|
||||
sessionId,
|
||||
@@ -15687,6 +15689,29 @@ export function PlatformEntryFlowShellImpl({
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<UnifiedModal
|
||||
open={Boolean(draftGenerationPointNotice)}
|
||||
title={draftGenerationPointNotice?.title ?? '泥点提示'}
|
||||
description={draftGenerationPointNoticeDescription}
|
||||
onClose={() => setDraftGenerationPointNotice(null)}
|
||||
closeOnBackdrop
|
||||
size="sm"
|
||||
overlayClassName={`platform-theme ${platformThemeClass} !items-center`}
|
||||
panelClassName="platform-remap-surface rounded-[1.75rem]"
|
||||
footer={
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setDraftGenerationPointNotice(null)}
|
||||
className="platform-button platform-button--primary min-h-0 rounded-full px-4 py-2 text-sm"
|
||||
>
|
||||
知道了
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<div className="text-sm leading-6 text-[var(--platform-text-base)]">
|
||||
{draftGenerationPointNotice?.message}
|
||||
</div>
|
||||
</UnifiedModal>
|
||||
<PublishShareModal
|
||||
open={Boolean(publishSharePayload)}
|
||||
payload={publishSharePayload}
|
||||
|
||||
@@ -315,3 +315,36 @@ test('groups visible platform creation types by backend category metadata', () =
|
||||
]);
|
||||
expect(groups[1]?.items.map((item) => item.id)).toEqual(['visual-novel']);
|
||||
});
|
||||
|
||||
test('falls back when backend creation type category metadata is missing', () => {
|
||||
const cards = derivePlatformCreationTypes([
|
||||
{
|
||||
id: 'legacy-entry',
|
||||
title: '历史入口',
|
||||
subtitle: '旧数据缺少分类字段',
|
||||
badge: '可创建',
|
||||
imageSrc: '/creation-type-references/puzzle.webp',
|
||||
visible: true,
|
||||
open: true,
|
||||
sortOrder: 10,
|
||||
categoryId: undefined as unknown as string,
|
||||
categoryLabel: undefined as unknown as string,
|
||||
categorySortOrder: 0,
|
||||
updatedAtMicros: 1,
|
||||
},
|
||||
]);
|
||||
|
||||
expect(cards[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
id: 'legacy-entry',
|
||||
categoryId: 'recent',
|
||||
categoryLabel: '最近创作',
|
||||
}),
|
||||
);
|
||||
expect(groupVisiblePlatformCreationTypes(cards)).toEqual([
|
||||
expect.objectContaining({
|
||||
id: 'recent',
|
||||
label: '最近创作',
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -55,13 +55,13 @@ export function isPlatformCreationTypeOpen(
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeCategoryId(value: string) {
|
||||
const normalized = value.trim();
|
||||
function normalizeCategoryId(value: string | null | undefined) {
|
||||
const normalized = typeof value === 'string' ? value.trim() : '';
|
||||
return normalized || FALLBACK_CREATION_CATEGORY_ID;
|
||||
}
|
||||
|
||||
function normalizeCategoryLabel(value: string) {
|
||||
const normalized = value.trim();
|
||||
function normalizeCategoryLabel(value: string | null | undefined) {
|
||||
const normalized = typeof value === 'string' ? value.trim() : '';
|
||||
return normalized || FALLBACK_CREATION_CATEGORY_LABEL;
|
||||
}
|
||||
|
||||
|
||||
@@ -1085,6 +1085,10 @@ vi.mock('../bark-battle-creation/BarkBattleConfigEditor', () => ({
|
||||
}) => (
|
||||
<div className="bark-battle-config-editor-mock">
|
||||
<div>汪汪声浪配置表单</div>
|
||||
<label>
|
||||
作品标题
|
||||
<input aria-label="汪汪作品标题" defaultValue="汪汪测试杯" />
|
||||
</label>
|
||||
<div data-testid="bark-battle-editor-back-state">
|
||||
{showBackButton ? 'back-visible' : 'back-hidden'}
|
||||
</div>
|
||||
@@ -3581,11 +3585,20 @@ test('bark battle form checks mud points before creating image assets', async ()
|
||||
|
||||
await openCreateTemplateHub(user);
|
||||
await user.click(await findCreationTypeButton('汪汪声浪'));
|
||||
const titleInput = await screen.findByLabelText('汪汪作品标题');
|
||||
await user.clear(titleInput);
|
||||
await user.type(titleInput, '自定义声浪杯');
|
||||
await user.click(await screen.findByRole('button', { name: '生成草稿' }));
|
||||
|
||||
const noticeDialog = await screen.findByRole('dialog', { name: '泥点不足' });
|
||||
expect(
|
||||
await screen.findByText('泥点不足,本次需要 3 泥点,当前 2 泥点。'),
|
||||
within(noticeDialog).getByText('本次需要 3 泥点,当前 2 泥点。'),
|
||||
).toBeTruthy();
|
||||
expect(screen.getByText('汪汪声浪配置表单')).toBeTruthy();
|
||||
expect(screen.queryByRole('tablist', { name: '玩法模板分类' })).toBeNull();
|
||||
expect((screen.getByLabelText('汪汪作品标题') as HTMLInputElement).value).toBe(
|
||||
'自定义声浪杯',
|
||||
);
|
||||
expect(createBarkBattleDraft).not.toHaveBeenCalled();
|
||||
expect(generateAllBarkBattleImageAssets).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -4302,11 +4315,15 @@ test('puzzle form checks mud points before creating a draft', async () => {
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await openCreateTemplateHub(user);
|
||||
await user.click(await findCreationTypeButton('拼图'));
|
||||
await user.click(await screen.findByRole('button', { name: '生成草稿' }));
|
||||
|
||||
const noticeDialog = await screen.findByRole('dialog', { name: '泥点不足' });
|
||||
expect(
|
||||
await screen.findByText('泥点不足,本次需要 2 泥点,当前 1 泥点。'),
|
||||
within(noticeDialog).getByText('本次需要 2 泥点,当前 1 泥点。'),
|
||||
).toBeTruthy();
|
||||
expect(screen.getByText('拼图工作区:missing-session')).toBeTruthy();
|
||||
expect(screen.queryByRole('tablist', { name: '玩法模板分类' })).toBeNull();
|
||||
expect(createPuzzleAgentSession).not.toHaveBeenCalled();
|
||||
expect(executePuzzleAgentAction).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -4323,14 +4340,17 @@ test('match3d form checks mud points before creating a draft', async () => {
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await openCreateTemplateHub(user);
|
||||
await user.click(screen.getByRole('tab', { name: '抓大鹅' }));
|
||||
await user.click(await findCreationTypeButton('抓大鹅'));
|
||||
await user.click(
|
||||
await screen.findByRole('button', { name: '生成抓大鹅草稿' }),
|
||||
);
|
||||
|
||||
const noticeDialog = await screen.findByRole('dialog', { name: '泥点不足' });
|
||||
expect(
|
||||
await screen.findByText('泥点不足,本次需要 10 泥点,当前 9 泥点。'),
|
||||
within(noticeDialog).getByText('本次需要 10 泥点,当前 9 泥点。'),
|
||||
).toBeTruthy();
|
||||
expect(screen.getByText('抓大鹅工作区:missing-session')).toBeTruthy();
|
||||
expect(screen.queryByRole('tablist', { name: '玩法模板分类' })).toBeNull();
|
||||
expect(match3dCreationClient.createSession).not.toHaveBeenCalled();
|
||||
expect(match3dCreationClient.executeAction).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user