From e6e0f931023f86e06e3b544a4fa6a61d0eb52f7f Mon Sep 17 00:00:00 2001 From: kdletters Date: Thu, 4 Jun 2026 02:04:53 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E6=94=B6=E5=8F=A3=E5=88=9B?= =?UTF-8?q?=E4=BD=9C=E6=81=A2=E5=A4=8D=E8=BA=AB=E4=BB=BD=E5=8C=B9=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .hermes/shared-memory/decision-log.md | 1 + docs/README.md | 2 +- ...】CreationUrlStateModel收口计划-2026-06-03.md | 4 +- ...玩法创作】平台入口与玩法链路-2026-05-15.md | 2 +- .../PlatformEntryFlowShellImpl.tsx | 70 +++++----- .../platformCreationUrlStateModel.test.ts | 130 +++++++++++++++++ .../platformCreationUrlStateModel.ts | 132 ++++++++++++++++-- 7 files changed, 288 insertions(+), 53 deletions(-) diff --git a/.hermes/shared-memory/decision-log.md b/.hermes/shared-memory/decision-log.md index 24fa12c2..4e03c97c 100644 --- a/.hermes/shared-memory/decision-log.md +++ b/.hermes/shared-memory/decision-log.md @@ -1341,6 +1341,7 @@ - 决策:新增 `src/components/platform-entry/platformCreationUrlStateModel.ts` 作为 Creation URL State Module,Interface 收口各玩法 `build*CreationUrlState`、拼图 `buildPuzzle*RuntimeUrlState`、URL state 非空判断和 runtime state key;新增 `src/components/platform-entry/platformPuzzleIdentityModel.ts` 作为拼图稳定身份 Module,`platformDraftGenerationShelfModel.ts` 仅 re-export 旧入口以保持兼容。`PlatformEntryFlowShellImpl.tsx` 只保留路由、URL 写入和网络副作用 Adapter。 - 追加决策:初始创作 URL 恢复的已处理、非创作路径、无私有 query、平台配置加载中、受保护数据暂不可读与可恢复判定也收口到 `resolveInitialCreationUrlRestoreDecision`;壳层只按 `skip`、`mark-handled`、`wait`、`restore` 执行 ref 标记或进入原恢复副作用。 - 追加决策:创作直达恢复目标解析收口到 `resolveCreationUrlRestoreTarget(pathname, state)`;Module 统一识别 big-fish、match3d、square-hole、puzzle、visual-novel、bark-battle、baby-object-match、jump-hop、wooden-fish 的 path、私有 query 归一化、生成路径标记和 big-fish workId 到 sessionId 兜底。壳层仍执行作品列表读取、草稿恢复、错误处理、stage 切换和 URL 写回;`/creation/rpg` 继续保持无具体恢复目标,后续要接入需先补规则与测试。 +- 追加决策:创作 URL 恢复的作品 / 草稿身份匹配谓词、以及跳一跳 / 敲木鱼恢复后的阶段落点也归入 `platformCreationUrlStateModel.ts`。身份匹配只允许非空目标值命中,避免 query 缺失时用空值误开草稿;壳层只把已读取的列表项、session 或 work 交给 Module 判定,然后执行对应打开 / restore 副作用。 - 影响范围:创作流程刷新恢复、拼图草稿 / 发布 runtime 深链、作品架打开试玩、跳一跳 / 敲木鱼 work-backed 恢复、Bark Battle / 宝贝识物本地草稿恢复。 - 验证方式:`npm run test -- src/components/platform-entry/platformCreationUrlStateModel.test.ts src/components/platform-entry/platformPuzzleIdentityModel.test.ts`、`npm run test -- src/services/creationUrlState.test.ts`、`npm run test -- src/components/platform-entry/platformDraftGenerationShelfModel.test.ts`、针对新 Module 执行 ESLint、`npm run typecheck`、`npm run check:encoding`。 - 关联文档:`docs/technical/【前端架构】CreationUrlStateModel收口计划-2026-06-03.md`。 diff --git a/docs/README.md b/docs/README.md index 53de69ab..036b6d89 100644 --- a/docs/README.md +++ b/docs/README.md @@ -49,7 +49,7 @@ AI 文字游戏模板接入以 [AI_NATIVE_TEXT_GAME_TEMPLATE_MOKU_REFERENCE_PRD_ 平台入口创作生成通知、pending 作品架占位、失败覆盖、拼图稳定 ID 和草稿 Tab 未读点收口到 `src/components/platform-entry/platformDraftGenerationShelfModel.ts`,规则见 [【前端架构】DraftGenerationShelfModel收口计划-2026-06-03.md](./technical/%E3%80%90%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84%E3%80%91DraftGenerationShelfModel%E6%94%B6%E5%8F%A3%E8%AE%A1%E5%88%92-2026-06-03.md)。 -平台入口创作恢复 URL 私有 query、初始恢复判定、创作直达恢复目标解析、拼图 runtime query 与拼图稳定身份互推收口到 `src/components/platform-entry/platformCreationUrlStateModel.ts` 和 `src/components/platform-entry/platformPuzzleIdentityModel.ts`,规则见 [【前端架构】CreationUrlStateModel收口计划-2026-06-03.md](./technical/【前端架构】CreationUrlStateModel收口计划-2026-06-03.md)。 +平台入口创作恢复 URL 私有 query、初始恢复判定、创作直达恢复目标解析、恢复目标身份匹配、跳一跳 / 敲木鱼恢复阶段落点、拼图 runtime query 与拼图稳定身份互推收口到 `src/components/platform-entry/platformCreationUrlStateModel.ts` 和 `src/components/platform-entry/platformPuzzleIdentityModel.ts`,规则见 [【前端架构】CreationUrlStateModel收口计划-2026-06-03.md](./technical/【前端架构】CreationUrlStateModel收口计划-2026-06-03.md)。 平台入口错误 / 完成弹窗的文案归一、来源格式、候选择一、dismiss key 与任务完成文案收口到 `src/components/platform-entry/platformDialogStateModel.ts`,规则见 [【前端架构】PlatformDialogStateModel收口计划-2026-06-03.md](./technical/【前端架构】PlatformDialogStateModel收口计划-2026-06-03.md)。 diff --git a/docs/technical/【前端架构】CreationUrlStateModel收口计划-2026-06-03.md b/docs/technical/【前端架构】CreationUrlStateModel收口计划-2026-06-03.md index 340f4220..a6612bb7 100644 --- a/docs/technical/【前端架构】CreationUrlStateModel收口计划-2026-06-03.md +++ b/docs/technical/【前端架构】CreationUrlStateModel收口计划-2026-06-03.md @@ -7,7 +7,7 @@ ## 决策 - 新增 `src/components/platform-entry/platformCreationUrlStateModel.ts` 作为 Creation URL State Module。 -- 该 Module 的 Interface 收口为各玩法 `build*CreationUrlState`、拼图 `buildPuzzle*RuntimeUrlState`、`normalizeCreationUrlValue`、`hasCreationUrlStateValue`、`hasPuzzleRuntimeUrlStateValue`、`buildPuzzleRuntimeUrlStateKey`、初始创作 URL 恢复判定 `resolveInitialCreationUrlRestoreDecision`,以及创作直达恢复目标解析 `resolveCreationUrlRestoreTarget`。 +- 该 Module 的 Interface 收口为各玩法 `build*CreationUrlState`、拼图 `buildPuzzle*RuntimeUrlState`、`normalizeCreationUrlValue`、`hasCreationUrlStateValue`、`hasPuzzleRuntimeUrlStateValue`、`buildPuzzleRuntimeUrlStateKey`、初始创作 URL 恢复判定 `resolveInitialCreationUrlRestoreDecision`、创作直达恢复目标解析 `resolveCreationUrlRestoreTarget`、恢复目标身份匹配谓词,以及跳一跳 / 敲木鱼恢复后的阶段落点判定。 - 新增 `src/components/platform-entry/platformPuzzleIdentityModel.ts` 作为拼图稳定身份 Module,统一 `puzzle-session-*`、`puzzle-profile-*`、`puzzle-work-*` 的互推规则。 - `PlatformEntryFlowShellImpl.tsx` 保留 React state、路由、登录门禁、网络请求和 URL 写入副作用 Adapter;不再在壳层内定义各玩法 URL 状态构造函数,也不直接内联初始恢复的已处理 / 等待 / 可恢复判定。 @@ -20,6 +20,8 @@ - 拼图 draft runtime 若没有 `sourceSessionId`,只允许从 `puzzle-profile-*` 反推出 `puzzle-session-*`。 - 初始创作 URL 恢复只在未处理、当前路径属于创作恢复路径、私有 query 有值、平台配置加载完成且受保护数据可读时执行;非创作路径或无私有 query 时标记已处理,加载中或暂不可读时等待。 - 创作直达恢复目标由 `resolveCreationUrlRestoreTarget(pathname, state)` 统一识别;它只返回玩法 kind、归一化后的四个私有 query、生成路径标记和大鱼吃小鱼 session 兜底,不执行网络请求、草稿打开、stage 切换或 URL 写回。 +- 作品 / 草稿身份匹配只允许非空目标值命中,避免 query 缺失时用 `null` / 空值误匹配到无效草稿。匹配谓词仍只判断身份,不触发列表读取或打开动作。 +- 跳一跳和敲木鱼的恢复阶段落点由 `resolveJumpHopCreationUrlRestoreStage` 与 `resolveWoodenFishCreationUrlRestoreStage` 决定;生成路径优先进入生成页,否则按是否恢复到 draft / work 落到结果页或工作台。 - `/creation/rpg` 当前仍不归入具体恢复目标;若后续要恢复 RPG 直达,需要先补明确恢复规则和测试,不得让壳层重新内联路径判定。 ## Depth / Leverage / Locality diff --git a/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md b/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md index 97d37305..b40e8bde 100644 --- a/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md +++ b/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md @@ -8,7 +8,7 @@ 当前点击底部加号进入的创作入口页承载后台公告位、创作入口页签和两列模板卡;页签中只有真实后端作品架摘要存在时才展示“最近创作”,其余为玩法模板分类。点击模板卡后直接进入对应玩法已有的入口创作表单 stage,不再经过空白占位页,也不把旧表单嵌进创作入口页。移动端创作入口页顶栏在 `陶泥儿` 品牌同一行显示真实账户泥点数,数据来自 `profileDashboard.walletBalance`,不得再把公告内容或活动奖池当作账号余额展示。创作入口页公告位数据优先读取 `GET /api/creation-entry/config` 的 `eventBanners` 数组,多条配置时前端自动轮播,旧 `eventBanner` 仅作为单条兼容兜底。后台公告配置面向表单:每条公告包含标题和 HTML 内容,后台保存时序列化为后端 `eventBannersJson` 传输字段,由前端空权限沙箱 iframe 展示;旧结构化 banner 字段仅保留回显兼容,不再作为后台公告配置主格式;不得执行 JSX 或把后台代码直接注入 DOM。玩法列表不再套外部边框卡片,移动端需要压缩横向边距和两列间距;玩法卡统一按“上图、左上状态标签(仅非开放态显示)、封面右下 `10-20泥点数`、下方白底标题/描述”结构展示,卡片高度保持紧凑但标题、描述和预估消耗点数都必须可见。创作入口页根容器不再使用 `platform-page-stage` 这类全局内容卡片壳,但继续保留 `platform-remap-surface` 作为主题和输入框样式命中钩子。创作入口页字号需要对齐平台普通 UI 档位:顶栏泥点组件、公告正文、分类 Tab 和玩法卡标题 / 副标题 / 消耗说明优先使用 `11px` 到 `14px`,不使用 `text-lg`、`text-xl` 或更大的展示级字号。草稿 Tab 继续承接作品架;底部加号入口页的“最近创作”只用 7 天内的真实后端作品架摘要判断是否展示,并从摘要里推导最近使用过的模板 ID,页面必须展示“仅显示最近7天内使用过的模板”提示,列表内容必须复用其它页签里的模板卡样式、文案和点击行为,不展示具体作品名称、摘要或生成状态,也不新增独立最近创作卡组件。RPG、RPG 之外的各玩法入口分别落到既有的 `agent-workspace`、`big-fish-agent-workspace`、`match3d-agent-workspace`、`square-hole-agent-workspace`、`jump-hop-workspace`、`wooden-fish-workspace`、`puzzle-agent-workspace`、`bark-battle-workspace`、`visual-novel-agent-workspace`、`baby-object-match-workspace`,这些入口继续承接各玩法自己的表单、草稿恢复和后续编排,不作为创作入口页内容。 -创作恢复参数只保留 `sessionId`、`profileId`、`draftId`、`workId` 这四个私有 query。它们只允许在同一条创作链路的结果页、生成页、工作台之间保留;切到首页、公开作品详情、runtime 或另一条玩法链路时必须清掉。平台入口刷新直达时,路径到玩法恢复目标、四个 query 归一化、生成页标记和大鱼吃小鱼 workId 兜底统一由 `platformCreationUrlStateModel.ts` 解析,壳层只执行读取作品、恢复草稿和切换阶段等副作用。生成页等待时间统一以生成状态里的 `startedAtMs` 为准;创建该状态时优先使用后端 session 下发的时间戳,作品摘要里的 `updatedAt` 仍只用于排序与摘要展示,不作为前端自行推导业务状态的真相。 +创作恢复参数只保留 `sessionId`、`profileId`、`draftId`、`workId` 这四个私有 query。它们只允许在同一条创作链路的结果页、生成页、工作台之间保留;切到首页、公开作品详情、runtime 或另一条玩法链路时必须清掉。平台入口刷新直达时,路径到玩法恢复目标、四个 query 归一化、生成页标记、大鱼吃小鱼 workId 兜底、作品 / 草稿身份匹配和跳一跳 / 敲木鱼恢复阶段落点统一由 `platformCreationUrlStateModel.ts` 解析,壳层只执行读取作品、恢复草稿和切换阶段等副作用。生成页等待时间统一以生成状态里的 `startedAtMs` 为准;创建该状态时优先使用后端 session 下发的时间戳,作品摘要里的 `updatedAt` 仍只用于排序与摘要展示,不作为前端自行推导业务状态的真相。 统一创作入口覆盖当前可进入创作链路的已有模板:`rpg`、`big-fish`、`puzzle`、`match3d`、`jump-hop`、`wooden-fish`、`square-hole`、`bark-battle`、`visual-novel`、`baby-object-match` 和 `creative-agent`;`airp` 仍是未开放占位,不作为当前统一创作链路目标。拼图、抓大鹅、跳一跳和敲木鱼在前端继续经过 `UnifiedCreationWorkspace` 和 `UnifiedGenerationPage`:`UnifiedCreationWorkspace` 作为平台壳依赖的统一创作编排层,再内部调用 `src/components/unified-creation/workspaces/` 下的 `PuzzleCreationWorkspace`、`Match3DCreationWorkspace`、`JumpHopCreationWorkspace` 和 `WoodenFishCreationWorkspace`。其它已有模板由平台壳用 `UnifiedCreationPage` 包住既有工作台,复用统一标题栏、返回入口、页面级纵向滚动和隐藏字段契约,同时保留各玩法自己的表单、草稿恢复和后续编排。创作页字段清单由后端在 `GET /api/creation-entry/config` 的 `creationTypes[].unifiedCreationSpec` 下发,前端仅在该扩展位缺失时回退到本地默认 spec;字段类型只保留 `text`、`select`、`image`、`audio`。`UnifiedCreationPage` 不在 UI 中额外展示字段说明 chip,也不在右上角显示内部 `playId`、模板 ID 或工作台阶段名;竖屏移动端必须能从标题、表单一路滑到提交按钮。各玩法工作台负责渲染真实输入控件、上传、历史素材、校验和提交,但返回按钮只保留在统一页头,工作台内部不再重复渲染。暗色创作进度卡片位于 `platform-remap-surface` 内时,必须用组件专属 class 覆盖浅色主题 remap,确保白字、浅色边框和进度条底色不会被全局规则改成深色;不要只依赖通用 `text-white*` 类。敲木鱼的音效和功德词条面板不得放进独立内部滚动容器,移动端应跟随页面自然滚动展开。生成页统一展示阶段、当前步骤、总进度、错误和重试动作。 diff --git a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx index 0d0f5b4c..04e18324 100644 --- a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx +++ b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx @@ -408,9 +408,16 @@ import { buildVisualNovelCreationUrlState, buildWoodenFishCreationUrlState, hasPuzzleRuntimeUrlStateValue, + matchesBabyObjectMatchCreationUrlRestoreTarget, + matchesBarkBattleCreationUrlRestoreTarget, + matchesBigFishCreationUrlRestoreTarget, + matchesSessionProfileWorkCreationUrlRestoreTarget, + matchesVisualNovelCreationUrlRestoreTarget, normalizeCreationUrlValue, resolveCreationUrlRestoreTarget, resolveInitialCreationUrlRestoreDecision, + resolveJumpHopCreationUrlRestoreStage, + resolveWoodenFishCreationUrlRestoreStage, } from './platformCreationUrlStateModel'; import { resolvePlatformCreationWorkDeleteConfirmationModel } from './platformCreationWorkDeleteFlow'; import { @@ -12150,7 +12157,7 @@ export function PlatformEntryFlowShellImpl({ if (!target) { return; } - const { sessionId, profileId, draftId, workId } = target; + const { sessionId, profileId } = target; if (target.kind === 'big-fish') { const targetSessionId = target.bigFishSessionId; @@ -12159,10 +12166,8 @@ export function PlatformEntryFlowShellImpl({ (bigFishWorks.length > 0 ? bigFishWorks : (await listBigFishWorks().catch(() => ({ items: [] }))).items - ).find( - (item) => - item.sourceSessionId === targetSessionId || - item.workId === workId, + ).find((item) => + matchesBigFishCreationUrlRestoreTarget(item, target), ) ?? null; if (matchedWork) { await openBigFishDraft(matchedWork); @@ -12180,11 +12185,8 @@ export function PlatformEntryFlowShellImpl({ : mapMatch3DWorksForRuntimeUi( (await listMatch3DWorks().catch(() => ({ items: [] }))).items, ) - ).find( - (item) => - item.sourceSessionId === sessionId || - item.profileId === profileId || - item.workId === workId, + ).find((item) => + matchesSessionProfileWorkCreationUrlRestoreTarget(item, target), ) ?? null; if (matchedWork) { await openMatch3DDraft(matchedWork, { forceDraft: true }); @@ -12201,11 +12203,8 @@ export function PlatformEntryFlowShellImpl({ (squareHoleWorks.length > 0 ? squareHoleWorks : (await listSquareHoleWorks().catch(() => ({ items: [] }))).items - ).find( - (item) => - item.sourceSessionId === sessionId || - item.profileId === profileId || - item.workId === workId, + ).find((item) => + matchesSessionProfileWorkCreationUrlRestoreTarget(item, target), ) ?? null; if (matchedWork) { await openSquareHoleDraft(matchedWork, { forceDraft: true }); @@ -12222,11 +12221,8 @@ export function PlatformEntryFlowShellImpl({ (puzzleWorks.length > 0 ? puzzleWorks : (await listPuzzleWorks().catch(() => ({ items: [] }))).items - ).find( - (item) => - item.sourceSessionId === sessionId || - item.profileId === profileId || - item.workId === workId, + ).find((item) => + matchesSessionProfileWorkCreationUrlRestoreTarget(item, target), ) ?? null; if (matchedWork) { await openPuzzleDraft(matchedWork); @@ -12243,7 +12239,9 @@ export function PlatformEntryFlowShellImpl({ (visualNovelWorks.length > 0 ? visualNovelWorks : (await listVisualNovelWorks().catch(() => ({ works: [] }))).works - ).find((item) => item.profileId === profileId) ?? null; + ).find((item) => + matchesVisualNovelCreationUrlRestoreTarget(item, target), + ) ?? null; if (matchedWork) { await openVisualNovelDraft(matchedWork, { forceDraft: true }); return; @@ -12259,8 +12257,8 @@ export function PlatformEntryFlowShellImpl({ (barkBattleWorks.length > 0 ? barkBattleWorks : (await listBarkBattleWorks().catch(() => ({ items: [] }))).items - ).find( - (item) => item.workId === workId || item.draftId === draftId, + ).find((item) => + matchesBarkBattleCreationUrlRestoreTarget(item, target), ) ?? null; if (matchedWork) { openBarkBattleDraft(matchedWork, { forceDraft: true }); @@ -12273,11 +12271,8 @@ export function PlatformEntryFlowShellImpl({ (babyObjectMatchDrafts.length > 0 ? babyObjectMatchDrafts : await listLocalBabyObjectMatchDrafts().catch(() => []) - ).find( - (item) => - item.profileId === profileId || - item.draftId === draftId || - item.profileId === workId, + ).find((item) => + matchesBabyObjectMatchCreationUrlRestoreTarget(item, target), ) ?? null; if (matchedDraft) { openBabyObjectMatchDraft(matchedDraft); @@ -12313,11 +12308,11 @@ export function PlatformEntryFlowShellImpl({ ); enterCreateTab(); setSelectionStage( - target.isGeneratingPath - ? 'jump-hop-generating' - : session?.draft || work - ? 'jump-hop-result' - : 'jump-hop-workspace', + resolveJumpHopCreationUrlRestoreStage({ + isGeneratingPath: target.isGeneratingPath, + hasRestoredDraft: Boolean(session?.draft), + hasRestoredWork: Boolean(work), + }), ); } catch (error) { setJumpHopError( @@ -12344,11 +12339,10 @@ export function PlatformEntryFlowShellImpl({ ); enterCreateTab(); setSelectionStage( - target.isGeneratingPath - ? 'wooden-fish-generating' - : session.draft - ? 'wooden-fish-result' - : 'wooden-fish-workspace', + resolveWoodenFishCreationUrlRestoreStage({ + isGeneratingPath: target.isGeneratingPath, + hasRestoredDraft: Boolean(session.draft), + }), ); } catch (error) { setWoodenFishError( diff --git a/src/components/platform-entry/platformCreationUrlStateModel.test.ts b/src/components/platform-entry/platformCreationUrlStateModel.test.ts index b7f7cbf4..6e8a5bc9 100644 --- a/src/components/platform-entry/platformCreationUrlStateModel.test.ts +++ b/src/components/platform-entry/platformCreationUrlStateModel.test.ts @@ -31,9 +31,16 @@ import { buildWoodenFishCreationUrlState, hasCreationUrlStateValue, hasPuzzleRuntimeUrlStateValue, + matchesBabyObjectMatchCreationUrlRestoreTarget, + matchesBarkBattleCreationUrlRestoreTarget, + matchesBigFishCreationUrlRestoreTarget, + matchesSessionProfileWorkCreationUrlRestoreTarget, + matchesVisualNovelCreationUrlRestoreTarget, normalizeCreationUrlValue, resolveCreationUrlRestoreTarget, resolveInitialCreationUrlRestoreDecision, + resolveJumpHopCreationUrlRestoreStage, + resolveWoodenFishCreationUrlRestoreStage, } from './platformCreationUrlStateModel'; describe('platformCreationUrlStateModel', () => { @@ -193,6 +200,129 @@ describe('platformCreationUrlStateModel', () => { ).toBeNull(); }); + test('matches restore targets against work and draft identities', () => { + const bigFishTarget = resolveCreationUrlRestoreTarget( + '/creation/big-fish/result', + { + workId: 'big-fish-work-river', + }, + ); + expect(bigFishTarget?.kind).toBe('big-fish'); + if (bigFishTarget?.kind !== 'big-fish') { + throw new Error('big fish target expected'); + } + expect( + matchesBigFishCreationUrlRestoreTarget( + { sourceSessionId: 'river' }, + bigFishTarget, + ), + ).toBe(true); + expect( + matchesBigFishCreationUrlRestoreTarget( + { workId: 'big-fish-work-river' }, + bigFishTarget, + ), + ).toBe(true); + + const target = { + sessionId: 'session-1', + profileId: 'profile-1', + draftId: 'draft-1', + workId: 'work-1', + }; + expect( + matchesSessionProfileWorkCreationUrlRestoreTarget( + { sourceSessionId: 'session-1' }, + target, + ), + ).toBe(true); + expect( + matchesSessionProfileWorkCreationUrlRestoreTarget( + { profileId: 'profile-1' }, + target, + ), + ).toBe(true); + expect( + matchesSessionProfileWorkCreationUrlRestoreTarget( + { workId: 'work-1' }, + target, + ), + ).toBe(true); + expect( + matchesVisualNovelCreationUrlRestoreTarget( + { profileId: 'profile-1' }, + target, + ), + ).toBe(true); + expect( + matchesBarkBattleCreationUrlRestoreTarget( + { draftId: 'draft-1' }, + target, + ), + ).toBe(true); + expect( + matchesBabyObjectMatchCreationUrlRestoreTarget( + { profileId: 'work-1' }, + target, + ), + ).toBe(true); + expect( + matchesSessionProfileWorkCreationUrlRestoreTarget( + { sourceSessionId: null, profileId: null, workId: null }, + { sessionId: null, profileId: null, workId: null }, + ), + ).toBe(false); + expect( + matchesBarkBattleCreationUrlRestoreTarget( + { workId: null, draftId: null }, + { workId: null, draftId: null }, + ), + ).toBe(false); + }); + + test('resolves work backed restore stages', () => { + expect( + resolveJumpHopCreationUrlRestoreStage({ + isGeneratingPath: true, + hasRestoredDraft: false, + hasRestoredWork: true, + }), + ).toBe('jump-hop-generating'); + expect( + resolveJumpHopCreationUrlRestoreStage({ + isGeneratingPath: false, + hasRestoredDraft: false, + hasRestoredWork: true, + }), + ).toBe('jump-hop-result'); + expect( + resolveJumpHopCreationUrlRestoreStage({ + isGeneratingPath: false, + hasRestoredDraft: false, + hasRestoredWork: false, + }), + ).toBe('jump-hop-workspace'); + + expect( + resolveWoodenFishCreationUrlRestoreStage({ + isGeneratingPath: true, + hasRestoredDraft: true, + }), + ).toBe('wooden-fish-generating'); + expect( + resolveWoodenFishCreationUrlRestoreStage({ + isGeneratingPath: false, + hasRestoredDraft: true, + }), + ).toBe('wooden-fish-result'); + expect( + resolveWoodenFishCreationUrlRestoreStage({ + isGeneratingPath: false, + hasRestoredDraft: false, + }), + ).toBe('wooden-fish-workspace'); + }); + test('builds creation restore state for core session based plays', () => { expect( buildBigFishCreationUrlState({ diff --git a/src/components/platform-entry/platformCreationUrlStateModel.ts b/src/components/platform-entry/platformCreationUrlStateModel.ts index fbf366fb..45a153dc 100644 --- a/src/components/platform-entry/platformCreationUrlStateModel.ts +++ b/src/components/platform-entry/platformCreationUrlStateModel.ts @@ -20,6 +20,7 @@ import type { WoodenFishSessionSnapshotResponse, WoodenFishWorkProfileResponse, } from '../../services/wooden-fish/woodenFishClient'; +import type { SelectionStage } from './platformEntryTypes'; import { buildPuzzleResultProfileId, buildPuzzleResultWorkId, @@ -80,19 +81,43 @@ type CreationUrlRestoreTargetBase = { isGeneratingPath: boolean; }; -export type CreationUrlRestoreTarget = - | (CreationUrlRestoreTargetBase & { - kind: 'big-fish'; - bigFishSessionId: string | null; - }) - | (CreationUrlRestoreTargetBase & { - kind: Exclude; - }); +export type BigFishCreationUrlRestoreTarget = CreationUrlRestoreTargetBase & { + kind: 'big-fish'; + bigFishSessionId: string | null; +}; -type NonBigFishCreationUrlRestoreTarget = Extract< - CreationUrlRestoreTarget, - { kind: Exclude } ->; +type NonBigFishCreationUrlRestoreTarget = CreationUrlRestoreTargetBase & { + kind: Exclude; +}; + +export type CreationUrlRestoreTarget = + | BigFishCreationUrlRestoreTarget + | NonBigFishCreationUrlRestoreTarget; + +export type BigFishRestoreWorkIdentity = { + sourceSessionId?: string | null; + workId?: string | null; +}; + +export type SessionProfileWorkRestoreIdentity = { + sourceSessionId?: string | null; + profileId?: string | null; + workId?: string | null; +}; + +export type ProfileRestoreWorkIdentity = { + profileId?: string | null; +}; + +export type BarkBattleRestoreWorkIdentity = { + workId?: string | null; + draftId?: string | null; +}; + +export type BabyObjectMatchRestoreDraftIdentity = { + profileId?: string | null; + draftId?: string | null; +}; const CREATION_URL_RESTORE_TARGET_ROUTES = [ ['/creation/big-fish', 'big-fish'], @@ -147,6 +172,89 @@ export function resolveCreationUrlRestoreTarget( return base as NonBigFishCreationUrlRestoreTarget; } +function matchesRestoreValue( + itemValue: string | null | undefined, + targetValue: string | null, +) { + return Boolean(targetValue && itemValue === targetValue); +} + +export function matchesBigFishCreationUrlRestoreTarget( + item: BigFishRestoreWorkIdentity, + target: BigFishCreationUrlRestoreTarget, +) { + return ( + matchesRestoreValue(item.sourceSessionId, target.bigFishSessionId) || + matchesRestoreValue(item.workId, target.workId) + ); +} + +export function matchesSessionProfileWorkCreationUrlRestoreTarget( + item: SessionProfileWorkRestoreIdentity, + target: Pick, +) { + return ( + matchesRestoreValue(item.sourceSessionId, target.sessionId) || + matchesRestoreValue(item.profileId, target.profileId) || + matchesRestoreValue(item.workId, target.workId) + ); +} + +export function matchesVisualNovelCreationUrlRestoreTarget( + item: ProfileRestoreWorkIdentity, + target: Pick, +) { + return matchesRestoreValue(item.profileId, target.profileId); +} + +export function matchesBarkBattleCreationUrlRestoreTarget( + item: BarkBattleRestoreWorkIdentity, + target: Pick, +) { + return ( + matchesRestoreValue(item.workId, target.workId) || + matchesRestoreValue(item.draftId, target.draftId) + ); +} + +export function matchesBabyObjectMatchCreationUrlRestoreTarget( + item: BabyObjectMatchRestoreDraftIdentity, + target: Pick, +) { + return ( + matchesRestoreValue(item.profileId, target.profileId) || + matchesRestoreValue(item.draftId, target.draftId) || + matchesRestoreValue(item.profileId, target.workId) + ); +} + +export function resolveJumpHopCreationUrlRestoreStage(params: { + isGeneratingPath: boolean; + hasRestoredDraft: boolean; + hasRestoredWork: boolean; +}): SelectionStage { + if (params.isGeneratingPath) { + return 'jump-hop-generating'; + } + + return params.hasRestoredDraft || params.hasRestoredWork + ? 'jump-hop-result' + : 'jump-hop-workspace'; +} + +export function resolveWoodenFishCreationUrlRestoreStage(params: { + isGeneratingPath: boolean; + hasRestoredDraft: boolean; +}): SelectionStage { + if (params.isGeneratingPath) { + return 'wooden-fish-generating'; + } + + return params.hasRestoredDraft + ? 'wooden-fish-result' + : 'wooden-fish-workspace'; +} + export type InitialCreationUrlRestoreDecision = | { type: 'skip' } | { type: 'mark-handled' }