feat: ajout de la fonctionnalité de suppression de mangas, incluant une modale de confirmation pour l'utilisateur, la gestion des erreurs et l'intégration avec l'API pour supprimer les mangas et leurs chapitres associés. Mise à jour des composants Vue et ajout de tests pour valider cette nouvelle fonctionnalité.
This commit is contained in:
parent
7f9d583c94
commit
f09f744a9b
@@ -377,4 +377,24 @@ export class ApiMangaRepository {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async deleteManga(mangaId) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/mangas/${mangaId}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to delete manga');
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('API Error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
<template>
|
||||||
|
<TransitionRoot as="template" :show="isOpen">
|
||||||
|
<Dialog as="div" class="relative z-50" @close="closeModal">
|
||||||
|
<TransitionChild
|
||||||
|
as="template"
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enter-from="opacity-0"
|
||||||
|
enter-to="opacity-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leave-from="opacity-100"
|
||||||
|
leave-to="opacity-0"
|
||||||
|
>
|
||||||
|
<div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
|
||||||
|
</TransitionChild>
|
||||||
|
|
||||||
|
<div class="fixed inset-0 z-10 overflow-y-auto">
|
||||||
|
<div class="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
||||||
|
<TransitionChild
|
||||||
|
as="template"
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enter-from="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
enter-to="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leave-from="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
leave-to="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
>
|
||||||
|
<DialogPanel class="relative transform overflow-hidden rounded-lg bg-white px-6 pb-6 pt-6 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg">
|
||||||
|
<div class="mb-6">
|
||||||
|
<DialogTitle as="h3" class="text-lg font-semibold leading-6 text-gray-900">
|
||||||
|
Supprimer le manga
|
||||||
|
</DialogTitle>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Error state -->
|
||||||
|
<div v-if="error" class="mb-6 bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
|
||||||
|
{{ error.message || 'Une erreur est survenue lors de la suppression.' }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Warning message -->
|
||||||
|
<div class="mb-6">
|
||||||
|
<div class="flex items-center mb-4">
|
||||||
|
<ExclamationTriangleIcon class="h-6 w-6 text-red-500 mr-3" />
|
||||||
|
<span class="text-sm font-medium text-gray-900">Action irréversible</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-gray-600 mb-4">
|
||||||
|
Êtes-vous sûr de vouloir supprimer le manga <strong>"{{ manga?.title }}"</strong> ?
|
||||||
|
</p>
|
||||||
|
<div class="bg-yellow-50 border border-yellow-200 rounded-md p-4">
|
||||||
|
<div class="flex">
|
||||||
|
<ExclamationTriangleIcon class="h-5 w-5 text-yellow-400" />
|
||||||
|
<div class="ml-3">
|
||||||
|
<h3 class="text-sm font-medium text-yellow-800">
|
||||||
|
Attention
|
||||||
|
</h3>
|
||||||
|
<div class="mt-2 text-sm text-yellow-700">
|
||||||
|
<p>Cette action supprimera définitivement :</p>
|
||||||
|
<ul class="list-disc list-inside mt-1 space-y-1">
|
||||||
|
<li>Le manga et toutes ses métadonnées</li>
|
||||||
|
<li>Tous les chapitres associés</li>
|
||||||
|
<li>Tous les fichiers CBZ téléchargés</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Action buttons -->
|
||||||
|
<div class="mt-6 flex justify-end space-x-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="inline-flex justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
|
||||||
|
@click="closeModal"
|
||||||
|
:disabled="isLoading"
|
||||||
|
>
|
||||||
|
Annuler
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="inline-flex justify-center rounded-md border border-transparent bg-red-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
@click="confirmDelete"
|
||||||
|
:disabled="isLoading"
|
||||||
|
>
|
||||||
|
<ArrowPathIcon v-if="isLoading" class="h-4 w-4 mr-2 animate-spin" />
|
||||||
|
{{ isLoading ? 'Suppression...' : 'Supprimer définitivement' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</DialogPanel>
|
||||||
|
</TransitionChild>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</TransitionRoot>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { Dialog, DialogPanel, DialogTitle, TransitionChild, TransitionRoot } from '@headlessui/vue';
|
||||||
|
import { ArrowPathIcon, ExclamationTriangleIcon } from '@heroicons/vue/24/outline';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
isOpen: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
manga: {
|
||||||
|
type: Object,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
isLoading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
type: Object,
|
||||||
|
default: null
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['close', 'confirm']);
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
emit('close');
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmDelete = () => {
|
||||||
|
emit('confirm');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { useMutation, useQueryClient } from '@tanstack/vue-query';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { ApiMangaRepository } from '../../infrastructure/api/apiMangaRepository';
|
||||||
|
|
||||||
|
export function useMangaDelete() {
|
||||||
|
const mangaRepository = new ApiMangaRepository();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const isDeleteModalOpen = ref(false);
|
||||||
|
|
||||||
|
const deleteMutation = useMutation({
|
||||||
|
mutationFn: ({ mangaId }) => {
|
||||||
|
return mangaRepository.deleteManga(mangaId);
|
||||||
|
},
|
||||||
|
onSuccess: (data, variables) => {
|
||||||
|
// Invalider et refetch les listes de mangas
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['mangas'] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['manga-search'] });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const openDeleteModal = () => {
|
||||||
|
isDeleteModalOpen.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeDeleteModal = () => {
|
||||||
|
isDeleteModalOpen.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteManga = async (mangaId) => {
|
||||||
|
try {
|
||||||
|
await deleteMutation.mutateAsync({ mangaId });
|
||||||
|
closeDeleteModal();
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur lors de la suppression du manga:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
isDeleteModalOpen,
|
||||||
|
openDeleteModal,
|
||||||
|
closeDeleteModal,
|
||||||
|
deleteManga,
|
||||||
|
isLoading: deleteMutation.isPending,
|
||||||
|
error: deleteMutation.error,
|
||||||
|
isSuccess: deleteMutation.isSuccess
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -68,6 +68,16 @@
|
|||||||
@close="closeManageChaptersModal"
|
@close="closeManageChaptersModal"
|
||||||
@save="saveChaptersChanges"
|
@save="saveChaptersChanges"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- Modale de suppression du manga -->
|
||||||
|
<MangaDeleteModal
|
||||||
|
:is-open="isDeleteModalOpen"
|
||||||
|
:manga="currentManga"
|
||||||
|
:is-loading="isDeleting"
|
||||||
|
:error="deleteError"
|
||||||
|
@close="closeDeleteModal"
|
||||||
|
@confirm="confirmDeleteManga"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="isLoadingDetails" class="flex justify-center items-center h-64">
|
<div v-else-if="isLoadingDetails" class="flex justify-center items-center h-64">
|
||||||
@@ -93,8 +103,9 @@
|
|||||||
WrenchIcon
|
WrenchIcon
|
||||||
} from '@heroicons/vue/24/outline';
|
} from '@heroicons/vue/24/outline';
|
||||||
import { computed, onUnmounted, ref, watch } from 'vue';
|
import { computed, onUnmounted, ref, watch } from 'vue';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
import { useMangaDelete } from '../composables/useMangaDelete';
|
||||||
import { useMangaDetails } from '../composables/useMangaDetails';
|
import { useMangaDetails } from '../composables/useMangaDetails';
|
||||||
import { useMangaEdit } from '../composables/useMangaEdit';
|
import { useMangaEdit } from '../composables/useMangaEdit';
|
||||||
import { useMangaMonitoring } from '../composables/useMangaMonitoring';
|
import { useMangaMonitoring } from '../composables/useMangaMonitoring';
|
||||||
@@ -103,6 +114,7 @@ import { useMangaRefresh } from '../composables/useMangaRefresh';
|
|||||||
import { useMangaVolumes } from '../composables/useMangaVolumes';
|
import { useMangaVolumes } from '../composables/useMangaVolumes';
|
||||||
|
|
||||||
import ManageChaptersModal from '../components/ManageChaptersModal.vue';
|
import ManageChaptersModal from '../components/ManageChaptersModal.vue';
|
||||||
|
import MangaDeleteModal from '../components/MangaDeleteModal.vue';
|
||||||
import MangaEditModal from '../components/MangaEditModal.vue';
|
import MangaEditModal from '../components/MangaEditModal.vue';
|
||||||
import MangaHeader from '../components/MangaHeader.vue';
|
import MangaHeader from '../components/MangaHeader.vue';
|
||||||
import MangaPreferredSourcesModal from '../components/MangaPreferredSourcesModal.vue';
|
import MangaPreferredSourcesModal from '../components/MangaPreferredSourcesModal.vue';
|
||||||
@@ -114,6 +126,7 @@ import Toolbar from '../../../../shared/components/ui/Toolbar.vue';
|
|||||||
import { useMangaStore } from '../../application/store/mangaStore';
|
import { useMangaStore } from '../../application/store/mangaStore';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
const mangaStore = useMangaStore();
|
const mangaStore = useMangaStore();
|
||||||
|
|
||||||
const mangaId = computed(() => route.params.id || null);
|
const mangaId = computed(() => route.params.id || null);
|
||||||
@@ -174,6 +187,16 @@ import { useMangaStore } from '../../application/store/mangaStore';
|
|||||||
toggleError: monitoringError
|
toggleError: monitoringError
|
||||||
} = useMangaMonitoring();
|
} = useMangaMonitoring();
|
||||||
|
|
||||||
|
// Composable pour la suppression
|
||||||
|
const {
|
||||||
|
isDeleteModalOpen,
|
||||||
|
openDeleteModal,
|
||||||
|
closeDeleteModal,
|
||||||
|
deleteManga,
|
||||||
|
isLoading: isDeleting,
|
||||||
|
error: deleteError
|
||||||
|
} = useMangaDelete();
|
||||||
|
|
||||||
// Charger les chapitres dans le store quand le manga est chargé
|
// Charger les chapitres dans le store quand le manga est chargé
|
||||||
watch(
|
watch(
|
||||||
mangaId,
|
mangaId,
|
||||||
@@ -202,6 +225,18 @@ import { useMangaStore } from '../../application/store/mangaStore';
|
|||||||
chaptersError.value = null;
|
chaptersError.value = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const confirmDeleteManga = async () => {
|
||||||
|
if (!mangaId.value) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await deleteManga(mangaId.value);
|
||||||
|
// Rediriger vers la liste des mangas après suppression
|
||||||
|
router.push('/manga');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur lors de la suppression du manga:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const savePreferredSources = async (sourceIds) => {
|
const savePreferredSources = async (sourceIds) => {
|
||||||
try {
|
try {
|
||||||
await saveSourcesOrder(sourceIds);
|
await saveSourcesOrder(sourceIds);
|
||||||
@@ -334,7 +369,7 @@ import { useMangaStore } from '../../application/store/mangaStore';
|
|||||||
icon: TrashIcon,
|
icon: TrashIcon,
|
||||||
label: 'Delete',
|
label: 'Delete',
|
||||||
type: 'button',
|
type: 'button',
|
||||||
onClick: () => console.log('Delete')
|
onClick: openDeleteModal
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: isAllExpanded.value ? ChevronDoubleUpIcon : ChevronDoubleDownIcon,
|
icon: isAllExpanded.value ? ChevronDoubleUpIcon : ChevronDoubleDownIcon,
|
||||||
|
|||||||
12
src/Domain/Manga/Application/Command/DeleteManga.php
Normal file
12
src/Domain/Manga/Application/Command/DeleteManga.php
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domain\Manga\Application\Command;
|
||||||
|
|
||||||
|
use App\Domain\Shared\Domain\Contract\CommandInterface;
|
||||||
|
|
||||||
|
readonly class DeleteManga implements CommandInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public string $mangaId
|
||||||
|
) {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domain\Manga\Application\CommandHandler;
|
||||||
|
|
||||||
|
use App\Domain\Manga\Application\Command\DeleteManga;
|
||||||
|
use App\Domain\Manga\Domain\Contract\Repository\MangaRepositoryInterface;
|
||||||
|
use App\Domain\Manga\Domain\Exception\MangaNotFoundException;
|
||||||
|
use App\Domain\Shared\Domain\Contract\CommandHandlerInterface;
|
||||||
|
use App\Domain\Shared\Domain\Contract\CommandInterface;
|
||||||
|
|
||||||
|
readonly class DeleteMangaHandler implements CommandHandlerInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private MangaRepositoryInterface $mangaRepository
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function handle(CommandInterface $command): void
|
||||||
|
{
|
||||||
|
assert($command instanceof DeleteManga);
|
||||||
|
|
||||||
|
$manga = $this->mangaRepository->findById($command->mangaId);
|
||||||
|
|
||||||
|
if (!$manga) {
|
||||||
|
throw new MangaNotFoundException(sprintf('Manga with id %s not found', $command->mangaId));
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->mangaRepository->delete($manga);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domain\Manga\Infrastructure\ApiPlatform\Resource;
|
||||||
|
|
||||||
|
use ApiPlatform\Metadata\ApiResource;
|
||||||
|
use ApiPlatform\Metadata\Delete;
|
||||||
|
use App\Domain\Manga\Infrastructure\ApiPlatform\State\Processor\DeleteMangaProcessor;
|
||||||
|
|
||||||
|
#[ApiResource(
|
||||||
|
shortName: 'Manga',
|
||||||
|
operations: [
|
||||||
|
new Delete(
|
||||||
|
uriTemplate: '/mangas/{id}',
|
||||||
|
processor: DeleteMangaProcessor::class,
|
||||||
|
name: 'delete_manga',
|
||||||
|
read: false,
|
||||||
|
openapiContext: [
|
||||||
|
'summary' => 'Delete a manga',
|
||||||
|
'description' => 'Permanently deletes a manga and all its associated chapters',
|
||||||
|
'parameters' => [
|
||||||
|
[
|
||||||
|
'name' => 'id',
|
||||||
|
'in' => 'path',
|
||||||
|
'required' => true,
|
||||||
|
'schema' => [
|
||||||
|
'type' => 'string'
|
||||||
|
],
|
||||||
|
'description' => 'The manga ID'
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'responses' => [
|
||||||
|
'204' => [
|
||||||
|
'description' => 'Manga successfully deleted'
|
||||||
|
],
|
||||||
|
'404' => [
|
||||||
|
'description' => 'Manga not found'
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)]
|
||||||
|
class DeleteMangaResource
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public string $id
|
||||||
|
) {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domain\Manga\Infrastructure\ApiPlatform\State\Processor;
|
||||||
|
|
||||||
|
use ApiPlatform\Metadata\Operation;
|
||||||
|
use ApiPlatform\State\ProcessorInterface;
|
||||||
|
use App\Domain\Manga\Application\Command\DeleteManga;
|
||||||
|
use App\Domain\Manga\Application\CommandHandler\DeleteMangaHandler;
|
||||||
|
use App\Domain\Manga\Domain\Exception\MangaNotFoundException;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||||
|
|
||||||
|
readonly class DeleteMangaProcessor implements ProcessorInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private DeleteMangaHandler $handler
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): int
|
||||||
|
{
|
||||||
|
if (!isset($uriVariables['id'])) {
|
||||||
|
throw new \InvalidArgumentException('Manga ID is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$command = new DeleteManga($uriVariables['id']);
|
||||||
|
$this->handler->handle($command);
|
||||||
|
|
||||||
|
return Response::HTTP_NO_CONTENT;
|
||||||
|
} catch (MangaNotFoundException $e) {
|
||||||
|
throw new NotFoundHttpException('Manga not found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ class Manga
|
|||||||
private readonly string $description,
|
private readonly string $description,
|
||||||
private readonly string $author,
|
private readonly string $author,
|
||||||
private readonly string $publicationYear,
|
private readonly string $publicationYear,
|
||||||
|
private readonly bool $monitored = false,
|
||||||
private readonly array $preferredSources = [],
|
private readonly array $preferredSources = [],
|
||||||
private readonly array $alternativeSlugs = [],
|
private readonly array $alternativeSlugs = [],
|
||||||
) {
|
) {
|
||||||
@@ -73,4 +74,9 @@ class Manga
|
|||||||
{
|
{
|
||||||
return $this->alternativeSlugs;
|
return $this->alternativeSlugs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function isMonitored(): bool
|
||||||
|
{
|
||||||
|
return $this->monitored;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,17 +5,26 @@ namespace App\Domain\Scraping\Infrastructure\EventListener;
|
|||||||
use App\Domain\Manga\Domain\Event\ChapterReadyForScraping;
|
use App\Domain\Manga\Domain\Event\ChapterReadyForScraping;
|
||||||
use App\Domain\Scraping\Application\Command\ScrapeChapter;
|
use App\Domain\Scraping\Application\Command\ScrapeChapter;
|
||||||
use App\Domain\Scraping\Application\CommandHandler\ScrapeChapterHandler;
|
use App\Domain\Scraping\Application\CommandHandler\ScrapeChapterHandler;
|
||||||
|
use App\Domain\Scraping\Domain\Contract\Repository\ChapterRepositoryInterface;
|
||||||
|
use App\Domain\Scraping\Domain\Contract\Repository\MangaRepositoryInterface;
|
||||||
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
||||||
|
|
||||||
class AutoScrapingListener
|
class AutoScrapingListener
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly ScrapeChapterHandler $scrapeChapterHandler
|
private readonly ScrapeChapterHandler $scrapeChapterHandler,
|
||||||
|
private readonly ChapterRepositoryInterface $chapterRepository,
|
||||||
|
private readonly MangaRepositoryInterface $mangaRepository,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
#[AsMessageHandler]
|
#[AsMessageHandler]
|
||||||
public function onChapterReadyForScraping(ChapterReadyForScraping $event): void
|
public function onChapterReadyForScraping(ChapterReadyForScraping $event): void
|
||||||
{
|
{
|
||||||
|
$chapter = $this->chapterRepository->getById($event->chapterId->getValue());
|
||||||
|
$manga = $this->mangaRepository->getById($chapter->mangaId);
|
||||||
|
|
||||||
|
if ($manga->isMonitored()) {
|
||||||
$this->scrapeChapterHandler->handle(new ScrapeChapter($event->chapterId->getValue()));
|
$this->scrapeChapterHandler->handle(new ScrapeChapter($event->chapterId->getValue()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -35,14 +35,15 @@ readonly class LegacyMangaRepository implements MangaRepositoryInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
return new Manga(
|
return new Manga(
|
||||||
(string) $mangaEntity->getId(),
|
id: (string) $mangaEntity->getId(),
|
||||||
$mangaEntity->getTitle(),
|
title: $mangaEntity->getTitle(),
|
||||||
$mangaEntity->getSlug(),
|
slug: $mangaEntity->getSlug(),
|
||||||
$mangaEntity->getDescription() ?? '',
|
description: $mangaEntity->getDescription() ?? '',
|
||||||
$mangaEntity->getAuthor() ?? '',
|
author: $mangaEntity->getAuthor() ?? '',
|
||||||
(string) ($mangaEntity->getPublicationYear() ?? ''),
|
publicationYear: (string) ($mangaEntity->getPublicationYear() ?? ''),
|
||||||
$preferredSourceIds,
|
monitored: $mangaEntity->isMonitored() ?? false,
|
||||||
$mangaEntity->getAlternativeSlugs() ?? []
|
preferredSources: $preferredSourceIds,
|
||||||
|
alternativeSlugs: $mangaEntity->getAlternativeSlugs() ?? []
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
86
tests/Feature/Manga/DeleteMangaTest.php
Normal file
86
tests/Feature/Manga/DeleteMangaTest.php
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tests\Feature\Manga;
|
||||||
|
|
||||||
|
use App\Entity\Manga;
|
||||||
|
use App\Entity\Chapter;
|
||||||
|
use App\Factory\MangaFactory;
|
||||||
|
use App\Factory\ChapterFactory;
|
||||||
|
use App\Tests\Feature\AbstractApiTestCase;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Zenstruck\Foundry\Test\Factories;
|
||||||
|
use Zenstruck\Foundry\Test\ResetDatabase;
|
||||||
|
|
||||||
|
class DeleteMangaTest extends AbstractApiTestCase
|
||||||
|
{
|
||||||
|
use ResetDatabase, Factories;
|
||||||
|
|
||||||
|
public function test_it_deletes_manga_with_chapters(): void
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
$manga = MangaFactory::createOne([
|
||||||
|
'title' => 'One Piece',
|
||||||
|
'slug' => 'one-piece'
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create chapters for the manga
|
||||||
|
ChapterFactory::createMany(3, [
|
||||||
|
'manga' => $manga,
|
||||||
|
'number' => 1.0,
|
||||||
|
'title' => 'Chapter 1',
|
||||||
|
'visible' => true
|
||||||
|
]);
|
||||||
|
|
||||||
|
ChapterFactory::createMany(2, [
|
||||||
|
'manga' => $manga,
|
||||||
|
'number' => 2.0,
|
||||||
|
'title' => 'Chapter 2',
|
||||||
|
'visible' => true
|
||||||
|
]);
|
||||||
|
|
||||||
|
ChapterFactory::createMany(1, [
|
||||||
|
'manga' => $manga,
|
||||||
|
'number' => 3.0,
|
||||||
|
'title' => 'Chapter 3',
|
||||||
|
'visible' => true
|
||||||
|
]);
|
||||||
|
|
||||||
|
$mangaId = $manga->getId();
|
||||||
|
|
||||||
|
// Verify chapters exist before deletion
|
||||||
|
$chaptersBefore = $this->entityManager->getRepository(Chapter::class)->findBy(['manga' => $mangaId]);
|
||||||
|
$this->assertCount(6, $chaptersBefore);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
static::createClient()->request('DELETE', "/api/mangas/{$mangaId}");
|
||||||
|
|
||||||
|
// Then
|
||||||
|
$this->assertResponseStatusCodeSame(204);
|
||||||
|
|
||||||
|
// Verify the manga was deleted
|
||||||
|
$freshManga = $this->entityManager->find(Manga::class, $mangaId);
|
||||||
|
$this->assertNull($freshManga);
|
||||||
|
|
||||||
|
// Verify all chapters were also deleted (cascade)
|
||||||
|
$chaptersAfter = $this->entityManager->getRepository(Chapter::class)->findBy(['manga' => $mangaId]);
|
||||||
|
$this->assertCount(0, $chaptersAfter);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_returns_404_for_non_existent_manga(): void
|
||||||
|
{
|
||||||
|
// When
|
||||||
|
static::createClient()->request('DELETE', '/api/mangas/999999');
|
||||||
|
|
||||||
|
// Then
|
||||||
|
$this->assertResponseStatusCodeSame(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_returns_404_for_missing_id(): void
|
||||||
|
{
|
||||||
|
// When
|
||||||
|
static::createClient()->request('DELETE', '/api/mangas/');
|
||||||
|
|
||||||
|
// Then
|
||||||
|
$this->assertResponseStatusCodeSame(404);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user