fix(frontend): prefer canonical phase for client action gating (#301 follow-up) #306
52
docs/ISSUE-301-CLIENT-ACTION-GATING-ARTIFACT.md
Normal file
52
docs/ISSUE-301-CLIENT-ACTION-GATING-ARTIFACT.md
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# Issue #301 Artifact — Client action gating from canonical phase state
|
||||||
|
|
||||||
|
Refs: #287, #301
|
||||||
|
|
||||||
|
## What changed
|
||||||
|
|
||||||
|
Frontend host/player shells now prefer the canonical phase exposed by `phase_view_model.current_phase` when deciding:
|
||||||
|
|
||||||
|
- which gameplay actions are enabled
|
||||||
|
- whether reveal data should still be shown
|
||||||
|
- which SPA hash-route should represent the active game state
|
||||||
|
|
||||||
|
This tightens the #301 slice so the client stays aligned with backend canonicalisation even when `session.status` lags during reveal/scoreboard promotion.
|
||||||
|
|
||||||
|
## Gated UI actions by phase
|
||||||
|
|
||||||
|
### Lobby
|
||||||
|
- **Host:** `startRound`
|
||||||
|
- **Player:** `join`
|
||||||
|
|
||||||
|
### Bluff / lie
|
||||||
|
- **Host:** `showQuestion`
|
||||||
|
- **Player:** `submitLie`
|
||||||
|
- **Blocked:** guess submission, scoreboard load, next round, finish game
|
||||||
|
|
||||||
|
### Guess
|
||||||
|
- **Host:** `mixAnswers`, `calculateScores`
|
||||||
|
- **Player:** `submitGuess`
|
||||||
|
- **Blocked:** lie submission, scoreboard load, next round, finish game
|
||||||
|
|
||||||
|
### Reveal
|
||||||
|
- **Host:** `loadScoreboard`
|
||||||
|
- **Player:** display-only reveal state
|
||||||
|
- **Blocked:** start next round, finish game, guess/lie submission
|
||||||
|
|
||||||
|
### Scoreboard
|
||||||
|
- **Host:** `startNextRound`, `finishGame`
|
||||||
|
- **Player:** display-only reveal/scoreboard state
|
||||||
|
- **Blocked:** scoreboard reload, guess/lie submission
|
||||||
|
|
||||||
|
## Test evidence
|
||||||
|
|
||||||
|
Targeted tests added/updated for:
|
||||||
|
|
||||||
|
- host shell canonical gating and route sync when `current_phase` differs from `session.status`
|
||||||
|
- player shell canonical gating and route sync when `current_phase` differs from `session.status`
|
||||||
|
- shared gameplay phase machine gating from canonical permissions
|
||||||
|
- shared API mapper contract coverage, including reveal/scoreboard payload stability
|
||||||
|
|
||||||
|
## Contract note
|
||||||
|
|
||||||
|
No backend protocol redesign was introduced. This follow-up only preserves and consumes the existing canonical phase/action contract more strictly on the client side.
|
||||||
@@ -21,6 +21,7 @@ function createFetchRouteMock(handler: FetchRouteHandler): FetchMock {
|
|||||||
function sessionDetailPayload(
|
function sessionDetailPayload(
|
||||||
status: string,
|
status: string,
|
||||||
options?: {
|
options?: {
|
||||||
|
currentPhase?: string;
|
||||||
roundQuestionId?: number | null;
|
roundQuestionId?: number | null;
|
||||||
reveal?: {
|
reveal?: {
|
||||||
correct_answer: string;
|
correct_answer: string;
|
||||||
@@ -81,6 +82,7 @@ function sessionDetailPayload(
|
|||||||
},
|
},
|
||||||
phase_view_model: {
|
phase_view_model: {
|
||||||
status,
|
status,
|
||||||
|
current_phase: options?.currentPhase ?? status,
|
||||||
round_number: 1,
|
round_number: 1,
|
||||||
players_count: 2,
|
players_count: 2,
|
||||||
constraints: {
|
constraints: {
|
||||||
@@ -89,14 +91,18 @@ function sessionDetailPayload(
|
|||||||
min_players_reached: true,
|
min_players_reached: true,
|
||||||
max_players_allowed: true,
|
max_players_allowed: true,
|
||||||
},
|
},
|
||||||
|
readiness: {
|
||||||
|
question_ready: (options?.currentPhase ?? status) !== 'lobby',
|
||||||
|
scoreboard_ready: (options?.currentPhase ?? status) === 'reveal' || (options?.currentPhase ?? status) === 'scoreboard',
|
||||||
|
},
|
||||||
host: {
|
host: {
|
||||||
can_start_round: status === 'lobby',
|
can_start_round: (options?.currentPhase ?? status) === 'lobby',
|
||||||
can_show_question: status === 'lie',
|
can_show_question: (options?.currentPhase ?? status) === 'lie',
|
||||||
can_mix_answers: status === 'lie' || status === 'guess',
|
can_mix_answers: (options?.currentPhase ?? status) === 'lie' || (options?.currentPhase ?? status) === 'guess',
|
||||||
can_calculate_scores: status === 'guess',
|
can_calculate_scores: (options?.currentPhase ?? status) === 'guess',
|
||||||
can_reveal_scoreboard: status === 'reveal',
|
can_reveal_scoreboard: (options?.currentPhase ?? status) === 'reveal',
|
||||||
can_start_next_round: status === 'reveal',
|
can_start_next_round: (options?.currentPhase ?? status) === 'scoreboard',
|
||||||
can_finish_game: status === 'reveal',
|
can_finish_game: (options?.currentPhase ?? status) === 'scoreboard',
|
||||||
},
|
},
|
||||||
player: {
|
player: {
|
||||||
can_join: status === 'lobby',
|
can_join: status === 'lobby',
|
||||||
@@ -259,7 +265,7 @@ describe('HostShellComponent gameplay wiring', () => {
|
|||||||
component.scoreboardPayload = '{"leaderboard":[]}';
|
component.scoreboardPayload = '{"leaderboard":[]}';
|
||||||
component.finalLeaderboardPayload = '{"leaderboard":[{"nickname":"Old","score":1}]}' ;
|
component.finalLeaderboardPayload = '{"leaderboard":[{"nickname":"Old","score":1}]}' ;
|
||||||
component.finalLeaderboard = [{ id: 9, nickname: 'Old', score: 1 }];
|
component.finalLeaderboard = [{ id: 9, nickname: 'Old', score: 1 }];
|
||||||
component.session = sessionDetailPayload('reveal', { roundQuestionId: 77 }) as any;
|
component.session = sessionDetailPayload('scoreboard', { roundQuestionId: 77 }) as any;
|
||||||
|
|
||||||
await component.startNextRound();
|
await component.startNextRound();
|
||||||
|
|
||||||
@@ -296,7 +302,7 @@ describe('HostShellComponent gameplay wiring', () => {
|
|||||||
|
|
||||||
const component = new HostShellComponent();
|
const component = new HostShellComponent();
|
||||||
component.sessionCode = 'ABCD12';
|
component.sessionCode = 'ABCD12';
|
||||||
component.session = sessionDetailPayload('reveal', { roundQuestionId: 77 }) as any;
|
component.session = sessionDetailPayload('scoreboard', { roundQuestionId: 77 }) as any;
|
||||||
|
|
||||||
await component.finishGame();
|
await component.finishGame();
|
||||||
expect(component.finishError).toContain('Finish game failed: Final leaderboard timeout');
|
expect(component.finishError).toContain('Finish game failed: Final leaderboard timeout');
|
||||||
@@ -320,7 +326,7 @@ describe('HostShellComponent gameplay wiring', () => {
|
|||||||
|
|
||||||
const component = new HostShellComponent();
|
const component = new HostShellComponent();
|
||||||
component.sessionCode = ' ';
|
component.sessionCode = ' ';
|
||||||
component.session = sessionDetailPayload('reveal', { roundQuestionId: 77 }) as any;
|
component.session = sessionDetailPayload('scoreboard', { roundQuestionId: 77 }) as any;
|
||||||
|
|
||||||
await component.startNextRound();
|
await component.startNextRound();
|
||||||
await component.finishGame();
|
await component.finishGame();
|
||||||
@@ -351,6 +357,10 @@ describe('HostShellComponent gameplay wiring', () => {
|
|||||||
for (const status of ['lie', 'guess', 'scoreboard'] as const) {
|
for (const status of ['lie', 'guess', 'scoreboard'] as const) {
|
||||||
component.session = sessionDetailPayload(status, { roundQuestionId: 77 }) as any;
|
component.session = sessionDetailPayload(status, { roundQuestionId: 77 }) as any;
|
||||||
await component.loadScoreboard();
|
await component.loadScoreboard();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const status of ['lie', 'guess', 'reveal'] as const) {
|
||||||
|
component.session = sessionDetailPayload(status, { roundQuestionId: 77 }) as any;
|
||||||
await component.startNextRound();
|
await component.startNextRound();
|
||||||
await component.finishGame();
|
await component.finishGame();
|
||||||
}
|
}
|
||||||
@@ -361,16 +371,42 @@ describe('HostShellComponent gameplay wiring', () => {
|
|||||||
component.session = sessionDetailPayload('reveal', { roundQuestionId: 77 }) as any;
|
component.session = sessionDetailPayload('reveal', { roundQuestionId: 77 }) as any;
|
||||||
expect(component.canCalculateScores).toBe(false);
|
expect(component.canCalculateScores).toBe(false);
|
||||||
expect(component.canLoadScoreboard).toBe(true);
|
expect(component.canLoadScoreboard).toBe(true);
|
||||||
expect(component.canStartNextRound).toBe(true);
|
expect(component.canStartNextRound).toBe(false);
|
||||||
expect(component.canFinishGame).toBe(true);
|
expect(component.canFinishGame).toBe(false);
|
||||||
|
|
||||||
component.session = sessionDetailPayload('scoreboard', { roundQuestionId: 77 }) as any;
|
component.session = sessionDetailPayload('scoreboard', { roundQuestionId: 77 }) as any;
|
||||||
expect(component.canLoadScoreboard).toBe(false);
|
expect(component.canLoadScoreboard).toBe(false);
|
||||||
expect(component.canStartNextRound).toBe(false);
|
expect(component.canStartNextRound).toBe(true);
|
||||||
expect(component.canFinishGame).toBe(false);
|
expect(component.canFinishGame).toBe(true);
|
||||||
expect(fetchMock).not.toHaveBeenCalled();
|
expect(fetchMock).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('prefers canonical current_phase for reveal panel and host routing when status lags behind', async () => {
|
||||||
|
const fetchMock: FetchMock = vi.fn().mockResolvedValue(
|
||||||
|
jsonResponse(200, sessionDetailPayload('reveal', { currentPhase: 'scoreboard', roundQuestionId: 77, reveal: { correct_answer: 'Mercury' } }))
|
||||||
|
);
|
||||||
|
vi.stubGlobal('fetch', fetchMock);
|
||||||
|
|
||||||
|
const replaceState = vi.fn();
|
||||||
|
vi.stubGlobal('window', {
|
||||||
|
location: { hash: '#/host/reveal/ABCD12' },
|
||||||
|
history: { state: null, replaceState },
|
||||||
|
sessionStorage: { getItem: vi.fn().mockReturnValue(null), setItem: vi.fn() },
|
||||||
|
});
|
||||||
|
|
||||||
|
const component = new HostShellComponent();
|
||||||
|
component.sessionCode = 'ABCD12';
|
||||||
|
|
||||||
|
await component.refreshSession();
|
||||||
|
|
||||||
|
expect(component.gameplayPhase).toBe('scoreboard');
|
||||||
|
expect(component.showRevealPanel).toBe(true);
|
||||||
|
expect(component.canLoadScoreboard).toBe(false);
|
||||||
|
expect(component.canStartNextRound).toBe(true);
|
||||||
|
expect(component.canFinishGame).toBe(true);
|
||||||
|
expect(replaceState).toHaveBeenCalledWith(null, '', '#/host/scoreboard/ABCD12');
|
||||||
|
});
|
||||||
|
|
||||||
it('syncs host hash-route with latest phase after refresh without page reload', async () => {
|
it('syncs host hash-route with latest phase after refresh without page reload', async () => {
|
||||||
const fetchMock: FetchMock = vi.fn().mockResolvedValue(jsonResponse(200, sessionDetailPayload('guess', { roundQuestionId: 77 })));
|
const fetchMock: FetchMock = vi.fn().mockResolvedValue(jsonResponse(200, sessionDetailPayload('guess', { roundQuestionId: 77 })));
|
||||||
vi.stubGlobal('fetch', fetchMock);
|
vi.stubGlobal('fetch', fetchMock);
|
||||||
@@ -408,12 +444,12 @@ describe('HostShellComponent gameplay wiring', () => {
|
|||||||
|
|
||||||
component.session = sessionDetailPayload('reveal') as any;
|
component.session = sessionDetailPayload('reveal') as any;
|
||||||
expect(component.canLoadScoreboard).toBe(true);
|
expect(component.canLoadScoreboard).toBe(true);
|
||||||
expect(component.canStartNextRound).toBe(true);
|
expect(component.canStartNextRound).toBe(false);
|
||||||
expect(component.canFinishGame).toBe(true);
|
expect(component.canFinishGame).toBe(false);
|
||||||
|
|
||||||
component.session = sessionDetailPayload('scoreboard') as any;
|
component.session = sessionDetailPayload('scoreboard') as any;
|
||||||
expect(component.canLoadScoreboard).toBe(false);
|
expect(component.canLoadScoreboard).toBe(false);
|
||||||
expect(component.canStartNextRound).toBe(false);
|
expect(component.canStartNextRound).toBe(true);
|
||||||
expect(component.canFinishGame).toBe(false);
|
expect(component.canFinishGame).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ type LeaderboardResponse = FinishGameResponse;
|
|||||||
<ul>
|
<ul>
|
||||||
<li *ngFor="let p of session.players">{{ p.nickname }}: {{ p.score }}</li>
|
<li *ngFor="let p of session.players">{{ p.nickname }}: {{ p.score }}</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="panel" *ngIf="session.reveal && (session.session.status === 'reveal' || session.session.status === 'scoreboard')">
|
<div class="panel" *ngIf="showRevealPanel">
|
||||||
<h3>Reveal</h3>
|
<h3>Reveal</h3>
|
||||||
<p><strong>Korrekt svar:</strong> {{ session.reveal.correct_answer }}</p>
|
<p><strong>Korrekt svar:</strong> {{ session.reveal.correct_answer }}</p>
|
||||||
<p><strong>Spørgsmål:</strong> {{ session.reveal.prompt }}</p>
|
<p><strong>Spørgsmål:</strong> {{ session.reveal.prompt }}</p>
|
||||||
@@ -163,6 +163,10 @@ export class HostShellComponent implements OnInit, OnDestroy {
|
|||||||
return isHostGameplayActionAllowed(this.session as any, 'finishGame');
|
return isHostGameplayActionAllowed(this.session as any, 'finishGame');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get showRevealPanel(): boolean {
|
||||||
|
return Boolean(this.session?.reveal && (this.gameplayPhase === 'reveal' || this.gameplayPhase === 'scoreboard'));
|
||||||
|
}
|
||||||
|
|
||||||
copy(key: string): string {
|
copy(key: string): string {
|
||||||
return t(key, this.locale);
|
return t(key, this.locale);
|
||||||
}
|
}
|
||||||
@@ -364,7 +368,7 @@ export class HostShellComponent implements OnInit, OnDestroy {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const phase = this.session.session.status || 'lobby';
|
const phase = this.gameplayPhase ?? this.session.session.status ?? 'lobby';
|
||||||
const code = this.normalizeCode(this.session.session.code || this.sessionCode);
|
const code = this.normalizeCode(this.session.session.code || this.sessionCode);
|
||||||
if (!code) {
|
if (!code) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ function jsonResponse(status: number, body: unknown) {
|
|||||||
function sessionDetailPayload(
|
function sessionDetailPayload(
|
||||||
status: string,
|
status: string,
|
||||||
options?: {
|
options?: {
|
||||||
|
currentPhase?: string;
|
||||||
answers?: string[];
|
answers?: string[];
|
||||||
players?: Array<{ id: number; nickname: string; score: number }>;
|
players?: Array<{ id: number; nickname: string; score: number }>;
|
||||||
roundQuestionId?: number | null;
|
roundQuestionId?: number | null;
|
||||||
@@ -79,6 +80,7 @@ function sessionDetailPayload(
|
|||||||
},
|
},
|
||||||
phase_view_model: {
|
phase_view_model: {
|
||||||
status,
|
status,
|
||||||
|
current_phase: options?.currentPhase ?? status,
|
||||||
round_number: 1,
|
round_number: 1,
|
||||||
players_count: (options?.players ?? []).length,
|
players_count: (options?.players ?? []).length,
|
||||||
constraints: {
|
constraints: {
|
||||||
@@ -87,6 +89,10 @@ function sessionDetailPayload(
|
|||||||
min_players_reached: true,
|
min_players_reached: true,
|
||||||
max_players_allowed: true,
|
max_players_allowed: true,
|
||||||
},
|
},
|
||||||
|
readiness: {
|
||||||
|
question_ready: (options?.currentPhase ?? status) !== 'lobby',
|
||||||
|
scoreboard_ready: (options?.currentPhase ?? status) === 'reveal' || (options?.currentPhase ?? status) === 'scoreboard',
|
||||||
|
},
|
||||||
host: {
|
host: {
|
||||||
can_start_round: false,
|
can_start_round: false,
|
||||||
can_show_question: false,
|
can_show_question: false,
|
||||||
@@ -97,10 +103,10 @@ function sessionDetailPayload(
|
|||||||
can_finish_game: false,
|
can_finish_game: false,
|
||||||
},
|
},
|
||||||
player: {
|
player: {
|
||||||
can_join: status === 'lobby',
|
can_join: (options?.currentPhase ?? status) === 'lobby',
|
||||||
can_submit_lie: status === 'lie',
|
can_submit_lie: (options?.currentPhase ?? status) === 'lie',
|
||||||
can_submit_guess: status === 'guess',
|
can_submit_guess: (options?.currentPhase ?? status) === 'guess',
|
||||||
can_view_final_result: status === 'finished',
|
can_view_final_result: (options?.currentPhase ?? status) === 'finished',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -437,6 +443,34 @@ describe('PlayerShellComponent gameplay wiring', () => {
|
|||||||
expect(values.get('wpp.session-context')).toBeUndefined();
|
expect(values.get('wpp.session-context')).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('prefers canonical current_phase for player reveal panel and routing when status lags behind', async () => {
|
||||||
|
const fetchMock: FetchMock = vi.fn().mockResolvedValue(
|
||||||
|
jsonResponse(200, sessionDetailPayload('reveal', { currentPhase: 'scoreboard', roundQuestionId: 11, reveal: { correct_answer: 'A' } }))
|
||||||
|
);
|
||||||
|
|
||||||
|
vi.stubGlobal('fetch', fetchMock);
|
||||||
|
|
||||||
|
const replaceState = vi.fn();
|
||||||
|
const localStorage = { getItem: vi.fn().mockReturnValue(null), setItem: vi.fn(), removeItem: vi.fn() };
|
||||||
|
vi.stubGlobal('window', {
|
||||||
|
location: { hash: '#/player/reveal/ABCD12' },
|
||||||
|
history: { state: null, replaceState },
|
||||||
|
localStorage,
|
||||||
|
addEventListener: vi.fn(),
|
||||||
|
removeEventListener: vi.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const component = new PlayerShellComponent();
|
||||||
|
component.sessionCode = 'ABCD12';
|
||||||
|
|
||||||
|
await component.refreshSession();
|
||||||
|
|
||||||
|
expect(component.gameplayPhase).toBe('scoreboard');
|
||||||
|
expect(component.showRevealPanel).toBe(true);
|
||||||
|
expect(component.showGuessControls).toBe(false);
|
||||||
|
expect(replaceState).toHaveBeenCalledWith(null, '', '#/player/scoreboard/ABCD12');
|
||||||
|
});
|
||||||
|
|
||||||
it('syncs player hash-route with latest phase during periodic state sync', async () => {
|
it('syncs player hash-route with latest phase during periodic state sync', async () => {
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
|
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ function resolveLocalStorage(): Storage | undefined {
|
|||||||
<button *ngIf="submitError?.kind === 'guess'" (click)="submitGuess()" [disabled]="loading || !canSubmitGuess">{{ copy('player.retry_guess_submit') }}</button>
|
<button *ngIf="submitError?.kind === 'guess'" (click)="submitGuess()" [disabled]="loading || !canSubmitGuess">{{ copy('player.retry_guess_submit') }}</button>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<div class="panel" *ngIf="session.reveal && (session.session.status === 'reveal' || session.session.status === 'scoreboard')">
|
<div class="panel" *ngIf="showRevealPanel">
|
||||||
<h3>Reveal</h3>
|
<h3>Reveal</h3>
|
||||||
<p><strong>Korrekt svar:</strong> {{ session.reveal.correct_answer }}</p>
|
<p><strong>Korrekt svar:</strong> {{ session.reveal.correct_answer }}</p>
|
||||||
<p><strong>Spørgsmål:</strong> {{ session.reveal.prompt }}</p>
|
<p><strong>Spørgsmål:</strong> {{ session.reveal.prompt }}</p>
|
||||||
@@ -221,6 +221,10 @@ export class PlayerShellComponent implements OnInit, OnDestroy {
|
|||||||
return isPlayerGameplayActionAllowed(this.session as any, 'submitGuess');
|
return isPlayerGameplayActionAllowed(this.session as any, 'submitGuess');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get showRevealPanel(): boolean {
|
||||||
|
return Boolean(this.session?.reveal && (this.gameplayPhase === 'reveal' || this.gameplayPhase === 'scoreboard'));
|
||||||
|
}
|
||||||
|
|
||||||
private readonly handleOnline = (): void => {
|
private readonly handleOnline = (): void => {
|
||||||
this.connectionState = 'reconnecting';
|
this.connectionState = 'reconnecting';
|
||||||
void this.retryReconnect();
|
void this.retryReconnect();
|
||||||
@@ -469,7 +473,7 @@ export class PlayerShellComponent implements OnInit, OnDestroy {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const phase = this.session.session.status || 'lobby';
|
const phase = this.gameplayPhase ?? this.session.session.status ?? 'lobby';
|
||||||
const code = this.normalizeCode(this.session.session.code || this.sessionCode);
|
const code = this.normalizeCode(this.session.session.code || this.sessionCode);
|
||||||
if (!code) {
|
if (!code) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -195,6 +195,7 @@ function mapSessionDetail(payload: unknown): SessionDetailResponse {
|
|||||||
reveal,
|
reveal,
|
||||||
phase_view_model: {
|
phase_view_model: {
|
||||||
status: readString(phase, 'status', 'session_detail.phase_view_model'),
|
status: readString(phase, 'status', 'session_detail.phase_view_model'),
|
||||||
|
current_phase: typeof phase.current_phase === 'string' ? phase.current_phase : undefined,
|
||||||
round_number: readNumber(phase, 'round_number', 'session_detail.phase_view_model'),
|
round_number: readNumber(phase, 'round_number', 'session_detail.phase_view_model'),
|
||||||
players_count: readNumber(phase, 'players_count', 'session_detail.phase_view_model'),
|
players_count: readNumber(phase, 'players_count', 'session_detail.phase_view_model'),
|
||||||
constraints: {
|
constraints: {
|
||||||
@@ -203,6 +204,19 @@ function mapSessionDetail(payload: unknown): SessionDetailResponse {
|
|||||||
min_players_reached: readBoolean(constraints, 'min_players_reached', 'session_detail.phase_view_model.constraints'),
|
min_players_reached: readBoolean(constraints, 'min_players_reached', 'session_detail.phase_view_model.constraints'),
|
||||||
max_players_allowed: readBoolean(constraints, 'max_players_allowed', 'session_detail.phase_view_model.constraints')
|
max_players_allowed: readBoolean(constraints, 'max_players_allowed', 'session_detail.phase_view_model.constraints')
|
||||||
},
|
},
|
||||||
|
readiness:
|
||||||
|
phase.readiness && typeof phase.readiness === 'object'
|
||||||
|
? {
|
||||||
|
question_ready:
|
||||||
|
typeof (phase.readiness as Record<string, unknown>).question_ready === 'boolean'
|
||||||
|
? ((phase.readiness as Record<string, unknown>).question_ready as boolean)
|
||||||
|
: undefined,
|
||||||
|
scoreboard_ready:
|
||||||
|
typeof (phase.readiness as Record<string, unknown>).scoreboard_ready === 'boolean'
|
||||||
|
? ((phase.readiness as Record<string, unknown>).scoreboard_ready as boolean)
|
||||||
|
: undefined,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
host: {
|
host: {
|
||||||
can_start_round: readBoolean(host, 'can_start_round', 'session_detail.phase_view_model.host'),
|
can_start_round: readBoolean(host, 'can_start_round', 'session_detail.phase_view_model.host'),
|
||||||
can_show_question: readBoolean(host, 'can_show_question', 'session_detail.phase_view_model.host'),
|
can_show_question: readBoolean(host, 'can_show_question', 'session_detail.phase_view_model.host'),
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export interface SessionRoundQuestion {
|
|||||||
|
|
||||||
export interface PhaseViewModel {
|
export interface PhaseViewModel {
|
||||||
status: string;
|
status: string;
|
||||||
|
current_phase?: string;
|
||||||
round_number: number;
|
round_number: number;
|
||||||
players_count: number;
|
players_count: number;
|
||||||
constraints: {
|
constraints: {
|
||||||
@@ -40,6 +41,10 @@ export interface PhaseViewModel {
|
|||||||
min_players_reached: boolean;
|
min_players_reached: boolean;
|
||||||
max_players_allowed: boolean;
|
max_players_allowed: boolean;
|
||||||
};
|
};
|
||||||
|
readiness?: {
|
||||||
|
question_ready?: boolean;
|
||||||
|
scoreboard_ready?: boolean;
|
||||||
|
};
|
||||||
host: {
|
host: {
|
||||||
can_start_round: boolean;
|
can_start_round: boolean;
|
||||||
can_show_question: boolean;
|
can_show_question: boolean;
|
||||||
|
|||||||
Reference in New Issue
Block a user