refactor: 收口创作恢复身份匹配
This commit is contained in:
@@ -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`。
|
||||
|
||||
@@ -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)。
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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*` 类。敲木鱼的音效和功德词条面板不得放进独立内部滚动容器,移动端应跟随页面自然滚动展开。生成页统一展示阶段、当前步骤、总进度、错误和重试动作。
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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<CreationUrlRestoreTargetKind, 'big-fish'>;
|
||||
});
|
||||
export type BigFishCreationUrlRestoreTarget = CreationUrlRestoreTargetBase & {
|
||||
kind: 'big-fish';
|
||||
bigFishSessionId: string | null;
|
||||
};
|
||||
|
||||
type NonBigFishCreationUrlRestoreTarget = Extract<
|
||||
CreationUrlRestoreTarget,
|
||||
{ kind: Exclude<CreationUrlRestoreTargetKind, 'big-fish'> }
|
||||
>;
|
||||
type NonBigFishCreationUrlRestoreTarget = CreationUrlRestoreTargetBase & {
|
||||
kind: Exclude<CreationUrlRestoreTargetKind, 'big-fish'>;
|
||||
};
|
||||
|
||||
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<CreationUrlRestoreTarget, 'sessionId' | 'profileId' | 'workId'>,
|
||||
) {
|
||||
return (
|
||||
matchesRestoreValue(item.sourceSessionId, target.sessionId) ||
|
||||
matchesRestoreValue(item.profileId, target.profileId) ||
|
||||
matchesRestoreValue(item.workId, target.workId)
|
||||
);
|
||||
}
|
||||
|
||||
export function matchesVisualNovelCreationUrlRestoreTarget(
|
||||
item: ProfileRestoreWorkIdentity,
|
||||
target: Pick<CreationUrlRestoreTarget, 'profileId'>,
|
||||
) {
|
||||
return matchesRestoreValue(item.profileId, target.profileId);
|
||||
}
|
||||
|
||||
export function matchesBarkBattleCreationUrlRestoreTarget(
|
||||
item: BarkBattleRestoreWorkIdentity,
|
||||
target: Pick<CreationUrlRestoreTarget, 'workId' | 'draftId'>,
|
||||
) {
|
||||
return (
|
||||
matchesRestoreValue(item.workId, target.workId) ||
|
||||
matchesRestoreValue(item.draftId, target.draftId)
|
||||
);
|
||||
}
|
||||
|
||||
export function matchesBabyObjectMatchCreationUrlRestoreTarget(
|
||||
item: BabyObjectMatchRestoreDraftIdentity,
|
||||
target: Pick<CreationUrlRestoreTarget, 'profileId' | 'draftId' | 'workId'>,
|
||||
) {
|
||||
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' }
|
||||
|
||||
Reference in New Issue
Block a user