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;
import java.net.URI;
import java.util.UUID;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
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.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@ -33,11 +36,11 @@ public class GameProposalController {
HoldingIdentityResolver holdingIdentityResolver;
@PostMapping()
public ResponseEntity<GameView> createGameProposal(
public ResponseEntity<GameView> create(
@AuthenticationPrincipal User sender,
@RequestBody ReqGameProposalCreate gpRequest,
UriComponentsBuilder ucb) throws JsonMappingException, JsonProcessingException {
UriComponentsBuilder ucb) throws JsonMappingException, JsonProcessingException
{
final HoldingIdentity gpSender = sender.getHoldingIdentity();
final HoldingIdentity gpReceiver = holdingIdentityResolver.getByUsername(gpRequest.opponentName());
final Stone.Color gpReceiverColor = gpRequest.opponentColor();
@ -47,7 +50,7 @@ public class GameProposalController {
gpSender,
gpReceiver,
gpReceiverColor,
// gpRequest.board() // GireaIssue #3: use provided board configuration
// gpRequest.board() // GiteaIssue #3: use provided board configuration
gpRequest.message());
URI locationOfNewGameProposal = ucb
@ -60,4 +63,17 @@ public class GameProposalController {
.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) {
final RequestBody requestBody = new RequestBody(
"gp.reject-" +UUID.randomUUID(),
"gp.cancel-" +UUID.randomUUID(),
"djmil.cordacheckers.gameproposal.CancelFlow",
gameUuid);

View File

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

View File

@ -93,10 +93,14 @@ function ActionPanel({ players, gamesApi }) {
<Routes>
<Route path='new' element={
<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='archive' element={[<Backward key={1} />, <Forward key={2} />]} />
</Routes>

View File

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

View File

@ -4,7 +4,7 @@ import Wobler from '../../../components/Wobler';
import { Color } from '../../../components/Checkers';
export default function Create({ isCurrentUser, pushNewGame }) {
export default function Create({ isCurrentUser, onClick }) {
const games = useContext(GamesContext);
const hasPlayers = checkPlayers(games.newGame);
@ -17,9 +17,6 @@ export default function Create({ isCurrentUser, pushNewGame }) {
if (!hasCurrentUser)
return alert("You must be one of the selected players");
if (games.isPushingNewGame)
return; // current request is still being processed
/*
* Prepare & send NewGame request
*/
@ -32,7 +29,7 @@ export default function Create({ isCurrentUser, pushNewGame }) {
message: games.newGame.message2opponent
}
pushNewGame(reqParams);
onClick(reqParams);
}
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
});
if (!response.ok)
throw new Error(`Error! status: ${response.status}`);
if (!response.ok) {
return console.warn(`Unexpected response status: ${response.status}`, response);
}
if (onSuccess)
onSuccess(await response.json());
if (onSuccess) {
var content = (response.headers.get('Content-Type') === "application/json")
? await response.json()
: null;
console.log("rsponce", response, "content", content);
onSuccess(content);
}
// } catch (err) {
} finally {
if (onPushing)

View File

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