refactor: 收口公开作品详情策略
This commit is contained in:
@@ -16,6 +16,14 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 2026-06-03 平台入口公开作品详情 Strategy 收口
|
||||||
|
|
||||||
|
- 背景:平台壳层直接判断公开作品详情入口的玩法类型、是否需要补读完整详情,以及自有作品按钮显示“编辑”还是“改造”,导致统一作品详情的纯决策散落在巨型 Implementation 内。
|
||||||
|
- 决策:新增 `src/components/platform-entry/platformPublicWorkDetailFlow.ts`,以 `getPlatformPublicWorkDetailKind`、`resolvePlatformPublicWorkDetailOpenStrategy` 和 `resolvePlatformPublicWorkActionMode` 收口公开作品详情 Strategy。`PlatformEntryFlowShellImpl.tsx` 只按 Strategy 调用现有详情读取 / 直接展示 Adapter;启动、点赞、remix 和编辑副作用暂不抽走。
|
||||||
|
- 影响范围:统一作品详情入口、公开详情打开策略、自有公开作品编辑 / 改造动作模式,以及后续新增玩法公开详情接入。
|
||||||
|
- 验证方式:`npm run test -- src/components/platform-entry/platformPublicWorkDetailFlow.test.ts`、公开详情壳层交互回归、`npm run typecheck`、`npm run check:encoding`。
|
||||||
|
- 关联文档:`docs/technical/【前端架构】PlatformPublicWorkDetailFlow收口计划-2026-06-03.md`。
|
||||||
|
|
||||||
## 2026-06-03 平台入口弹窗状态规则收口
|
## 2026-06-03 平台入口弹窗状态规则收口
|
||||||
|
|
||||||
- 背景:`PlatformEntryFlowShellImpl.tsx` 曾同时持有平台级错误 / 完成弹窗的文案归一、来源格式、候选择一、dismiss key、后台生成 still-running 识别和任务完成文案,导致壳层 Interface 偏浅,测试面不稳定。
|
- 背景:`PlatformEntryFlowShellImpl.tsx` 曾同时持有平台级错误 / 完成弹窗的文案归一、来源格式、候选择一、dismiss key、后台生成 still-running 识别和任务完成文案,导致壳层 Interface 偏浅,测试面不稳定。
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ AI 文字游戏模板接入以 [AI_NATIVE_TEXT_GAME_TEMPLATE_MOKU_REFERENCE_PRD_
|
|||||||
|
|
||||||
平台入口公开作品身份、跨玩法去重、推荐运行态 kind 判定和最新排序收口到 `src/components/platform-entry/platformPublicGalleryFlow.ts`,规则见 [【前端架构】平台入口PublicGalleryFlowModule收口计划-2026-06-03.md](./technical/%E3%80%90%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84%E3%80%91%E5%B9%B3%E5%8F%B0%E5%85%A5%E5%8F%A3PublicGalleryFlowModule%E6%94%B6%E5%8F%A3%E8%AE%A1%E5%88%92-2026-06-03.md)。
|
平台入口公开作品身份、跨玩法去重、推荐运行态 kind 判定和最新排序收口到 `src/components/platform-entry/platformPublicGalleryFlow.ts`,规则见 [【前端架构】平台入口PublicGalleryFlowModule收口计划-2026-06-03.md](./technical/%E3%80%90%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84%E3%80%91%E5%B9%B3%E5%8F%B0%E5%85%A5%E5%8F%A3PublicGalleryFlowModule%E6%94%B6%E5%8F%A3%E8%AE%A1%E5%88%92-2026-06-03.md)。
|
||||||
|
|
||||||
|
统一作品详情页的玩法 kind、详情打开策略和自有作品动作模式收口到 `src/components/platform-entry/platformPublicWorkDetailFlow.ts`,规则见 [【前端架构】PlatformPublicWorkDetailFlow收口计划-2026-06-03.md](./technical/【前端架构】PlatformPublicWorkDetailFlow收口计划-2026-06-03.md)。
|
||||||
|
|
||||||
创作中心作品架打开动作由 `CreationWorkShelfItem.actions.open` 统一承载,生产 Hub 只接收 `CreationWorkShelfItem[]` 与 UI 状态,不再接收各玩法 raw items 和回调列阵,规则见 [【前端架构】WorkShelfModule收口计划-2026-06-03.md](./technical/%E3%80%90%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84%E3%80%91WorkShelfModule%E6%94%B6%E5%8F%A3%E8%AE%A1%E5%88%92-2026-06-03.md)。
|
创作中心作品架打开动作由 `CreationWorkShelfItem.actions.open` 统一承载,生产 Hub 只接收 `CreationWorkShelfItem[]` 与 UI 状态,不再接收各玩法 raw items 和回调列阵,规则见 [【前端架构】WorkShelfModule收口计划-2026-06-03.md](./technical/%E3%80%90%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84%E3%80%91WorkShelfModule%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)。
|
平台入口创作生成通知、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)。
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# PlatformPublicWorkDetailFlow 收口计划
|
||||||
|
|
||||||
|
## 背景
|
||||||
|
|
||||||
|
`PlatformEntryFlowShellImpl.tsx` 已把公开作品身份、去重和推荐 runtime kind 收口到 `platformPublicGalleryFlow.ts`,但统一作品详情入口仍在壳层 Implementation 内直接判断 RPG、拼图、跳一跳、敲木鱼、视觉小说和其它玩法。壳层既要知道哪些公开详情可直接使用当前 entry,又要知道哪些玩法必须先补读完整详情,还要按当前用户判断详情按钮是“编辑”还是“改造”。这些是纯决策规则,继续留在巨型壳层会削弱 Locality。
|
||||||
|
|
||||||
|
## 决策
|
||||||
|
|
||||||
|
- 新增 `src/components/platform-entry/platformPublicWorkDetailFlow.ts` 作为 Platform Public Work Detail Flow Module。
|
||||||
|
- Module Interface 收口:
|
||||||
|
- `getPlatformPublicWorkDetailKind(entry)`
|
||||||
|
- `resolvePlatformPublicWorkDetailOpenStrategy(entry)`
|
||||||
|
- `resolvePlatformPublicWorkActionMode(entry, viewerUserId)`
|
||||||
|
- `PlatformEntryFlowShellImpl.tsx` 继续作为 Adapter:根据 open strategy 调用 `openPublicWorkDetail`、`openPuzzlePublicWorkDetail`、`openJumpHopPublicWorkDetail`、`openWoodenFishPublicWorkDetail`、`openVisualNovelPublicWorkDetail` 或 `openRpgPublicWorkDetail`。
|
||||||
|
- 本次不抽 `startSelectedPublicWork`、`likePublicWork`、`remixPublicWork`、`editOwnedPublicWork`。这些函数牵涉运行态启动、计数写入、草稿恢复、作品架缓存和多路错误 setter;若直接搬进一个 Hook,会形成浅 Interface。
|
||||||
|
|
||||||
|
## Interface 约束
|
||||||
|
|
||||||
|
- `getPlatformPublicWorkDetailKind` 只根据 `PlatformPublicGalleryCard` 的玩法判定 helper 归一 kind;没有 `sourceType` 的公开 RPG 作品回退为 `rpg`。
|
||||||
|
- `resolvePlatformPublicWorkDetailOpenStrategy` 只表达“如何打开详情”,不执行网络请求或 state setter。
|
||||||
|
- 拼图、跳一跳、敲木鱼、视觉小说需要按 `profileId` 补读完整详情;返回对应 `load-*` strategy。
|
||||||
|
- 大鱼吃小鱼、抓大鹅、方洞挑战、汪汪声浪、宝贝识物和其它可直接展示的公开 entry 返回 `use-entry` strategy。
|
||||||
|
- RPG 返回 `load-rpg-detail` strategy,由壳层 Adapter 继续调用 RPG 详情读取流程。
|
||||||
|
- `resolvePlatformPublicWorkActionMode` 只比较 `entry.ownerUserId` 与当前 viewer user id;当前用户拥有该公开作品时返回 `edit`,否则返回 `remix`。
|
||||||
|
|
||||||
|
## Depth / Leverage / Locality
|
||||||
|
|
||||||
|
- **Depth**:壳层传入公开作品 entry 和当前用户 id,即可得到详情打开策略和动作模式;玩法判定细则藏在 Module Implementation 内。
|
||||||
|
- **Leverage**:新增玩法公开详情时先补 Strategy 单测,再接壳层 Adapter,不必在多个 JSX / callback 位置重复 sourceType 判断。
|
||||||
|
- **Locality**:公开作品详情入口的纯策略集中到一个小 Module;启动运行态、点赞、改造、编辑等副作用仍留在壳层,避免伪 Seam。
|
||||||
|
|
||||||
|
## 验收
|
||||||
|
|
||||||
|
- `npm run test -- src/components/platform-entry/platformPublicWorkDetailFlow.test.ts`
|
||||||
|
- `npm run test -- src/components/platform-entry/platformPublicGalleryFlow.test.ts`
|
||||||
|
- `npm run test -- src/components/platform-entry/PlatformWorkDetailView.test.tsx`
|
||||||
|
- `npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "public detail|owned public puzzle detail|direct missing public work detail"`
|
||||||
|
- `npx eslint src/components/platform-entry/platformPublicWorkDetailFlow.ts src/components/platform-entry/platformPublicWorkDetailFlow.test.ts --max-warnings 0`
|
||||||
|
- `npx eslint src/components/platform-entry/PlatformEntryFlowShellImpl.tsx --quiet`
|
||||||
|
- `npm run typecheck`
|
||||||
|
- `npm run check:encoding`
|
||||||
@@ -515,6 +515,10 @@ import {
|
|||||||
mergePlatformPublicGalleryEntries,
|
mergePlatformPublicGalleryEntries,
|
||||||
type RecommendRuntimeKind,
|
type RecommendRuntimeKind,
|
||||||
} from './platformPublicGalleryFlow';
|
} from './platformPublicGalleryFlow';
|
||||||
|
import {
|
||||||
|
resolvePlatformPublicWorkActionMode,
|
||||||
|
resolvePlatformPublicWorkDetailOpenStrategy,
|
||||||
|
} from './platformPublicWorkDetailFlow';
|
||||||
import {
|
import {
|
||||||
buildPuzzleResultProfileId,
|
buildPuzzleResultProfileId,
|
||||||
buildPuzzleResultWorkId,
|
buildPuzzleResultWorkId,
|
||||||
@@ -4025,13 +4029,13 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
const resultViewError =
|
const resultViewError =
|
||||||
autosaveCoordinator.customWorldAutoSaveError ??
|
autosaveCoordinator.customWorldAutoSaveError ??
|
||||||
sessionController.customWorldError;
|
sessionController.customWorldError;
|
||||||
const isSelectedPublicWorkOwned = Boolean(
|
const selectedPublicWorkActionMode = selectedPublicWorkDetail
|
||||||
authUi?.user?.id &&
|
? resolvePlatformPublicWorkActionMode(
|
||||||
selectedPublicWorkDetail?.ownerUserId === authUi.user.id,
|
selectedPublicWorkDetail,
|
||||||
);
|
authUi?.user?.id,
|
||||||
const selectedPublicWorkActionMode = isSelectedPublicWorkOwned
|
)
|
||||||
? 'edit'
|
|
||||||
: 'remix';
|
: 'remix';
|
||||||
|
const isSelectedPublicWorkOwned = selectedPublicWorkActionMode === 'edit';
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
@@ -11574,54 +11578,33 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
|
|
||||||
const openPublicGalleryDetail = useCallback(
|
const openPublicGalleryDetail = useCallback(
|
||||||
(entry: PlatformPublicGalleryCard) => {
|
(entry: PlatformPublicGalleryCard) => {
|
||||||
if (isBigFishGalleryEntry(entry)) {
|
const strategy = resolvePlatformPublicWorkDetailOpenStrategy(entry);
|
||||||
|
switch (strategy.type) {
|
||||||
|
case 'use-entry':
|
||||||
openPublicWorkDetail(entry);
|
openPublicWorkDetail(entry);
|
||||||
return;
|
return;
|
||||||
}
|
case 'load-puzzle-detail':
|
||||||
|
void openPuzzlePublicWorkDetail(strategy.profileId, {
|
||||||
if (isPuzzleGalleryEntry(entry)) {
|
|
||||||
void openPuzzlePublicWorkDetail(entry.profileId, {
|
|
||||||
tab: platformBootstrap.platformTab,
|
tab: platformBootstrap.platformTab,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
case 'load-jump-hop-detail':
|
||||||
|
void openJumpHopPublicWorkDetail(strategy.profileId);
|
||||||
if (isMatch3DGalleryEntry(entry)) {
|
|
||||||
openPublicWorkDetail(entry);
|
|
||||||
return;
|
return;
|
||||||
}
|
case 'load-wooden-fish-detail':
|
||||||
|
void openWoodenFishPublicWorkDetail(strategy.profileId);
|
||||||
if (isSquareHoleGalleryEntry(entry)) {
|
|
||||||
openPublicWorkDetail(entry);
|
|
||||||
return;
|
return;
|
||||||
}
|
case 'load-visual-novel-detail':
|
||||||
|
void openVisualNovelPublicWorkDetail(strategy.profileId);
|
||||||
if (isJumpHopGalleryEntry(entry)) {
|
|
||||||
void openJumpHopPublicWorkDetail(entry.profileId);
|
|
||||||
return;
|
return;
|
||||||
}
|
case 'load-rpg-detail':
|
||||||
|
void openRpgPublicWorkDetail(strategy.entry);
|
||||||
if (isWoodenFishGalleryEntry(entry)) {
|
|
||||||
void openWoodenFishPublicWorkDetail(entry.profileId);
|
|
||||||
return;
|
return;
|
||||||
|
default: {
|
||||||
|
const exhaustive: never = strategy;
|
||||||
|
return exhaustive;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isVisualNovelGalleryEntry(entry)) {
|
|
||||||
void openVisualNovelPublicWorkDetail(entry.profileId);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isBarkBattleGalleryEntry(entry)) {
|
|
||||||
openPublicWorkDetail(entry);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isEdutainmentGalleryEntry(entry)) {
|
|
||||||
openPublicWorkDetail(entry);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
void openRpgPublicWorkDetail(entry);
|
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
openPuzzlePublicWorkDetail,
|
openPuzzlePublicWorkDetail,
|
||||||
|
|||||||
@@ -0,0 +1,229 @@
|
|||||||
|
import { expect, test } from 'vitest';
|
||||||
|
|
||||||
|
import type { CustomWorldGalleryCard } from '../../../packages/shared/src/contracts/runtime';
|
||||||
|
import {
|
||||||
|
EDUTAINMENT_BABY_OBJECT_MATCH_TEMPLATE_ID,
|
||||||
|
EDUTAINMENT_BABY_OBJECT_MATCH_TEMPLATE_NAME,
|
||||||
|
type PlatformPublicGalleryCard,
|
||||||
|
} from '../rpg-entry/rpgEntryWorldPresentation';
|
||||||
|
import {
|
||||||
|
getPlatformPublicWorkDetailKind,
|
||||||
|
type PlatformPublicWorkDetailKind,
|
||||||
|
type PlatformPublicWorkDetailOpenStrategy,
|
||||||
|
resolvePlatformPublicWorkActionMode,
|
||||||
|
resolvePlatformPublicWorkDetailOpenStrategy,
|
||||||
|
} from './platformPublicWorkDetailFlow';
|
||||||
|
|
||||||
|
type TypedPlatformPublicGalleryCard = Extract<
|
||||||
|
PlatformPublicGalleryCard,
|
||||||
|
{ sourceType: string }
|
||||||
|
>;
|
||||||
|
type PlatformGallerySourceType = TypedPlatformPublicGalleryCard['sourceType'];
|
||||||
|
type TypedPlatformPublicGalleryCardOverrides = Partial<
|
||||||
|
Omit<TypedPlatformPublicGalleryCard, 'sourceType'>
|
||||||
|
>;
|
||||||
|
|
||||||
|
function buildRpgEntry(
|
||||||
|
overrides: Partial<CustomWorldGalleryCard> = {},
|
||||||
|
): CustomWorldGalleryCard {
|
||||||
|
return {
|
||||||
|
ownerUserId: 'user-1',
|
||||||
|
profileId: 'rpg-profile',
|
||||||
|
publicWorkCode: 'CW-RPG',
|
||||||
|
authorPublicUserCode: null,
|
||||||
|
visibility: 'published',
|
||||||
|
publishedAt: '2026-06-01T00:00:00.000Z',
|
||||||
|
updatedAt: '2026-06-01T01:00:00.000Z',
|
||||||
|
authorDisplayName: '玩家',
|
||||||
|
worldName: 'RPG 世界',
|
||||||
|
subtitle: '公开作品',
|
||||||
|
summaryText: '公开作品摘要',
|
||||||
|
coverImageSrc: null,
|
||||||
|
themeMode: 'martial',
|
||||||
|
playableNpcCount: 1,
|
||||||
|
landmarkCount: 1,
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTypedEntry(
|
||||||
|
sourceType: PlatformGallerySourceType,
|
||||||
|
overrides: TypedPlatformPublicGalleryCardOverrides = {},
|
||||||
|
): PlatformPublicGalleryCard {
|
||||||
|
const common = {
|
||||||
|
workId: `${sourceType}-work`,
|
||||||
|
profileId: `${sourceType}-profile`,
|
||||||
|
publicWorkCode: `${sourceType}-code`,
|
||||||
|
ownerUserId: 'user-1',
|
||||||
|
authorDisplayName: '玩家',
|
||||||
|
worldName: `${sourceType} 作品`,
|
||||||
|
subtitle: '公开作品',
|
||||||
|
summaryText: '公开作品摘要',
|
||||||
|
coverImageSrc: null,
|
||||||
|
themeTags: [sourceType],
|
||||||
|
visibility: 'published' as const,
|
||||||
|
publishedAt: '2026-06-01T00:00:00.000Z',
|
||||||
|
updatedAt: '2026-06-01T01:00:00.000Z',
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (sourceType) {
|
||||||
|
case 'puzzle':
|
||||||
|
return { ...common, ...overrides, sourceType };
|
||||||
|
case 'big-fish':
|
||||||
|
return { ...common, ...overrides, sourceType };
|
||||||
|
case 'match3d':
|
||||||
|
return { ...common, ...overrides, sourceType };
|
||||||
|
case 'square-hole':
|
||||||
|
return { ...common, ...overrides, sourceType };
|
||||||
|
case 'visual-novel':
|
||||||
|
return { ...common, ...overrides, sourceType };
|
||||||
|
case 'jump-hop':
|
||||||
|
return { ...common, ...overrides, sourceType };
|
||||||
|
case 'wooden-fish':
|
||||||
|
return { ...common, ...overrides, sourceType };
|
||||||
|
case 'edutainment':
|
||||||
|
return {
|
||||||
|
...common,
|
||||||
|
...overrides,
|
||||||
|
sourceType,
|
||||||
|
templateId: EDUTAINMENT_BABY_OBJECT_MATCH_TEMPLATE_ID,
|
||||||
|
templateName: EDUTAINMENT_BABY_OBJECT_MATCH_TEMPLATE_NAME,
|
||||||
|
};
|
||||||
|
case 'bark-battle':
|
||||||
|
return {
|
||||||
|
...common,
|
||||||
|
...overrides,
|
||||||
|
sourceType,
|
||||||
|
authorPublicUserCode: null,
|
||||||
|
coverRenderMode: 'image',
|
||||||
|
coverCharacterImageSrcs: [],
|
||||||
|
themeMode: 'martial',
|
||||||
|
playableNpcCount: 1,
|
||||||
|
landmarkCount: 1,
|
||||||
|
};
|
||||||
|
default: {
|
||||||
|
const exhaustive: never = sourceType;
|
||||||
|
return exhaustive;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test('platform public work detail flow resolves detail kind for every play kind', () => {
|
||||||
|
const cases: Array<
|
||||||
|
[sourceType: PlatformGallerySourceType, kind: PlatformPublicWorkDetailKind]
|
||||||
|
> = [
|
||||||
|
['big-fish', 'big-fish'],
|
||||||
|
['puzzle', 'puzzle'],
|
||||||
|
['jump-hop', 'jump-hop'],
|
||||||
|
['wooden-fish', 'wooden-fish'],
|
||||||
|
['match3d', 'match3d'],
|
||||||
|
['square-hole', 'square-hole'],
|
||||||
|
['visual-novel', 'visual-novel'],
|
||||||
|
['bark-battle', 'bark-battle'],
|
||||||
|
['edutainment', 'edutainment'],
|
||||||
|
];
|
||||||
|
|
||||||
|
cases.forEach(([sourceType, kind]) => {
|
||||||
|
expect(getPlatformPublicWorkDetailKind(buildTypedEntry(sourceType))).toBe(
|
||||||
|
kind,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getPlatformPublicWorkDetailKind(buildRpgEntry())).toBe('rpg');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('platform public work detail flow resolves open strategy', () => {
|
||||||
|
const rpgEntry = buildRpgEntry();
|
||||||
|
const cases: Array<
|
||||||
|
[
|
||||||
|
entry: PlatformPublicGalleryCard,
|
||||||
|
strategy: PlatformPublicWorkDetailOpenStrategy,
|
||||||
|
]
|
||||||
|
> = [
|
||||||
|
[
|
||||||
|
buildTypedEntry('big-fish'),
|
||||||
|
{
|
||||||
|
type: 'use-entry',
|
||||||
|
kind: 'big-fish',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
buildTypedEntry('match3d'),
|
||||||
|
{
|
||||||
|
type: 'use-entry',
|
||||||
|
kind: 'match3d',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
buildTypedEntry('square-hole'),
|
||||||
|
{
|
||||||
|
type: 'use-entry',
|
||||||
|
kind: 'square-hole',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
buildTypedEntry('bark-battle'),
|
||||||
|
{
|
||||||
|
type: 'use-entry',
|
||||||
|
kind: 'bark-battle',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
buildTypedEntry('edutainment'),
|
||||||
|
{
|
||||||
|
type: 'use-entry',
|
||||||
|
kind: 'edutainment',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
buildTypedEntry('puzzle'),
|
||||||
|
{
|
||||||
|
type: 'load-puzzle-detail',
|
||||||
|
profileId: 'puzzle-profile',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
buildTypedEntry('jump-hop'),
|
||||||
|
{
|
||||||
|
type: 'load-jump-hop-detail',
|
||||||
|
profileId: 'jump-hop-profile',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
buildTypedEntry('wooden-fish'),
|
||||||
|
{
|
||||||
|
type: 'load-wooden-fish-detail',
|
||||||
|
profileId: 'wooden-fish-profile',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
buildTypedEntry('visual-novel'),
|
||||||
|
{
|
||||||
|
type: 'load-visual-novel-detail',
|
||||||
|
profileId: 'visual-novel-profile',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
rpgEntry,
|
||||||
|
{
|
||||||
|
type: 'load-rpg-detail',
|
||||||
|
entry: rpgEntry,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
cases.forEach(([entry, strategy]) => {
|
||||||
|
expect(resolvePlatformPublicWorkDetailOpenStrategy(entry)).toEqual(
|
||||||
|
strategy,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('platform public work detail flow resolves edit mode only for owned works', () => {
|
||||||
|
const entry = buildTypedEntry('puzzle');
|
||||||
|
|
||||||
|
expect(resolvePlatformPublicWorkActionMode(entry, 'user-1')).toBe('edit');
|
||||||
|
expect(resolvePlatformPublicWorkActionMode(entry, ' user-1 ')).toBe('edit');
|
||||||
|
expect(resolvePlatformPublicWorkActionMode(entry, 'user-2')).toBe('remix');
|
||||||
|
expect(resolvePlatformPublicWorkActionMode(entry, null)).toBe('remix');
|
||||||
|
});
|
||||||
190
src/components/platform-entry/platformPublicWorkDetailFlow.ts
Normal file
190
src/components/platform-entry/platformPublicWorkDetailFlow.ts
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
import type { CustomWorldGalleryCard } from '../../../packages/shared/src/contracts/runtime';
|
||||||
|
import {
|
||||||
|
isBarkBattleGalleryEntry,
|
||||||
|
isBigFishGalleryEntry,
|
||||||
|
isEdutainmentGalleryEntry,
|
||||||
|
isJumpHopGalleryEntry,
|
||||||
|
isMatch3DGalleryEntry,
|
||||||
|
isPuzzleGalleryEntry,
|
||||||
|
isSquareHoleGalleryEntry,
|
||||||
|
isVisualNovelGalleryEntry,
|
||||||
|
isWoodenFishGalleryEntry,
|
||||||
|
type PlatformPublicGalleryCard,
|
||||||
|
} from '../rpg-entry/rpgEntryWorldPresentation';
|
||||||
|
|
||||||
|
export type PlatformPublicWorkDetailKind =
|
||||||
|
| 'bark-battle'
|
||||||
|
| 'big-fish'
|
||||||
|
| 'edutainment'
|
||||||
|
| 'jump-hop'
|
||||||
|
| 'match3d'
|
||||||
|
| 'puzzle'
|
||||||
|
| 'rpg'
|
||||||
|
| 'square-hole'
|
||||||
|
| 'visual-novel'
|
||||||
|
| 'wooden-fish';
|
||||||
|
|
||||||
|
export type PlatformPublicWorkDetailOpenStrategy =
|
||||||
|
| {
|
||||||
|
type: 'use-entry';
|
||||||
|
kind: Exclude<
|
||||||
|
PlatformPublicWorkDetailKind,
|
||||||
|
'jump-hop' | 'puzzle' | 'rpg' | 'visual-novel' | 'wooden-fish'
|
||||||
|
>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'load-puzzle-detail';
|
||||||
|
profileId: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'load-jump-hop-detail';
|
||||||
|
profileId: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'load-wooden-fish-detail';
|
||||||
|
profileId: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'load-visual-novel-detail';
|
||||||
|
profileId: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'load-rpg-detail';
|
||||||
|
entry: CustomWorldGalleryCard;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PlatformPublicWorkActionMode = 'edit' | 'remix';
|
||||||
|
|
||||||
|
export function isRpgPublicWorkDetailEntry(
|
||||||
|
entry: PlatformPublicGalleryCard,
|
||||||
|
): entry is CustomWorldGalleryCard {
|
||||||
|
return !('sourceType' in entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPlatformPublicWorkDetailKind(
|
||||||
|
entry: PlatformPublicGalleryCard,
|
||||||
|
): PlatformPublicWorkDetailKind {
|
||||||
|
if (isBigFishGalleryEntry(entry)) {
|
||||||
|
return 'big-fish';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPuzzleGalleryEntry(entry)) {
|
||||||
|
return 'puzzle';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isJumpHopGalleryEntry(entry)) {
|
||||||
|
return 'jump-hop';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isWoodenFishGalleryEntry(entry)) {
|
||||||
|
return 'wooden-fish';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMatch3DGalleryEntry(entry)) {
|
||||||
|
return 'match3d';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSquareHoleGalleryEntry(entry)) {
|
||||||
|
return 'square-hole';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isVisualNovelGalleryEntry(entry)) {
|
||||||
|
return 'visual-novel';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isBarkBattleGalleryEntry(entry)) {
|
||||||
|
return 'bark-battle';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEdutainmentGalleryEntry(entry)) {
|
||||||
|
return 'edutainment';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'rpg';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolvePlatformPublicWorkDetailOpenStrategy(
|
||||||
|
entry: PlatformPublicGalleryCard,
|
||||||
|
): PlatformPublicWorkDetailOpenStrategy {
|
||||||
|
if (isBigFishGalleryEntry(entry)) {
|
||||||
|
return {
|
||||||
|
type: 'use-entry',
|
||||||
|
kind: 'big-fish',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPuzzleGalleryEntry(entry)) {
|
||||||
|
return {
|
||||||
|
type: 'load-puzzle-detail',
|
||||||
|
profileId: entry.profileId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isJumpHopGalleryEntry(entry)) {
|
||||||
|
return {
|
||||||
|
type: 'load-jump-hop-detail',
|
||||||
|
profileId: entry.profileId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isWoodenFishGalleryEntry(entry)) {
|
||||||
|
return {
|
||||||
|
type: 'load-wooden-fish-detail',
|
||||||
|
profileId: entry.profileId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMatch3DGalleryEntry(entry)) {
|
||||||
|
return {
|
||||||
|
type: 'use-entry',
|
||||||
|
kind: 'match3d',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSquareHoleGalleryEntry(entry)) {
|
||||||
|
return {
|
||||||
|
type: 'use-entry',
|
||||||
|
kind: 'square-hole',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isVisualNovelGalleryEntry(entry)) {
|
||||||
|
return {
|
||||||
|
type: 'load-visual-novel-detail',
|
||||||
|
profileId: entry.profileId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isBarkBattleGalleryEntry(entry)) {
|
||||||
|
return {
|
||||||
|
type: 'use-entry',
|
||||||
|
kind: 'bark-battle',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEdutainmentGalleryEntry(entry)) {
|
||||||
|
return {
|
||||||
|
type: 'use-entry',
|
||||||
|
kind: 'edutainment',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRpgPublicWorkDetailEntry(entry)) {
|
||||||
|
return {
|
||||||
|
type: 'load-rpg-detail',
|
||||||
|
entry,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const exhaustive: never = entry;
|
||||||
|
return exhaustive;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolvePlatformPublicWorkActionMode(
|
||||||
|
entry: PlatformPublicGalleryCard,
|
||||||
|
viewerUserId: string | null | undefined,
|
||||||
|
): PlatformPublicWorkActionMode {
|
||||||
|
return viewerUserId?.trim() && entry.ownerUserId === viewerUserId.trim()
|
||||||
|
? 'edit'
|
||||||
|
: 'remix';
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user