Skip to content

Commit

Permalink
Implemented roster item removal. (#16)
Browse files Browse the repository at this point in the history
  • Loading branch information
Utar94 authored Jan 5, 2024
1 parent 575dab9 commit 8fc8b06
Show file tree
Hide file tree
Showing 15 changed files with 183 additions and 67 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using MediatR;

namespace PokeData.Application.Roster.Commands;

internal record RemoveRosterItemCommand(ushort SpeciesId) : IRequest<Unit>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using MediatR;
using PokeData.Domain.Roster;

namespace PokeData.Application.Roster.Commands;

internal class RemoveRosterItemCommandHandler : IRequestHandler<RemoveRosterItemCommand, Unit>
{
private readonly IPokemonRosterRepository _rosterRepository;

public RemoveRosterItemCommandHandler(IPokemonRosterRepository rosterRepository)
{
_rosterRepository = rosterRepository;
}

public async Task<Unit> Handle(RemoveRosterItemCommand command, CancellationToken cancellationToken)
{
await _rosterRepository.DeleteAsync(command.SpeciesId, cancellationToken);
return Unit.Value;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ public async Task<PokemonRoster> GetAsync(CancellationToken cancellationToken)
return await _mediator.Send(new ReadPokemonRosterQuery(), cancellationToken);
}

public async Task RemoveItemAsync(ushort speciesId, CancellationToken cancellationToken)
{
await _mediator.Send(new RemoveRosterItemCommand(speciesId), cancellationToken);
}

public async Task SaveItemAsync(ushort speciesId, SaveRosterItemPayload payload, CancellationToken cancellationToken)
{
await _mediator.Send(new SaveRosterItemCommand(speciesId, payload), cancellationToken);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
public interface IPokemonRosterService
{
Task<PokemonRoster> GetAsync(CancellationToken cancellationToken = default);
Task RemoveItemAsync(ushort speciesId, CancellationToken cancellationToken = default);
Task SaveItemAsync(ushort speciesId, SaveRosterItemPayload payload, CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@

public interface IPokemonRosterRepository
{
Task DeleteAsync(ushort speciesId, CancellationToken cancellationToken = default);
Task SaveAsync(PokemonRoster roster, CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,17 @@ public PokemonRosterRepository(PokemonContext context)
_context = context;
}

public async Task DeleteAsync(ushort speciesId, CancellationToken cancellationToken)
{
PokemonRosterEntity? entity = await _context.PokemonRoster
.SingleOrDefaultAsync(x => x.PokemonSpeciesId == speciesId, cancellationToken);
if (entity != null)
{
_context.PokemonRoster.Remove(entity);
await _context.SaveChangesAsync(cancellationToken);
}
}

public async Task SaveAsync(PokemonRoster roster, CancellationToken cancellationToken)
{
PokemonRosterEntity? entity = await _context.PokemonRoster
Expand Down
14 changes: 12 additions & 2 deletions backend/src/PokeData/Controllers/PokemonRosterController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,27 @@ public async Task<ActionResult<PokemonRoster>> GetAsync(CancellationToken cancel
return Ok(await _rosterService.GetAsync(cancellationToken));
}

[HttpDelete("{speciesId}")]
public async Task<ActionResult<SavedRosterItem>> RemoveAsync(ushort speciesId, CancellationToken cancellationToken)
{
await _rosterService.RemoveItemAsync(speciesId, cancellationToken);
return Ok(await GetSavedRosterItemAsync(speciesId, cancellationToken));
}

[HttpPut("{speciesId}")]
public async Task<ActionResult<SavedRosterItem>> SaveAsync(ushort speciesId, [FromBody] SaveRosterItemPayload payload, CancellationToken cancellationToken)
{
await _rosterService.SaveItemAsync(speciesId, payload, cancellationToken);
return Ok(await GetSavedRosterItemAsync(speciesId, cancellationToken));
}

private async Task<SavedRosterItem> GetSavedRosterItemAsync(ushort speciesId, CancellationToken cancellationToken)
{
PokemonRoster roster = await _rosterService.GetAsync(cancellationToken);
SavedRosterItem result = new()
return new SavedRosterItem
{
Item = roster.Items.Single(item => item.SpeciesId == speciesId),
Stats = roster.Stats
};
return Ok(result);
}
}
2 changes: 1 addition & 1 deletion backend/src/PokeData/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
},
"Cors": {
"AllowedOrigins": [],
"AllowedMethods": [ "GET", "POST", "PUT" ],
"AllowedMethods": [ "DELETE", "GET", "POST", "PUT" ],
"AllowedHeaders": [ "Content-Type" ],
"AllowCredentials": false
},
Expand Down
2 changes: 1 addition & 1 deletion frontend/.env.development
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
__VITE_APP_API_BASE_URL="http://localhost:43551/"
VITE_APP_API_BASE_URL="https://localhost:32770/"
VITE_APP_API_BASE_URL="https://localhost:32772/"
6 changes: 5 additions & 1 deletion frontend/src/api/roster.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import type { PokemonRoster, SaveRosterItemPayload, SavedRosterItem } from "@/types/roster";
import { get, put } from ".";
import { _delete, get, put } from ".";

export async function readRoster(): Promise<PokemonRoster> {
return (await get<PokemonRoster>("/pokemon/roster")).data;
}

export async function removeRosterItem(speciesId: number): Promise<SavedRosterItem> {
return (await _delete<SavedRosterItem>(`/pokemon/roster/${speciesId}`)).data;
}

export async function saveRosterItem(speciesId: number, payload: SaveRosterItemPayload): Promise<SavedRosterItem> {
return (await put<SaveRosterItemPayload, SavedRosterItem>(`/pokemon/roster/${speciesId}`, payload)).data;
}
86 changes: 33 additions & 53 deletions frontend/src/components/RosterEditModal.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { TarButton, TarCheckbox, TarModal } from "logitar-vue3-ui";
import { computed, inject, onMounted, ref, watch } from "vue";
import { computed, ref, watch } from "vue";
import PokemonCategoryInput from "./PokemonCategoryInput.vue";
import PokemonNameInput from "./PokemonNameInput.vue";
Expand All @@ -11,16 +11,12 @@ import RosterItem from "./RosterItem.vue";
import SearchNumberInput from "./SearchNumberInput.vue";
import SearchResultSelect from "./SearchResultSelect.vue";
import SearchTextInput from "./SearchTextInput.vue";
import type { PokemonType } from "@/types/pokemon";
import type { RosterInfo, RosterItem as RosterItemType, SaveRosterItemPayload, SavedRosterItem } from "@/types/roster";
import { saveRosterItem } from "@/api/roster";
import { searchPokemonTypes } from "@/api/pokemon";
import { toastsKey, type ToastUtils } from "@/App";
const defaults = {
payload: { number: 0, name: "", category: "", region: "", primaryType: "", secondaryType: "", isBaby: false, isLegendary: false, isMythical: false },
};
const toasts = inject(toastsKey) as ToastUtils;
const props = defineProps<{
item?: RosterItemType;
Expand All @@ -33,7 +29,6 @@ const payload = ref<SaveRosterItemPayload>({ ...defaults.payload });
const searchNumber = ref<number>(0);
const searchText = ref<string>();
const selectedPokemon = ref<number>();
const types = ref<PokemonType[]>([]);
const hasChanges = computed<boolean>(() => {
const left = payload.value;
Expand Down Expand Up @@ -82,9 +77,8 @@ async function submit(): Promise<void> {
if (!loading.value && props.item) {
loading.value = true;
try {
const saved = await saveRosterItem(props.item?.speciesId, payload.value);
const saved = await saveRosterItem(props.item.speciesId, payload.value);
emit("saved", saved);
toasts.success({ text: "The roster item has been saved successfully." });
loading.value = false;
modal.value.hide();
} catch (e: unknown) {
Expand All @@ -109,18 +103,6 @@ watch(
);
watch(searchNumber, () => (selectedPokemon.value = undefined));
watch(searchText, () => (selectedPokemon.value = undefined));
onMounted(async () => {
types.value = (
await searchPokemonTypes({
numberIn: [],
search: { terms: [], operator: "And" },
sort: [{ field: "DisplayName", isDescending: false }],
skip: 0,
limit: 0,
})
).items;
});
</script>

<template>
Expand All @@ -140,44 +122,42 @@ onMounted(async () => {
</div>
<SearchResultSelect :items="items" :search-number="searchNumber" :search-text="searchText" v-model="selectedPokemon" @selected="onSelected" />
<h4>Properties</h4>
<form @submit.prevent="submit" @reset.prevent="reset">
<div class="row">
<div class="col">
<PokemonNumberInput required v-model="payload.number" />
</div>
<div class="col">
<PokemonNameInput required v-model="payload.name" />
</div>
<div class="row">
<div class="col">
<PokemonNumberInput required v-model="payload.number" />
</div>
<div class="row">
<div class="col">
<PokemonCategoryInput v-model="payload.category" />
</div>
<div class="col">
<RegionSelect required v-model="payload.region" />
</div>
<div class="col">
<PokemonNameInput required v-model="payload.name" />
</div>
<div class="row">
<div class="col">
<PokemonTypeSelect :exclude="[payload.secondaryType ?? '']" id="primary-type" label="Primary Type" required v-model="payload.primaryType" />
</div>
<div class="col">
<PokemonTypeSelect :exclude="[payload.primaryType]" id="secondary-type" label="Secondary Type" required v-model="payload.secondaryType" />
</div>
</div>
<div class="row">
<div class="col">
<PokemonCategoryInput v-model="payload.category" />
</div>
<div class="row">
<div class="col">
<TarCheckbox id="is-baby" label="Is Baby?" v-model="payload.isBaby" />
<TarCheckbox id="is-legendary" label="Is Legendary?" v-model="payload.isLegendary" />
<TarCheckbox id="is-mythical" label="Is Mythical?" v-model="payload.isMythical" />
</div>
<div class="col">
<div class="my-3 text-end">
<TarButton :disabled="loading" :icon="['fas', 'times']" text="Reset" type="reset" variant="warning" />
</div>
<div class="col">
<RegionSelect required v-model="payload.region" />
</div>
</div>
<div class="row">
<div class="col">
<PokemonTypeSelect :exclude="[payload.secondaryType ?? '']" id="primary-type" label="Primary Type" required v-model="payload.primaryType" />
</div>
<div class="col">
<PokemonTypeSelect :exclude="[payload.primaryType]" id="secondary-type" label="Secondary Type" v-model="payload.secondaryType" />
</div>
</div>
<div class="row">
<div class="col">
<TarCheckbox id="is-baby" label="Is Baby?" v-model="payload.isBaby" />
<TarCheckbox id="is-legendary" label="Is Legendary?" v-model="payload.isLegendary" />
<TarCheckbox id="is-mythical" label="Is Mythical?" v-model="payload.isMythical" />
</div>
<div class="col">
<div class="my-3 text-end">
<TarButton :disabled="loading" :icon="['fas', 'times']" text="Reset" type="reset" variant="warning" />
</div>
</div>
</form>
</div>
<template #footer>
<TarButton :icon="['fas', 'ban']" text="Cancel" variant="secondary" @click="modal.hide()" />
<TarButton :disabled="loading || !hasChanges" :icon="['fas', 'floppy-disk']" :loading="loading" text="Save" type="submit" />
Expand Down
52 changes: 52 additions & 0 deletions frontend/src/components/RosterRemoveModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<script setup lang="ts">
import { TarButton, TarModal } from "logitar-vue3-ui";
import { ref } from "vue";
import RosterItem from "./RosterItem.vue";
import type { RosterItem as RosterItemType, SavedRosterItem } from "@/types/roster";
import { removeRosterItem } from "@/api/roster";
const props = defineProps<{
item?: RosterItemType;
}>();
const loading = ref<boolean>(false);
const modal = ref<InstanceType<typeof TarModal>>();
const emit = defineEmits<{
(e: "removed", item: SavedRosterItem): void;
}>();
async function onRemove(): Promise<void> {
if (!loading.value && props.item) {
loading.value = true;
try {
const removed = await removeRosterItem(props.item.speciesId);
emit("removed", removed);
loading.value = false;
modal.value.hide();
} catch (e: unknown) {
loading.value = false;
throw e;
}
}
}
function show(): void {
modal.value.show();
}
defineExpose({ show });
</script>

<template>
<TarModal ref="modal" title="Remove Pokémon">
<template v-if="item?.destination">
<p>Do you really want to remove the following Pokémon from the roster?</p>
<RosterItem :pokemon="item.destination" />
</template>
<template #footer>
<TarButton :icon="['fas', 'ban']" text="Cancel" variant="secondary" @click="modal.hide()" />
<TarButton :disabled="loading" :icon="['fas', 'trash-can']" :loading="loading" text="Remove" variant="danger" @click="onRemove" />
</template>
</TarModal>
</template>
4 changes: 2 additions & 2 deletions frontend/src/components/RosterSelection.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ defineProps<{
}>();
defineEmits<{
(e: "removed", pokemon: RosterItemType): void;
(e: "selected", pokemon: RosterItemType): void;
}>();
</script>
Expand Down Expand Up @@ -37,8 +38,7 @@ defineEmits<{
<div class="text-end">
<template v-if="item.destination">
<TarButton class="me-2" :icon="['fas', 'pen-to-square']" text="Edit" variant="primary" @click="$emit('selected', item)" />
<TarButton disabled :icon="['fas', 'times']" text="Remove" variant="danger" />
<!-- TODO(fpion): complete Remove -->
<TarButton :icon="['fas', 'trash-can']" text="Remove" variant="danger" @click="$emit('removed', item)" />
</template>
<TarButton v-else :icon="['fas', 'plus']" text="Add" variant="success" @click="$emit('selected', item)" />
</div>
Expand Down
14 changes: 12 additions & 2 deletions frontend/src/fontAwesome.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
import { faArrowUpRightFromSquare, faArrowsRotate, faBan, faCheck, faFloppyDisk, faPenToSquare, faPlus, faTimes } from "@fortawesome/free-solid-svg-icons";
import {
faArrowUpRightFromSquare,
faArrowsRotate,
faBan,
faCheck,
faFloppyDisk,
faPenToSquare,
faPlus,
faTimes,
faTrashCan,
} from "@fortawesome/free-solid-svg-icons";
import { library } from "@fortawesome/fontawesome-svg-core";

library.add(faArrowUpRightFromSquare, faArrowsRotate, faBan, faCheck, faFloppyDisk, faPenToSquare, faPlus, faTimes);
library.add(faArrowUpRightFromSquare, faArrowsRotate, faBan, faCheck, faFloppyDisk, faPenToSquare, faPlus, faTimes, faTrashCan);
Loading

0 comments on commit 8fc8b06

Please sign in to comment.