Skip to content

feat: migrate game Dev Interest page to React #2997

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions app/Platform/Actions/BuildGameInterestedDevelopersDataAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

declare(strict_types=1);

namespace App\Platform\Actions;

use App\Community\Enums\UserGameListType;
use App\Data\UserData;
use App\Models\Game;
use App\Models\Role;
use App\Models\UserGameListEntry;
use Illuminate\Support\Collection;

class BuildGameInterestedDevelopersDataAction
{
/**
* @return Collection<int, UserData>
*/
public function execute(Game $game): Collection
{
$users = UserGameListEntry::whereType(UserGameListType::Develop)
->where('GameID', $game->id)
->with(['user' => function ($query) {
$query->orderBy('User');
}])
->get()
->filter(fn (UserGameListEntry $entry) => $entry->user
&& ($entry->user->hasRole(Role::DEVELOPER) || $entry->user->hasRole(Role::DEVELOPER_JUNIOR))
)
->values()
->map(fn (UserGameListEntry $entry) => UserData::fromUser($entry->user));

return $users;
}
}
15 changes: 15 additions & 0 deletions app/Platform/Controllers/GameController.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@
use App\Models\Game;
use App\Models\System;
use App\Models\User;
use App\Platform\Actions\BuildGameInterestedDevelopersDataAction;
use App\Platform\Actions\BuildGameListAction;
use App\Platform\Actions\GetRandomGameAction;
use App\Platform\Data\DeveloperInterestPagePropsData;
use App\Platform\Data\GameData;
use App\Platform\Data\GameListPagePropsData;
use App\Platform\Data\SystemData;
use App\Platform\Enums\GameListSortField;
Expand Down Expand Up @@ -158,6 +161,18 @@ public function destroy(Game $game): void
$this->authorize('delete', $game);
}

public function devInterest(Game $game): InertiaResponse
{
$this->authorize('viewDeveloperInterest', $game);

$props = new DeveloperInterestPagePropsData(
game: GameData::fromGame($game)->include('badgeUrl', 'system'),
developers: (new BuildGameInterestedDevelopersDataAction())->execute($game)
);

return Inertia::render('game/[game]/dev-interest', $props);
}

public function random(GameListRequest $request): RedirectResponse
{
$this->authorize('viewAny', Game::class);
Expand Down
21 changes: 21 additions & 0 deletions app/Platform/Data/DeveloperInterestPagePropsData.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace App\Platform\Data;

use App\Data\UserData;
use Illuminate\Support\Collection;
use Spatie\LaravelData\Data;
use Spatie\TypeScriptTransformer\Attributes\TypeScript;

#[TypeScript('DeveloperInterestPageProps')]
class DeveloperInterestPagePropsData extends Data
{
public function __construct(
public GameData $game,
/** @var Collection<int, UserData> */
public Collection $developers,
) {
}
}
1 change: 1 addition & 0 deletions app/Platform/RouteServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ protected function mapWebRoutes(): void
});

Route::middleware(['web', 'inertia'])->group(function () {
Route::get('game/{game}/dev-interest', [GameController::class, 'devInterest'])->name('game.dev-interest');
Route::get('game/{game}/hashes', [GameHashController::class, 'index'])->name('game.hashes.index');
Route::get('game/{game}/top-achievers', [GameTopAchieversController::class, 'index'])->name('game.top-achievers.index');

Expand Down
27 changes: 0 additions & 27 deletions app/Platform/Services/GameDevInterestPageService.php

This file was deleted.

23 changes: 23 additions & 0 deletions app/Policies/GamePolicy.php
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,29 @@ public function manageContributionCredit(User $user, Game $game): bool
]);
}

public function viewDeveloperInterest(User $user, Game $game): bool
{
$hasActivePrimaryClaim = $user->loadMissing('achievementSetClaims')
->achievementSetClaims()
->whereGameId($game->id)
->primaryClaim()
->active()
->exists();

// Devs and JrDevs can see the page, but they need to have an
// active primary claim first. Collaborators for the game
// cannot open the page.
if ($hasActivePrimaryClaim) {
return true;
}

// Mods and admins can see everything.
return $user->hasAnyRole([
Role::ADMINISTRATOR,
Role::MODERATOR,
]);
}

