GameProposal: Cancel

- doPushing
 * JSON content type detection
 * unexpeted responce status: show warning instead of throwing exeption
- useGamesApi: if already pushing - do not push another one
wobling for Cancel button
- middleware: GameProposal controller: handle Cancel requests
This commit is contained in:
djmil 2023-11-15 13:27:52 +01:00
parent 2522da6349
commit 3063146a76
8 changed files with 70 additions and 32 deletions

View File

@ -1,11 +1,14 @@
package djmil.cordacheckers.api; package djmil.cordacheckers.api;
import java.net.URI; import java.net.URI;
import java.util.UUID;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
@ -33,11 +36,11 @@ public class GameProposalController {
HoldingIdentityResolver holdingIdentityResolver; HoldingIdentityResolver holdingIdentityResolver;
@PostMapping() @PostMapping()
public ResponseEntity<GameView> createGameProposal( public ResponseEntity<GameView> create(
@AuthenticationPrincipal User sender, @AuthenticationPrincipal User sender,
@RequestBody ReqGameProposalCreate gpRequest, @RequestBody ReqGameProposalCreate gpRequest,
UriComponentsBuilder ucb) throws JsonMappingException, JsonProcessingException { UriComponentsBuilder ucb) throws JsonMappingException, JsonProcessingException
{
final HoldingIdentity gpSender = sender.getHoldingIdentity(); final HoldingIdentity gpSender = sender.getHoldingIdentity();
final HoldingIdentity gpReceiver = holdingIdentityResolver.getByUsername(gpRequest.opponentName()); final HoldingIdentity gpReceiver = holdingIdentityResolver.getByUsername(gpRequest.opponentName());
final Stone.Color gpReceiverColor = gpRequest.opponentColor(); final Stone.Color gpReceiverColor = gpRequest.opponentColor();
@ -47,7 +50,7 @@ public class GameProposalController {
gpSender, gpSender,
gpReceiver, gpReceiver,
gpReceiverColor, gpReceiverColor,
// gpRequest.board() // GireaIssue #3: use provided board configuration // gpRequest.board() // GiteaIssue #3: use provided board configuration
gpRequest.message()); gpRequest.message());
URI locationOfNewGameProposal = ucb URI locationOfNewGameProposal = ucb
@ -60,4 +63,17 @@ public class GameProposalController {
.body(gameStateView); .body(gameStateView);
} }
@PutMapping("/{uuid}/cancel")
public ResponseEntity<GameView> cancel(
@AuthenticationPrincipal User issuer,
@PathVariable UUID uuid
) {
final GameView canceledGameView = cordaClient.gameProposalCancel(
issuer.getHoldingIdentity(),
uuid
);
return ResponseEntity
.ok(canceledGameView);
}
} }

View File

