refactor: 收口创作直达恢复目标
This commit is contained in:
@@ -1340,6 +1340,7 @@
|
|||||||
- 背景:平台壳内散落各玩法创作恢复 URL 的 `sessionId` / `profileId` / `draftId` / `workId` 组装、空值归一化、拼图 runtime query key 与拼图稳定身份互推,导致刷新恢复规则缺少稳定测试面。
|
- 背景:平台壳内散落各玩法创作恢复 URL 的 `sessionId` / `profileId` / `draftId` / `workId` 组装、空值归一化、拼图 runtime query key 与拼图稳定身份互推,导致刷新恢复规则缺少稳定测试面。
|
||||||
- 决策:新增 `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。
|
- 决策:新增 `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 标记或进入原恢复副作用。
|
- 追加决策:初始创作 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` 继续保持无具体恢复目标,后续要接入需先补规则与测试。
|
||||||
- 影响范围:创作流程刷新恢复、拼图草稿 / 发布 runtime 深链、作品架打开试玩、跳一跳 / 敲木鱼 work-backed 恢复、Bark Battle / 宝贝识物本地草稿恢复。
|
- 影响范围:创作流程刷新恢复、拼图草稿 / 发布 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`。
|
- 验证方式:`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`。
|
- 关联文档:`docs/technical/【前端架构】CreationUrlStateModel收口计划-2026-06-03.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)。
|
平台入口创作生成通知、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)。
|
平台入口错误 / 完成弹窗的文案归一、来源格式、候选择一、dismiss key 与任务完成文案收口到 `src/components/platform-entry/platformDialogStateModel.ts`,规则见 [【前端架构】PlatformDialogStateModel收口计划-2026-06-03.md](./technical/【前端架构】PlatformDialogStateModel收口计划-2026-06-03.md)。
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
## 决策
|
## 决策
|
||||||
|
|
||||||
- 新增 `src/components/platform-entry/platformCreationUrlStateModel.ts` 作为 Creation URL State Module。
|
- 新增 `src/components/platform-entry/platformCreationUrlStateModel.ts` 作为 Creation URL State Module。
|
||||||
- 该 Module 的 Interface 收口为各玩法 `build*CreationUrlState`、拼图 `buildPuzzle*RuntimeUrlState`、`normalizeCreationUrlValue`、`hasCreationUrlStateValue`、`hasPuzzleRuntimeUrlStateValue`、`buildPuzzleRuntimeUrlStateKey` 与初始创作 URL 恢复判定 `resolveInitialCreationUrlRestoreDecision`。
|
- 该 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-*` 的互推规则。
|
- 新增 `src/components/platform-entry/platformPuzzleIdentityModel.ts` 作为拼图稳定身份 Module,统一 `puzzle-session-*`、`puzzle-profile-*`、`puzzle-work-*` 的互推规则。
|
||||||
- `PlatformEntryFlowShellImpl.tsx` 保留 React state、路由、登录门禁、网络请求和 URL 写入副作用 Adapter;不再在壳层内定义各玩法 URL 状态构造函数,也不直接内联初始恢复的已处理 / 等待 / 可恢复判定。
|
- `PlatformEntryFlowShellImpl.tsx` 保留 React state、路由、登录门禁、网络请求和 URL 写入副作用 Adapter;不再在壳层内定义各玩法 URL 状态构造函数,也不直接内联初始恢复的已处理 / 等待 / 可恢复判定。
|
||||||
|
|
||||||
@@ -19,11 +19,13 @@
|
|||||||
- 拼图 runtime query 独立使用 `mode`、`runtimeSessionId`、`runtimeProfileId`、`runtimeLevelId`、`publicWorkCode`,不与创作恢复 query 混写。
|
- 拼图 runtime query 独立使用 `mode`、`runtimeSessionId`、`runtimeProfileId`、`runtimeLevelId`、`publicWorkCode`,不与创作恢复 query 混写。
|
||||||
- 拼图 draft runtime 若没有 `sourceSessionId`,只允许从 `puzzle-profile-*` 反推出 `puzzle-session-*`。
|
- 拼图 draft runtime 若没有 `sourceSessionId`,只允许从 `puzzle-profile-*` 反推出 `puzzle-session-*`。
|
||||||
- 初始创作 URL 恢复只在未处理、当前路径属于创作恢复路径、私有 query 有值、平台配置加载完成且受保护数据可读时执行;非创作路径或无私有 query 时标记已处理,加载中或暂不可读时等待。
|
- 初始创作 URL 恢复只在未处理、当前路径属于创作恢复路径、私有 query 有值、平台配置加载完成且受保护数据可读时执行;非创作路径或无私有 query 时标记已处理,加载中或暂不可读时等待。
|
||||||
|
- 创作直达恢复目标由 `resolveCreationUrlRestoreTarget(pathname, state)` 统一识别;它只返回玩法 kind、归一化后的四个私有 query、生成路径标记和大鱼吃小鱼 session 兜底,不执行网络请求、草稿打开、stage 切换或 URL 写回。
|
||||||
|
- `/creation/rpg` 当前仍不归入具体恢复目标;若后续要恢复 RPG 直达,需要先补明确恢复规则和测试,不得让壳层重新内联路径判定。
|
||||||
|
|
||||||
## Depth / Leverage / Locality
|
## Depth / Leverage / Locality
|
||||||
|
|
||||||
- **Depth**:调用方只传玩法快照或作品摘要,即可得到规范化 URL state;各玩法字段优先级藏在 Module Implementation 内。
|
- **Depth**:调用方只传玩法快照或作品摘要,即可得到规范化 URL state;各玩法字段优先级藏在 Module Implementation 内。
|
||||||
- **Leverage**:新增或调整玩法恢复规则、恢复等待条件时,优先补 Module Interface 测试,再接壳层 Adapter。
|
- **Leverage**:新增或调整玩法恢复规则、恢复目标或恢复等待条件时,优先补 Module Interface 测试,再接壳层 Adapter。
|
||||||
- **Locality**:恢复 query、拼图 runtime query 和拼图稳定身份规则集中在两个小 Module,避免散落在页面壳、作品架和 runtime 打开逻辑中。
|
- **Locality**:恢复 query、拼图 runtime query 和拼图稳定身份规则集中在两个小 Module,避免散落在页面壳、作品架和 runtime 打开逻辑中。
|
||||||
|
|
||||||
## 验收
|
## 验收
|
||||||
|
|||||||
@@ -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`,这些入口继续承接各玩法自己的表单、草稿恢复和后续编排,不作为创作入口页内容。
|
当前点击底部加号进入的创作入口页承载后台公告位、创作入口页签和两列模板卡;页签中只有真实后端作品架摘要存在时才展示“最近创作”,其余为玩法模板分类。点击模板卡后直接进入对应玩法已有的入口创作表单 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 或另一条玩法链路时必须清掉。生成页等待时间统一以生成状态里的 `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*` 类。敲木鱼的音效和功德词条面板不得放进独立内部滚动容器,移动端应跟随页面自然滚动展开。生成页统一展示阶段、当前步骤、总进度、错误和重试动作。
|
统一创作入口覆盖当前可进入创作链路的已有模板:`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*` 类。敲木鱼的音效和功德词条面板不得放进独立内部滚动容器,移动端应跟随页面自然滚动展开。生成页统一展示阶段、当前步骤、总进度、错误和重试动作。
|
||||||
|
|
||||||
|
|||||||
@@ -409,6 +409,7 @@ import {
|
|||||||
buildWoodenFishCreationUrlState,
|
buildWoodenFishCreationUrlState,
|
||||||
hasPuzzleRuntimeUrlStateValue,
|
hasPuzzleRuntimeUrlStateValue,
|
||||||
normalizeCreationUrlValue,
|
normalizeCreationUrlValue,
|
||||||
|
resolveCreationUrlRestoreTarget,
|
||||||
resolveInitialCreationUrlRestoreDecision,
|
resolveInitialCreationUrlRestoreDecision,
|
||||||
} from './platformCreationUrlStateModel';
|
} from './platformCreationUrlStateModel';
|
||||||
import { resolvePlatformCreationWorkDeleteConfirmationModel } from './platformCreationWorkDeleteFlow';
|
import { resolvePlatformCreationWorkDeleteConfirmationModel } from './platformCreationWorkDeleteFlow';
|
||||||
@@ -12142,21 +12143,17 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
handledInitialCreationUrlStateRef.current = true;
|
handledInitialCreationUrlStateRef.current = true;
|
||||||
|
|
||||||
const restoreCreationUrlState = async () => {
|
const restoreCreationUrlState = async () => {
|
||||||
const path = window.location.pathname;
|
const target = resolveCreationUrlRestoreTarget(
|
||||||
const sessionId = normalizeCreationUrlValue(
|
window.location.pathname,
|
||||||
initialCreationUrlState.sessionId,
|
initialCreationUrlState,
|
||||||
);
|
);
|
||||||
const profileId = normalizeCreationUrlValue(
|
if (!target) {
|
||||||
initialCreationUrlState.profileId,
|
return;
|
||||||
);
|
}
|
||||||
const draftId = normalizeCreationUrlValue(
|
const { sessionId, profileId, draftId, workId } = target;
|
||||||
initialCreationUrlState.draftId,
|
|
||||||
);
|
|
||||||
const workId = normalizeCreationUrlValue(initialCreationUrlState.workId);
|
|
||||||
|
|
||||||
if (path.startsWith('/creation/big-fish')) {
|
if (target.kind === 'big-fish') {
|
||||||
const targetSessionId =
|
const targetSessionId = target.bigFishSessionId;
|
||||||
sessionId ?? workId?.replace(/^big-fish-work-/u, '');
|
|
||||||
if (targetSessionId) {
|
if (targetSessionId) {
|
||||||
const matchedWork =
|
const matchedWork =
|
||||||
(bigFishWorks.length > 0
|
(bigFishWorks.length > 0
|
||||||
@@ -12176,7 +12173,7 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (path.startsWith('/creation/match3d')) {
|
if (target.kind === 'match3d') {
|
||||||
const matchedWork =
|
const matchedWork =
|
||||||
(match3dWorks.length > 0
|
(match3dWorks.length > 0
|
||||||
? match3dWorks
|
? match3dWorks
|
||||||
@@ -12199,7 +12196,7 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (path.startsWith('/creation/square-hole')) {
|
if (target.kind === 'square-hole') {
|
||||||
const matchedWork =
|
const matchedWork =
|
||||||
(squareHoleWorks.length > 0
|
(squareHoleWorks.length > 0
|
||||||
? squareHoleWorks
|
? squareHoleWorks
|
||||||
@@ -12220,7 +12217,7 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (path.startsWith('/creation/puzzle')) {
|
if (target.kind === 'puzzle') {
|
||||||
const matchedWork =
|
const matchedWork =
|
||||||
(puzzleWorks.length > 0
|
(puzzleWorks.length > 0
|
||||||
? puzzleWorks
|
? puzzleWorks
|
||||||
@@ -12241,7 +12238,7 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (path.startsWith('/creation/visual-novel')) {
|
if (target.kind === 'visual-novel') {
|
||||||
const matchedWork =
|
const matchedWork =
|
||||||
(visualNovelWorks.length > 0
|
(visualNovelWorks.length > 0
|
||||||
? visualNovelWorks
|
? visualNovelWorks
|
||||||
@@ -12257,7 +12254,7 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (path.startsWith('/creation/bark-battle')) {
|
if (target.kind === 'bark-battle') {
|
||||||
const matchedWork =
|
const matchedWork =
|
||||||
(barkBattleWorks.length > 0
|
(barkBattleWorks.length > 0
|
||||||
? barkBattleWorks
|
? barkBattleWorks
|
||||||
@@ -12271,7 +12268,7 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (path.startsWith('/creation/baby-object-match')) {
|
if (target.kind === 'baby-object-match') {
|
||||||
const matchedDraft =
|
const matchedDraft =
|
||||||
(babyObjectMatchDrafts.length > 0
|
(babyObjectMatchDrafts.length > 0
|
||||||
? babyObjectMatchDrafts
|
? babyObjectMatchDrafts
|
||||||
@@ -12288,7 +12285,7 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (path.startsWith('/creation/jump-hop')) {
|
if (target.kind === 'jump-hop') {
|
||||||
let session: JumpHopSessionSnapshotResponse | null = null;
|
let session: JumpHopSessionSnapshotResponse | null = null;
|
||||||
let work: JumpHopWorkProfileResponse | null = null;
|
let work: JumpHopWorkProfileResponse | null = null;
|
||||||
try {
|
try {
|
||||||
@@ -12316,7 +12313,7 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
);
|
);
|
||||||
enterCreateTab();
|
enterCreateTab();
|
||||||
setSelectionStage(
|
setSelectionStage(
|
||||||
path.includes('/generating')
|
target.isGeneratingPath
|
||||||
? 'jump-hop-generating'
|
? 'jump-hop-generating'
|
||||||
: session?.draft || work
|
: session?.draft || work
|
||||||
? 'jump-hop-result'
|
? 'jump-hop-result'
|
||||||
@@ -12330,7 +12327,7 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (path.startsWith('/creation/wooden-fish')) {
|
if (target.kind === 'wooden-fish') {
|
||||||
if (!sessionId) {
|
if (!sessionId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -12347,7 +12344,7 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
);
|
);
|
||||||
enterCreateTab();
|
enterCreateTab();
|
||||||
setSelectionStage(
|
setSelectionStage(
|
||||||
path.includes('/generating')
|
target.isGeneratingPath
|
||||||
? 'wooden-fish-generating'
|
? 'wooden-fish-generating'
|
||||||
: session.draft
|
: session.draft
|
||||||
? 'wooden-fish-result'
|
? 'wooden-fish-result'
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import {
|
|||||||
hasCreationUrlStateValue,
|
hasCreationUrlStateValue,
|
||||||
hasPuzzleRuntimeUrlStateValue,
|
hasPuzzleRuntimeUrlStateValue,
|
||||||
normalizeCreationUrlValue,
|
normalizeCreationUrlValue,
|
||||||
|
resolveCreationUrlRestoreTarget,
|
||||||
resolveInitialCreationUrlRestoreDecision,
|
resolveInitialCreationUrlRestoreDecision,
|
||||||
} from './platformCreationUrlStateModel';
|
} from './platformCreationUrlStateModel';
|
||||||
|
|
||||||
@@ -94,6 +95,104 @@ describe('platformCreationUrlStateModel', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('resolves supported creation url restore targets from paths', () => {
|
||||||
|
const state = {
|
||||||
|
sessionId: ' session-1 ',
|
||||||
|
profileId: ' profile-1 ',
|
||||||
|
draftId: ' draft-1 ',
|
||||||
|
workId: ' work-1 ',
|
||||||
|
};
|
||||||
|
const cases = [
|
||||||
|
['/creation/big-fish/result', 'big-fish'],
|
||||||
|
['/creation/match3d/result', 'match3d'],
|
||||||
|
['/creation/square-hole/result', 'square-hole'],
|
||||||
|
['/creation/puzzle/result', 'puzzle'],
|
||||||
|
['/creation/visual-novel/result', 'visual-novel'],
|
||||||
|
['/creation/bark-battle/result', 'bark-battle'],
|
||||||
|
['/creation/baby-object-match/result', 'baby-object-match'],
|
||||||
|
['/creation/jump-hop/result', 'jump-hop'],
|
||||||
|
['/creation/wooden-fish/result', 'wooden-fish'],
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
cases.forEach(([pathname, kind]) => {
|
||||||
|
expect(resolveCreationUrlRestoreTarget(pathname, state)).toMatchObject({
|
||||||
|
kind,
|
||||||
|
sessionId: 'session-1',
|
||||||
|
profileId: 'profile-1',
|
||||||
|
draftId: 'draft-1',
|
||||||
|
workId: 'work-1',
|
||||||
|
isGeneratingPath: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('normalizes creation url restore target values and generating paths', () => {
|
||||||
|
expect(
|
||||||
|
resolveCreationUrlRestoreTarget('/creation/jump-hop/generating', {
|
||||||
|
sessionId: ' ',
|
||||||
|
profileId: ' jump-profile-1 ',
|
||||||
|
draftId: undefined,
|
||||||
|
workId: null,
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
kind: 'jump-hop',
|
||||||
|
sessionId: null,
|
||||||
|
profileId: 'jump-profile-1',
|
||||||
|
draftId: null,
|
||||||
|
workId: null,
|
||||||
|
isGeneratingPath: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('derives big fish restore session from work id when needed', () => {
|
||||||
|
expect(
|
||||||
|
resolveCreationUrlRestoreTarget('/creation/big-fish/result', {
|
||||||
|
workId: 'big-fish-work-river',
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
kind: 'big-fish',
|
||||||
|
sessionId: null,
|
||||||
|
profileId: null,
|
||||||
|
draftId: null,
|
||||||
|
workId: 'big-fish-work-river',
|
||||||
|
isGeneratingPath: false,
|
||||||
|
bigFishSessionId: 'river',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
resolveCreationUrlRestoreTarget('/creation/big-fish/result', {
|
||||||
|
sessionId: 'big-fish-session-carp',
|
||||||
|
workId: 'big-fish-work-river',
|
||||||
|
}),
|
||||||
|
).toMatchObject({
|
||||||
|
kind: 'big-fish',
|
||||||
|
bigFishSessionId: 'big-fish-session-carp',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('keeps unsupported creation paths without a concrete restore target', () => {
|
||||||
|
expect(
|
||||||
|
resolveCreationUrlRestoreTarget('/creation/rpg/result', {
|
||||||
|
sessionId: 'rpg-session-1',
|
||||||
|
}),
|
||||||
|
).toBeNull();
|
||||||
|
expect(
|
||||||
|
resolveCreationUrlRestoreTarget('/creation/unknown/result', {
|
||||||
|
sessionId: 'unknown-session-1',
|
||||||
|
}),
|
||||||
|
).toBeNull();
|
||||||
|
expect(
|
||||||
|
resolveCreationUrlRestoreTarget('/creation/big-fishery/result', {
|
||||||
|
sessionId: 'big-fish-session-1',
|
||||||
|
}),
|
||||||
|
).toBeNull();
|
||||||
|
expect(
|
||||||
|
resolveCreationUrlRestoreTarget('/works/detail', {
|
||||||
|
workId: 'work-1',
|
||||||
|
}),
|
||||||
|
).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
test('builds creation restore state for core session based plays', () => {
|
test('builds creation restore state for core session based plays', () => {
|
||||||
expect(
|
expect(
|
||||||
buildBigFishCreationUrlState({
|
buildBigFishCreationUrlState({
|
||||||
|
|||||||
@@ -60,6 +60,93 @@ export function buildPuzzleRuntimeUrlStateKey(state: PuzzleRuntimeUrlState) {
|
|||||||
].join('|');
|
].join('|');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type CreationUrlRestoreTargetKind =
|
||||||
|
| 'big-fish'
|
||||||
|
| 'match3d'
|
||||||
|
| 'square-hole'
|
||||||
|
| 'puzzle'
|
||||||
|
| 'visual-novel'
|
||||||
|
| 'bark-battle'
|
||||||
|
| 'baby-object-match'
|
||||||
|
| 'jump-hop'
|
||||||
|
| 'wooden-fish';
|
||||||
|
|
||||||
|
type CreationUrlRestoreTargetBase = {
|
||||||
|
kind: CreationUrlRestoreTargetKind;
|
||||||
|
sessionId: string | null;
|
||||||
|
profileId: string | null;
|
||||||
|
draftId: string | null;
|
||||||
|
workId: string | null;
|
||||||
|
isGeneratingPath: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CreationUrlRestoreTarget =
|
||||||
|
| (CreationUrlRestoreTargetBase & {
|
||||||
|
kind: 'big-fish';
|
||||||
|
bigFishSessionId: string | null;
|
||||||
|
})
|
||||||
|
| (CreationUrlRestoreTargetBase & {
|
||||||
|
kind: Exclude<CreationUrlRestoreTargetKind, 'big-fish'>;
|
||||||
|
});
|
||||||
|
|
||||||
|
type NonBigFishCreationUrlRestoreTarget = Extract<
|
||||||
|
CreationUrlRestoreTarget,
|
||||||
|
{ kind: Exclude<CreationUrlRestoreTargetKind, 'big-fish'> }
|
||||||
|
>;
|
||||||
|
|
||||||
|
const CREATION_URL_RESTORE_TARGET_ROUTES = [
|
||||||
|
['/creation/big-fish', 'big-fish'],
|
||||||
|
['/creation/match3d', 'match3d'],
|
||||||
|
['/creation/square-hole', 'square-hole'],
|
||||||
|
['/creation/puzzle', 'puzzle'],
|
||||||
|
['/creation/visual-novel', 'visual-novel'],
|
||||||
|
['/creation/bark-battle', 'bark-battle'],
|
||||||
|
['/creation/baby-object-match', 'baby-object-match'],
|
||||||
|
['/creation/jump-hop', 'jump-hop'],
|
||||||
|
['/creation/wooden-fish', 'wooden-fish'],
|
||||||
|
] as const satisfies readonly (readonly [
|
||||||
|
string,
|
||||||
|
CreationUrlRestoreTargetKind,
|
||||||
|
])[];
|
||||||
|
|
||||||
|
export function resolveCreationUrlRestoreTarget(
|
||||||
|
pathname: string | undefined,
|
||||||
|
state: CreationUrlState,
|
||||||
|
): CreationUrlRestoreTarget | null {
|
||||||
|
const path = pathname?.trim() ?? '';
|
||||||
|
const route = CREATION_URL_RESTORE_TARGET_ROUTES.find(([prefix]) =>
|
||||||
|
path === prefix || path.startsWith(`${prefix}/`),
|
||||||
|
);
|
||||||
|
if (!route) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const kind = route[1];
|
||||||
|
const sessionId = normalizeCreationUrlValue(state.sessionId);
|
||||||
|
const profileId = normalizeCreationUrlValue(state.profileId);
|
||||||
|
const draftId = normalizeCreationUrlValue(state.draftId);
|
||||||
|
const workId = normalizeCreationUrlValue(state.workId);
|
||||||
|
const base = {
|
||||||
|
kind,
|
||||||
|
sessionId,
|
||||||
|
profileId,
|
||||||
|
draftId,
|
||||||
|
workId,
|
||||||
|
isGeneratingPath: path.includes('/generating'),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (kind === 'big-fish') {
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
kind,
|
||||||
|
bigFishSessionId:
|
||||||
|
sessionId ?? workId?.replace(/^big-fish-work-/u, '') ?? null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return base as NonBigFishCreationUrlRestoreTarget;
|
||||||
|
}
|
||||||
|
|
||||||
export type InitialCreationUrlRestoreDecision =
|
export type InitialCreationUrlRestoreDecision =
|
||||||
| { type: 'skip' }
|
| { type: 'skip' }
|
||||||
| { type: 'mark-handled' }
|
| { type: 'mark-handled' }
|
||||||
|
|||||||
Reference in New Issue
Block a user