private function canDeveloperJuniorUpdateGame(User $user, Game $game): bool
{
// If the user has a DEVELOPER_JUNIOR role, they need to have a claim
Expand Down
4 changes: 4 additions & 0 deletions lang/en_US.json
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,10 @@
"Moderation Comments - {{user}}": "Moderation Comments - {{user}}",
"Moderation Comments": "Moderation Comments",
"Columns": "Columns",
"Developer Interest - {{gameTitle}}": "Developer Interest - {{gameTitle}}",
"Developer Interest": "Developer Interest",
"The following users have added this game to their Want to Develop list:": "The following users have added this game to their Want to Develop list:",
"No users have added this game to their Want to Develop list.": "No users have added this game to their Want to Develop list.",
"Developer Feed": "Developer Feed",
"Unlocks Contributed": "Unlocks Contributed",
"Points Contributed": "Points Contributed",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -807,82 +807,86 @@ describe('Component: CreateAchievementTicketMainRoot', () => {
},
);

it('sends along data from the ?extra query param if that data is provided', async () => {
// ARRANGE
const postSpy = vi.spyOn(axios, 'post').mockResolvedValueOnce({ data: { ticketId: 123 } });
it(
'sends along data from the ?extra query param if that data is provided',
{ timeout: 10_000 },
async () => {
// ARRANGE
const postSpy = vi.spyOn(axios, 'post').mockResolvedValueOnce({ data: { ticketId: 123 } });

const achievement = createAchievement();
const gameHashes = [createGameHash({ name: 'Hash A' }), createGameHash({ name: 'Hash B' })];
const emulators = [
createEmulator({ name: 'Bizhawk' }),
createEmulator({ name: 'RALibRetro' }),
createEmulator({ name: 'RetroArch' }),
];
const achievement = createAchievement();
const gameHashes = [createGameHash({ name: 'Hash A' }), createGameHash({ name: 'Hash B' })];
const emulators = [
createEmulator({ name: 'Bizhawk' }),
createEmulator({ name: 'RALibRetro' }),
createEmulator({ name: 'RetroArch' }),
];

render<App.Platform.Data.CreateAchievementTicketPageProps>(
<CreateAchievementTicketMainRoot />,
{
pageProps: {
achievement,
emulators,
gameHashes,
auth: { user: createAuthenticatedUser({ points: 500 }) },
ziggy: createZiggyProps({
query: {
// !!!!!
extra:
'eyJ0cmlnZ2VyUmljaFByZXNlbmNlIjoi8J+Qukxpbmsg8J+Xuu+4j0RlYXRoIE1vdW50YWluIOKdpO+4jzMvMyDwn5GlMS80IPCfp78wLzQg8J+RuzAvNjAg8J+QnDAvMjQg8J+SgDUg8J+VmTEyOjAwIEFN8J+MmSJ9',
},
}),
render<App.Platform.Data.CreateAchievementTicketPageProps>(
<CreateAchievementTicketMainRoot />,
{
pageProps: {
achievement,
emulators,
gameHashes,
auth: { user: createAuthenticatedUser({ points: 500 }) },
ziggy: createZiggyProps({
query: {
// !!!!!
extra:
'eyJ0cmlnZ2VyUmljaFByZXNlbmNlIjoi8J+Qukxpbmsg8J+Xuu+4j0RlYXRoIE1vdW50YWluIOKdpO+4jzMvMyDwn5GlMS80IPCfp78wLzQg8J+RuzAvNjAg8J+QnDAvMjQg8J+SgDUg8J+VmTEyOjAwIEFN8J+MmSJ9',
},
}),
},
},
},
);
);

// ACT
await userEvent.click(screen.getByRole('combobox', { name: /issue/i }));
await userEvent.click(screen.getByRole('option', { name: /did not trigger/i }));
// ACT
await userEvent.click(screen.getByRole('combobox', { name: /issue/i }));
await userEvent.click(screen.getByRole('option', { name: /did not trigger/i }));

await userEvent.click(screen.getByRole('combobox', { name: /emulator/i }));
await userEvent.click(screen.getByRole('option', { name: /retroarch/i }));
await userEvent.click(screen.getByRole('combobox', { name: /emulator/i }));
await userEvent.click(screen.getByRole('option', { name: /retroarch/i }));

await userEvent.type(screen.getByRole('textbox', { name: /emulator core/i }), 'gambatte');
await userEvent.type(screen.getByRole('textbox', { name: /emulator core/i }), 'gambatte');

await userEvent.click(screen.getByRole('radio', { name: /softcore/i }));
await userEvent.click(screen.getByText(/softcore/i));
await userEvent.click(screen.getByRole('radio', { name: /softcore/i }));
await userEvent.click(screen.getByText(/softcore/i));

await userEvent.click(screen.getByRole('combobox', { name: /supported game file/i }));
await userEvent.click(screen.getByRole('option', { name: /hash a/i }));
await userEvent.click(screen.getByRole('combobox', { name: /supported game file/i }));
await userEvent.click(screen.getByRole('option', { name: /hash a/i }));

await userEvent.type(
screen.getByRole('textbox', { name: /description/i }),
'Something is very wrong with this achievement. I tried many things and it just wont unlock. Help.',
);
await userEvent.type(
screen.getByRole('textbox', { name: /description/i }),
'Something is very wrong with this achievement. I tried many things and it just wont unlock. Help.',
);

await userEvent.click(screen.getByRole('button', { name: /submit/i }));
await userEvent.click(screen.getByRole('button', { name: /submit/i }));

// ASSERT
await waitFor(
() => {
expect(postSpy).toHaveBeenCalledOnce();
},
{ timeout: 6000 },
);
// ASSERT
await waitFor(
() => {
expect(postSpy).toHaveBeenCalledOnce();
},
{ timeout: 6000 },
);

expect(postSpy).toHaveBeenCalledWith(['api.ticket.store'], {
core: 'gambatte',
description:
'Something is very wrong with this achievement. I tried many things and it just wont unlock. Help.',
emulator: 'RetroArch',
emulatorVersion: null,
extra:
'eyJ0cmlnZ2VyUmljaFByZXNlbmNlIjoi8J+Qukxpbmsg8J+Xuu+4j0RlYXRoIE1vdW50YWluIOKdpO+4jzMvMyDwn5GlMS80IPCfp78wLzQg8J+RuzAvNjAg8J+QnDAvMjQg8J+SgDUg8J+VmTEyOjAwIEFN8J+MmSJ9',
gameHashId: gameHashes[0].id,
issue: 2,
mode: 'softcore',
ticketableId: achievement.id,
ticketableModel: 'achievement',
});
});
expect(postSpy).toHaveBeenCalledWith(['api.ticket.store'], {
core: 'gambatte',
description:
'Something is very wrong with this achievement. I tried many things and it just wont unlock. Help.',
emulator: 'RetroArch',
emulatorVersion: null,
extra:
'eyJ0cmlnZ2VyUmljaFByZXNlbmNlIjoi8J+Qukxpbmsg8J+Xuu+4j0RlYXRoIE1vdW50YWluIOKdpO+4jzMvMyDwn5GlMS80IPCfp78wLzQg8J+RuzAvNjAg8J+QnDAvMjQg8J+SgDUg8J+VmTEyOjAwIEFN8J+MmSJ9',
gameHashId: gameHashes[0].id,
issue: 2,
mode: 'softcore',
ticketableId: achievement.id,
ticketableModel: 'achievement',
});
},
);

it('given the user is using a non-English locale, shows a warning about their ticket description', () => {
// ARRANGE
Expand Down
Loading