@ -134,7 +134,7 @@ public class CordaClient {
public GameView gameProposalCancel(HoldingIdentity holdingIdentity, UUID gameUuid) { public GameView gameProposalCancel(HoldingIdentity holdingIdentity, UUID gameUuid) {
final RequestBody requestBody = new RequestBody( final RequestBody requestBody = new RequestBody(
"gp.reject-" +UUID.randomUUID(), "gp.cancel-" +UUID.randomUUID(),
"djmil.cordacheckers.gameproposal.CancelFlow", "djmil.cordacheckers.gameproposal.CancelFlow",
gameUuid); gameUuid);

View File

@ -1,4 +1,5 @@
import { usePolling, doPushing } from '../hook/api'; import { usePolling, doPushing } from '../hook/api';
import { gamesInitialState } from '../reducer/games';
export default function useGamesApi(gamesReducer, config) { export default function useGamesApi(gamesReducer, config) {
const [games, dispatchGames] = gamesReducer; const [games, dispatchGames] = gamesReducer;
@ -16,19 +17,23 @@ export default function useGamesApi(gamesReducer, config) {
return games; return games;
} }
return { return {
pollGamesList: usePollingGamesList, pollGamesList: usePollingGamesList,
pushNewGame: (reqParams) => doPushing('/api/gameproposal', 'POST', reqParams, { pushNewGame: ({ opponentName, opponentColor, board, message }) => ifNot(games.isPushingNewGame) &&
onPushing: (isPushingNewGame) => dispatchGames({ type: 'next', isPushingNewGame }), doPushing('/api/gameproposal', 'POST', { opponentName, opponentColor, board, message }, {
onSuccess: (game) => dispatchGames({ type: 'next', gamesList: [game, ...games.gamesList], newGame: emptyNewGame }) onPushing: (isPushingNewGame) => dispatchGames({ type: 'next', isPushingNewGame }),
}), onSuccess: (game) => dispatchGames({ type: 'next', gamesList: [game, ...games.gamesList], newGame: gamesInitialState.newGame })
}),
pushGameProposalCancel: ({ uuid }) => ifNot(games.isPushingGameProposalCancel) &&
doPushing(`/api/gameproposal/${uuid}/cancel`, 'PUT', null, {
onPushing: (isPushingGameProposalCancel) => dispatchGames({ type: 'next', isPushingGameProposalCancel }),
onSuccess: (canceledGame) => dispatchGames({ type: 'next', gamesList: games.nextGameList(canceledGame), proposal: gamesInitialState.proposal })
}),
} }
} }
const emptyNewGame = { function ifNot(expression) {
whitePlayer: '', return !expression;
blackPlayer: '',
message2opponent: ''
} }

View File

@ -93,10 +93,14 @@ function ActionPanel({ players, gamesApi }) {
<Routes> <Routes>
<Route path='new' element={ <Route path='new' element={
<Create isCurrentUser={players.isCurrentUser} <Create isCurrentUser={players.isCurrentUser}
pushNewGame={(reqParams) => gamesApi.pushNewGame(reqParams)} onClick={(reqParams) => gamesApi.pushNewGame(reqParams)}
/> />
} /> } />
<Route path='proposal' element={[<Accept key={1} />, <Reject key={2} />, <Cancel key={3} />]} /> <Route path='proposal' element={[
<Accept key={1} />,
<Reject key={2} />,
<Cancel key={3} onClick={({uuid}) => gamesApi.pushGameProposalCancel({ uuid })} />
]} />
<Route path='active' element={[<DrawReq key={1} />, <DrawAcq key={2} />, <Surrender key={3} />]} /> <Route path='active' element={[<DrawReq key={1} />, <DrawAcq key={2} />, <Surrender key={3} />]} />
<Route path='archive' element={[<Backward key={1} />, <Forward key={2} />]} /> <Route path='archive' element={[<Backward key={1} />, <Forward key={2} />]} />
</Routes> </Routes>

View File

@ -1,15 +1,16 @@
import React, { useContext } from 'react'; import React, { useContext } from 'react';
import Wobler from '../../../components/Wobler';
import { GamesContext } from '../../../context/games'; import { GamesContext } from '../../../context/games';
export default function Cancel() { export default function Cancel({ onClick }) {
const games = useContext(GamesContext); const games = useContext(GamesContext);
const selectedGame = games.findGame({ uuid: games.proposal.selectedUUID }); const selectedGame = games.findGame({ uuid: games.proposal.selectedUUID });
if (selectedGame?.status === 'GAME_PROPOSAL_WAIT_FOR_OPPONENT') if (selectedGame?.status === 'GAME_PROPOSAL_WAIT_FOR_OPPONENT')
return ( return (
<button className='Cancel'> <button className='Cancel' onClick={() => onClick({ uuid: games.proposal.selectedUUID })} >
Cancel <Wobler text="Cancel" dance={games.isPushingGameProposalCancel} />
</button> </button>
) )
} }

View File

@ -4,7 +4,7 @@ import Wobler from '../../../components/Wobler';
import { Color } from '../../../components/Checkers'; import { Color } from '../../../components/Checkers';
export default function Create({ isCurrentUser, pushNewGame }) { export default function Create({ isCurrentUser, onClick }) {
const games = useContext(GamesContext); const games = useContext(GamesContext);
const hasPlayers = checkPlayers(games.newGame); const hasPlayers = checkPlayers(games.newGame);
@ -17,9 +17,6 @@ export default function Create({ isCurrentUser, pushNewGame }) {
if (!hasCurrentUser) if (!hasCurrentUser)
return alert("You must be one of the selected players"); return alert("You must be one of the selected players");
if (games.isPushingNewGame)
return; // current request is still being processed
/* /*
* Prepare & send NewGame request * Prepare & send NewGame request
*/ */
@ -32,7 +29,7 @@ export default function Create({ isCurrentUser, pushNewGame }) {
message: games.newGame.message2opponent message: games.newGame.message2opponent
} }
pushNewGame(reqParams); onClick(reqParams);
} }
return ( return (

View File

@ -72,11 +72,18 @@ export async function doPushing(uri, method, data, { onSuccess, onPushing }) {
body: JSON.stringify(data), // body data type must match "Content-Type" header body: JSON.stringify(data), // body data type must match "Content-Type" header
}); });
if (!response.ok) if (!response.ok) {
throw new Error(`Error! status: ${response.status}`); return console.warn(`Unexpected response status: ${response.status}`, response);
}
if (onSuccess) if (onSuccess) {
onSuccess(await response.json()); var content = (response.headers.get('Content-Type') === "application/json")
? await response.json()
: null;
console.log("rsponce", response, "content", content);
onSuccess(content);
}
// } catch (err) { // } catch (err) {
} finally { } finally {
if (onPushing) if (onPushing)

View File

@ -1,7 +1,7 @@
import { useReducer } from 'react'; import { useReducer } from 'react';
import { nextState } from '../util/StateHelper'; import { nextState } from '../util/StateHelper';
const initialState = { export const gamesInitialState = {
gamesList: null, gamesList: null,
newGame: { newGame: {
@ -18,8 +18,10 @@ const initialState = {
// Network // Network
isPollingGamesList: false, isPollingGamesList: false,
isPushingNewGame: false, isPushingNewGame: false,
isPushingGameProposalCancel: false,
findGame, findGame,
nextGameList,
}; };
function reducer(state, action) { function reducer(state, action) {
@ -46,9 +48,15 @@ function reducer(state, action) {
} }
export default function useGamesReducer() { export default function useGamesReducer() {
return useReducer(reducer, initialState); return useReducer(reducer, gamesInitialState);
} }
function findGame({uuid}) { function findGame({ uuid }) {
return this.gamesList?.find((game) => game.uuid === uuid); return this.gamesList?.find((game) => game.uuid === uuid);
}
function nextGameList(nextGame) {
return this.gamesList?.map(
(game) => (nextGame?.uuid === game.uuid) ? nextGame : game
);
} }