diff --git a/assets/vue/app/domain/setting/application/store/contentSourceStore.js b/assets/vue/app/domain/setting/application/store/contentSourceStore.js index 17b710d..ef8d7bb 100644 --- a/assets/vue/app/domain/setting/application/store/contentSourceStore.js +++ b/assets/vue/app/domain/setting/application/store/contentSourceStore.js @@ -28,6 +28,10 @@ export const useContentSourceStore = defineStore('contentSource', { // Health check state checkingHealth: false, checkHealthError: null, + + // Delete state + deleting: false, + deleteError: null, }), getters: { @@ -172,6 +176,28 @@ export const useContentSourceStore = defineStore('contentSource', { } }, + // Delete a source + async deleteSource(id) { + if (this.deleting) return; + + this.deleting = true; + this.deleteError = null; + + try { + await contentSourceRepository.delete(id); + this.sources = this.sources.filter(source => source.id !== id); + if (this.currentSource && this.currentSource.id === id) { + this.currentSource = null; + } + } catch (error) { + this.deleteError = error.message; + console.error('Erreur lors de la suppression de la source:', error); + throw error; + } finally { + this.deleting = false; + } + }, + // Clear current source clearCurrentSource() { this.currentSource = null; diff --git a/assets/vue/app/domain/setting/infrastructure/api/apiContentSourceRepository.js b/assets/vue/app/domain/setting/infrastructure/api/apiContentSourceRepository.js index 0e2c78a..68a2d51 100644 --- a/assets/vue/app/domain/setting/infrastructure/api/apiContentSourceRepository.js +++ b/assets/vue/app/domain/setting/infrastructure/api/apiContentSourceRepository.js @@ -93,6 +93,17 @@ export class ApiContentSourceRepository { } } + /** + * Supprime une source de contenu + */ + async delete(id) { + try { + await this.apiClient.delete(`/content-sources/${id}`); + } catch (error) { + throw new Error(error.response?.data?.message || 'Erreur lors de la suppression de la source'); + } + } + /** * Teste une configuration de scraper */ diff --git a/assets/vue/app/domain/setting/presentation/components/ContentSourceDeleteModal.vue b/assets/vue/app/domain/setting/presentation/components/ContentSourceDeleteModal.vue new file mode 100644 index 0000000..8d49ec7 --- /dev/null +++ b/assets/vue/app/domain/setting/presentation/components/ContentSourceDeleteModal.vue @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + Supprimer la source de contenu + + + + + + {{ error }} + + + + + + + Action irréversible + + + Êtes-vous sûr de vouloir supprimer la source {{ source?.baseUrl }} ? + + + + + + + Attention + + + Cette source ne pourra plus être utilisée pour le scraping des chapitres. + + + + + + + + + + Annuler + + + + {{ isLoading ? 'Suppression...' : 'Supprimer définitivement' }} + + + + + + + + + + + diff --git a/assets/vue/app/domain/setting/presentation/pages/ScrapperEdit.vue b/assets/vue/app/domain/setting/presentation/pages/ScrapperEdit.vue index 39aecd4..596f898 100644 --- a/assets/vue/app/domain/setting/presentation/pages/ScrapperEdit.vue +++ b/assets/vue/app/domain/setting/presentation/pages/ScrapperEdit.vue @@ -176,6 +176,15 @@ + + + @@ -185,6 +194,7 @@ import { CheckCircleIcon, ExclamationTriangleIcon, PencilSquareIcon, + TrashIcon, XCircleIcon, XMarkIcon } from '@heroicons/vue/24/outline'; @@ -194,6 +204,7 @@ import { useRoute, useRouter } from 'vue-router'; import Toolbar from '../../../../shared/components/ui/Toolbar.vue'; import { useContentSourceStore } from '../../application/store/contentSourceStore'; import { ApiContentSourceRepository } from '../../infrastructure/api/apiContentSourceRepository'; +import ContentSourceDeleteModal from '../components/ContentSourceDeleteModal.vue'; import ContentSourceForm from '../components/ContentSourceForm.vue'; const route = useRoute(); @@ -217,6 +228,9 @@ const showTestResults = ref(false); const showSuccessMessage = ref(false); const testResults = ref({}); const testingConfiguration = ref(false); +const isDeleteModalOpen = ref(false); +const isDeleting = ref(false); +const deleteError = ref(null); const isEditing = computed(() => !!route.params.id); @@ -238,6 +252,7 @@ const toolbarConfig = computed(() => ({ { type: 'label', text: isEditing.value ? 'Modifier la configuration' : 'Nouvelle configuration', class: 'text-sm font-medium' }, ], rightSection: [ + ...(isEditing.value ? [{ type: 'button', icon: TrashIcon, label: 'Supprimer', onClick: () => { isDeleteModalOpen.value = true; }, class: 'text-red-600 hover:text-red-700' }, { type: 'divider' }] : []), { type: 'button', icon: PencilSquareIcon, label: isEditing.value ? 'Mettre à jour' : 'Créer', onClick: () => formRef.value?.submitForm(), disabled: saving.value }, ], })); @@ -328,6 +343,21 @@ const handleImageLoad = (event) => { event.target.style.display = 'block'; }; +const confirmDeleteSource = async () => { + isDeleting.value = true; + deleteError.value = null; + + try { + await contentSourceStore.deleteSource(route.params.id); + isDeleteModalOpen.value = false; + await router.push({ name: 'scrapper-configurations' }); + } catch (error) { + deleteError.value = error.message; + } finally { + isDeleting.value = false; + } +}; + const formatErrorType = (type) => { const typeMap = { 'selector_error': 'Erreur sélecteur', diff --git a/src/Domain/Setting/Application/Command/DeleteContentSourceCommand.php b/src/Domain/Setting/Application/Command/DeleteContentSourceCommand.php new file mode 100644 index 0000000..5483f60 --- /dev/null +++ b/src/Domain/Setting/Application/Command/DeleteContentSourceCommand.php @@ -0,0 +1,11 @@ +contentSourceRepository->findById($command->id); + + if (!$contentSource) { + throw new ContentSourceNotFoundException($command->id); + } + + $this->contentSourceRepository->delete($contentSource); + } +} diff --git a/src/Domain/Setting/Infrastructure/ApiPlatform/Resource/DeleteContentSourceResource.php b/src/Domain/Setting/Infrastructure/ApiPlatform/Resource/DeleteContentSourceResource.php new file mode 100644 index 0000000..ac0094b --- /dev/null +++ b/src/Domain/Setting/Infrastructure/ApiPlatform/Resource/DeleteContentSourceResource.php @@ -0,0 +1,50 @@ + 'Delete a content source', + 'description' => 'Permanently deletes a content source', + 'parameters' => [ + [ + 'name' => 'id', + 'in' => 'path', + 'required' => true, + 'schema' => [ + 'type' => 'integer' + ], + 'description' => 'The content source ID' + ] + ], + 'responses' => [ + '204' => [ + 'description' => 'Content source successfully deleted' + ], + '404' => [ + 'description' => 'Content source not found' + ] + ] + ] + ) + ] +)] +class DeleteContentSourceResource +{ + public function __construct( + public int $id + ) { + } +} diff --git a/src/Domain/Setting/Infrastructure/ApiPlatform/State/Processor/DeleteContentSourceStateProcessor.php b/src/Domain/Setting/Infrastructure/ApiPlatform/State/Processor/DeleteContentSourceStateProcessor.php new file mode 100644 index 0000000..a8acfca --- /dev/null +++ b/src/Domain/Setting/Infrastructure/ApiPlatform/State/Processor/DeleteContentSourceStateProcessor.php @@ -0,0 +1,35 @@ +handler->handle($command); + + return Response::HTTP_NO_CONTENT; + } catch (ContentSourceNotFoundException $e) { + throw new NotFoundHttpException($e->getMessage()); + } + } +} diff --git a/src/Domain/Setting/Infrastructure/ApiPlatform/State/Provider/DeleteContentSourceStateProvider.php b/src/Domain/Setting/Infrastructure/ApiPlatform/State/Provider/DeleteContentSourceStateProvider.php new file mode 100644 index 0000000..ed5cb70 --- /dev/null +++ b/src/Domain/Setting/Infrastructure/ApiPlatform/State/Provider/DeleteContentSourceStateProvider.php @@ -0,0 +1,39 @@ +contentSourceRepository->findById($id); + + if (!$contentSource) { + throw new ContentSourceNotFoundException($id); + } + + return new DeleteContentSourceResource($id); + } catch (ContentSourceNotFoundException $e) { + throw new NotFoundHttpException($e->getMessage()); + } + } +} diff --git a/tests/Feature/Setting/DeleteContentSourceTest.php b/tests/Feature/Setting/DeleteContentSourceTest.php new file mode 100644 index 0000000..ae77b9f --- /dev/null +++ b/tests/Feature/Setting/DeleteContentSourceTest.php @@ -0,0 +1,50 @@ +setBaseUrl('https://mangadex.org') + ->setChapterUrlFormat('https://mangadex.org/chapter/{id}') + ->setScrapingType('html'); + + $this->entityManager->persist($source); + $this->entityManager->flush(); + + $this->sourceId = $source->getId(); + } + + public function testItDeletesSourceSuccessfully(): void + { + static::createClient()->request('DELETE', "/api/content-sources/{$this->sourceId}"); + + $this->assertResponseStatusCodeSame(Response::HTTP_NO_CONTENT); + + $this->entityManager->clear(); + $deletedSource = $this->entityManager->find(ContentSource::class, $this->sourceId); + $this->assertNull($deletedSource); + } + + public function testItReturnsNotFoundWhenSourceDoesNotExist(): void + { + static::createClient()->request('DELETE', '/api/content-sources/999999'); + + $this->assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND); + } +}
+ Êtes-vous sûr de vouloir supprimer la source {{ source?.baseUrl }} ? +
Cette source ne pourra plus être utilisée pour le scraping des chapitres.