diff --git a/config/packages/twig.yaml b/config/packages/twig.yaml index 32e464a35dd0020ec8822edacf61bacb26b9e898..6932c929a11718933320eba5bfad10f17d005dae 100644 --- a/config/packages/twig.yaml +++ b/config/packages/twig.yaml @@ -4,6 +4,7 @@ twig: globals: languages: '@App\Manager\LanguagesManager' label_manager: '@App\Manager\LabelManager' + lexicon_manager: '@App\Manager\LexiconManager' when@test: twig: diff --git a/public/assets/css/app.css b/public/assets/css/app.css index a4b78a7f3adfbf599fbb46638d839175410063cc..1b3b5a16377f7d5be9fbe627b4483ada41a72622 100644 --- a/public/assets/css/app.css +++ b/public/assets/css/app.css @@ -155,7 +155,11 @@ table.label-table > tbody > tr > td { .bg-definition { background-color: #D6EADA; } -.badge.badge-milestone {line-height: 1.3} +.badge.badge-milestone { + line-height: 1.3; + padding-top: 0; + padding-bottom: 0; +} .card-body.card-definition { margin-top: 2px; @@ -171,4 +175,37 @@ table.label-table > tbody > tr > td { .faded { opacity: 0.5; +} +.ajax-link { + cursor: pointer; +} + +.nav-tabs .nav-link.tab-pink { + background-color: #FDF6F6; +} +.nav-tabs .nav-link.tab-dark-pink { + background-color: #FFE4E4; +} +.nav-tabs .nav-link.tab-grey { + background-color: #E4E4E4; + color: grey; +} +.nav-tabs .nav-link { + color: black; + border-bottom: 0; +} +.nav-tabs a.nav-link.active { + font-weight: bold; +} + +#tabContent { + background-color: #FDF6F6; + padding: 15px; +} +#tabContent.tab-wiktionnary { + background-color: #FFE4E4; +} + +#tableLabels.table > tbody > tr > td { + height: 52px; } \ No newline at end of file diff --git a/public/assets/js/app.js b/public/assets/js/app.js index fd5681fbfae0f61ec7daea9f5891cc4f7a385e15..2f1e070e38ef7a76f85804234ce5a80909f79fea 100644 --- a/public/assets/js/app.js +++ b/public/assets/js/app.js @@ -326,45 +326,61 @@ function initializeFilterFormFieldsBackground() { function initializeAjaxLinks() { $('body').on('click', '.ajax-link', function () { - var data; var $overlay = $('#overlay').show(); - var $target = $(this).closest('.blink-target').length ? $(this).closest('.blink-target') : $(this); - $target.addClass('faded'); + var method = $(this).data('method'); - // on récupère le json - if ($(this).data('json')) { - data = JSON.stringify($(this).data('json')); - } else { - data = $('#'+$(this).data('json-dynamic')).val(); - } + if (!method || method.toUpperCase() === 'GET') { - $.ajax({ - type: $(this).data('method'), - url: $(this).data('url'), - contentType: "application/json", - dataType: "json", - data: data, - processData: false, - success: function(response, textStatus, xhr) { -// console.log(response); - location.reload(); - }, - complete: function () { - $overlay.hide(); - // $target = $target.removeClass('faded'); - }, - error: function (jqXHR, textStatus, errorThrown) { - // console.log(jqXHR.responseText, textStatus, errorThrown); - $target = $target.removeClass('faded'); - var message = JSON.parse(jqXHR.responseText); - if ($('#bootstrap-modal').is(':visible')) { - $('#bootstrap-modal').find('.ajax-message').html('<div class="alert alert-danger">' + message.error + '</div>') - } else { - $('.ajax-message').html('<div class="alert alert-danger">' + message.error + '</div>') + $.ajax({ + type: 'GET', + url: $(this).data('url'), + dataType: "json", + success: function (response, textStatus, xhr) { + location.reload(); + }, + error: function (jqXHR, textStatus, errorThrown) { + console.log(jqXHR.responseText, textStatus, errorThrown); + $overlay.hide(); } - $overlay.hide(); + }) + + } else { + + var data; + var $target = $(this).closest('.blink-target').length ? $(this).closest('.blink-target') : $(this); + $target.addClass('faded'); + + // on récupère le json + if ($(this).data('json')) { + data = JSON.stringify($(this).data('json')); + } else { + data = $('#' + $(this).data('json-dynamic')).val(); } - }) + + $.ajax({ + type: method, + url: $(this).data('url'), + contentType: "application/json", + dataType: "json", + data: data, + processData: false, + success: function (response, textStatus, xhr) { + // console.log(response); + location.reload(); + }, + error: function (jqXHR, textStatus, errorThrown) { + // console.log(jqXHR.responseText, textStatus, errorThrown); + $target = $target.removeClass('faded'); + var message = JSON.parse(jqXHR.responseText); + if ($('#bootstrap-modal').is(':visible')) { + $('#bootstrap-modal').find('.ajax-message').html('<div class="alert alert-danger">' + message.error + '</div>') + } else { + $('.ajax-message').html('<div class="alert alert-danger">' + message.error + '</div>') + } + $overlay.hide(); + } + }) + } }) } \ No newline at end of file diff --git a/src/Controller/EntryController.php b/src/Controller/EntryController.php index bba3a24cd70b3a70e6e508d974f6618fb82b73a9..da121c16befca3e443cf1e7228a48b84bab7f937 100644 --- a/src/Controller/EntryController.php +++ b/src/Controller/EntryController.php @@ -4,6 +4,7 @@ namespace App\Controller; use App\Entity\Entry; use App\Entity\Label; +use App\Entity\Lexicon; use App\Entity\Log; use App\Form\SearchStringType; use App\Manager\LabelManager; @@ -22,55 +23,15 @@ use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; class EntryController extends AppBaseController { /** - * @Route("/{id}", name="app_entry_show", methods={"GET"}) + * @Route("/{id}/show", name="app_entry_show", requirements={"id" = "\d+"}) */ - public function show(ManagerRegistry $doctrine, Request $request, Entry $entry): Response + public function show(LabelManager $labelManager, Request $request, Entry $entry) { - $em = $doctrine->getManager(); - $sortingColumn = null; - $sortingOrder = null; - $this->getSortingParameters($request, $sortingColumn, $sortingOrder, 'entry_show'); - - $form = $this->createForm(SearchStringType::class,null, array('method' => 'GET')); - $form->handleRequest($request); - $filter = $form->getData(); - - $filter['entry'] = $entry; - - // Entrées triées par createdAt - // On ajoute l'ordre d'ajout de l'entrée (on se base sur createdAt) comme index - $entriesOrdered = $em->getRepository(Entry::class)->filter($filter, 'createdAt', 'ASC'); - foreach ($entriesOrdered as $key => $entryOrdered) { - $entryOrdered->setAddingOrder($key); - } - // On ajoute l'id de l'entrée en index ( => tableau assciatif 'id' => $entry (avec colonne addingOrder) - $entriesOrderedIndexedById = []; - foreach ($entriesOrdered as $entryOrdered) { - $entriesOrderedIndexedById[$entryOrdered->getId()] = $entryOrdered; - } - - // Entrées résultat cherché (triées avec le filtre sortingColumn) - // On ajoute l'id de l'entrée en index ( => tableau assciatif 'id' => $entry (avec colonne addingOrder) - $entries = $em->getRepository(Entry::class)->filter($filter, $sortingColumn, $sortingOrder); - $entriesIndexedById = []; - foreach ($entries as $entry) { - $entriesIndexedById[$entry->getId()] = $entry; - } - // On ajoute l'ordre d'ajout de l'entrée à partir du premier tableau - foreach ($entriesIndexedById as $id => $entryIndexedById) { - $entryIndexedById->setAddingOrder($entriesOrderedIndexedById[$id]->getAddingOrder()); - } - -// $entriesWithAddingOrderIndex = $entriesOrdered; -// usort($entriesWithAddingOrderIndex, function ($a, $b) { return $b->getCreatedAt()->getTimestamp() - $a->getCreatedAt()->getTimestamp(); }); - return $this->render('entry/show.html.twig', [ - 'entries' => $entriesIndexedById, - 'entry' => $entry, - 'form' => $form->createView(), - 'sortingColumn' => $sortingColumn, - 'sortingOrder' => $sortingOrder, - ]); + return $this->render('entry/show.html.twig', array( + 'entry' => $entry, + 'wiktionnaryLexicon' => false, + )); } /** @@ -88,6 +49,50 @@ class EntryController extends AppBaseController )); } + /** + * @Route("/{id}/copy/{lexiconId}", name="app_entry_copy", requirements={"id" = "\d+"}) + * @ParamConverter("entry", options={"id" = "id"}) + * @ParamConverter("lexicon", options={"id" = "lexiconId"}) + */ + public function copyEntry(Request $request, Entry $entry, Lexicon $lexicon) + { + $forwardRequest = Request::create( + $this->generateUrl('api_copy_entries'), 'POST', [], [], [], [], + json_encode([ + 'entries' => [$entry->getId()], + 'merge' => Lexicon::MERGE_OVERWRITE, + 'target_lex' => [$lexicon->getId()], + ]) + ); + + $response = $this->forward('App\Controller\ApiEntryController::copyEntries', array( + 'request' => $forwardRequest, +// '_route' => $request->attributes->get('_route'), +// '_route_params' => $request->attributes->get('_route_params'), + )); + + if ($response->getStatusCode() == 200) { + $this->addFlash('success', "Copie effectuée"); + } else { + $message = (array) json_decode($response->getContent()); + $this->addFlash('danger', reset($message) ?? 'Requête terminée'); + } + + return $this->redirectToRoute('app_entry_show', ['id' => $entry->getId()]); + } + + /** + * @Route("/{id}/delete", name="app_entry_delete", requirements={"id" = "\d+"}) + */ + public function deleteEntry(Request $request, Entry $entry) + { + $this->em->remove($entry); + $this->em->flush(); + $this->addFlash('success', sprintf("L'entrée a été supprimée du lexique %s", $entry->getLexicon())); + + return $this->redirect($request->get('backUrl') ? : $this->generateUrl('app_lexicon_show', ['id' => $entry->getLexicon()->getId()])); + } + /** * PAS UTILISÉ * diff --git a/src/Controller/HeadwordController.php b/src/Controller/HeadwordController.php index 9f3d85a859392184eaa0cf5f07ff29c95f42a0bd..a35ed0120e0cac855b6f92cd3864f9c1f447c0ae 100644 --- a/src/Controller/HeadwordController.php +++ b/src/Controller/HeadwordController.php @@ -15,6 +15,7 @@ use App\Repository\HeadwordRepository; use Doctrine\Persistence\ManagerRegistry; use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; @@ -63,7 +64,7 @@ class HeadwordController extends AppBaseController $this->em->flush(); - return $this->redirectToRoute('app_lexicon_show', ['id' => $request->get('lexiconId')]); + return new JsonResponse(); } } diff --git a/src/Controller/LexiconController.php b/src/Controller/LexiconController.php index 7f7729456ea22b73a608f4e8b4f659eefa0b4c49..c4ee8e05d294aef4ca59a916486dee594407a365 100644 --- a/src/Controller/LexiconController.php +++ b/src/Controller/LexiconController.php @@ -133,7 +133,7 @@ class LexiconController extends AppBaseController /** * @Route("/{id}/copy-entries", name="app_lexicon_copy_entries") */ - public function copySelection(LabelManager $labelManager, Request $request, Lexicon $lexicon, EntryRepository $entryRepository) + public function copySelection(Request $request, Lexicon $lexicon) { $formData = $request->get('form'); $selectedIds = $formData['selected_entries'] ?? null; diff --git a/src/Controller/WiktionnaryController.php b/src/Controller/WiktionnaryController.php new file mode 100644 index 0000000000000000000000000000000000000000..78a856308399fb65f8132f5ef46cd82434800c35 --- /dev/null +++ b/src/Controller/WiktionnaryController.php @@ -0,0 +1,35 @@ +<?php + +namespace App\Controller; + +use App\Entity\Entry; +use App\Entity\Label; +use App\Entity\Log; +use App\Form\SearchStringType; +use App\Manager\LabelManager; +use App\Repository\EntryRepository; +use Doctrine\Persistence\ManagerRegistry; +use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Annotation\Route; +use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; + +/** + * @Route("/wiktionnary") + */ +class WiktionnaryController extends AppBaseController +{ + /** + * @Route("/{id}/show", name="app_wiktionnary_show", requirements={"id" = "\d+"}) + */ + public function show(LabelManager $labelManager, Request $request, Entry $entry) + { + + return $this->render('entry/show.html.twig', array( + 'entry' => $entry, + )); + } + +} diff --git a/src/Entity/Lexicon.php b/src/Entity/Lexicon.php index 7e12d0d8220ed61cd8110b41f994deb554f540a8..1dac381f74155db9b8d9553e3f243d778060b311 100644 --- a/src/Entity/Lexicon.php +++ b/src/Entity/Lexicon.php @@ -201,6 +201,20 @@ class Lexicon } } + /** + * @param Headword $headword + * @return Entry|mixed|null + */ + public function getEntryForHeadword(Headword $headword) + { + foreach ($this->getEntries() as $entry) { + if ($entry->getHeadword() === $headword) { + return $entry; + } + } + return null; + } + public function getId(): ?int { return $this->id; diff --git a/templates/base.html.twig b/templates/base.html.twig index fc74482a2f6bfd3ea8e6c88bcb97a96a022b0999..77af615f6c84312ecd0afa16ab3624e68afd9634 100644 --- a/templates/base.html.twig +++ b/templates/base.html.twig @@ -93,6 +93,26 @@ </div> </div> + {# CONFIRM MODAL #} + <div id="confirm-dialog" class="modal fade " tabindex="-1" role="dialog" aria-hidden="true" data-keyboard="false" + data-backdrop="static" data-show-on-load="{{ showConfirmDialog|default(false) }}"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <h4 class="modal-title" id="bootstrap-modal-title">{{ 'Confirmation'|trans }}</h4> + <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> + </div> + <div class="modal-body"> + <div id="confirm-dialog-message" class="pb-5">{{ confirmMessage|default('')|raw }}</div> + <div class="text-center"> + <a class="btn btn-danger btn-ok" href="{{ confirmUrl|default('#') }}">{{ 'Confirmer'|trans }}</a> + <button type="button" class="btn btn-light" data-bs-dismiss="modal" aria-label="Close">{{ "Annuler"|trans }}</button> + </div> + </div> + </div> + </div> + </div> + </div> </body> </html> diff --git a/templates/entry/_entryLabels.html.twig b/templates/entry/_entryLabels.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..05cf4c54bb4810c0d91d0a91ab5234abea01f514 --- /dev/null +++ b/templates/entry/_entryLabels.html.twig @@ -0,0 +1,21 @@ + +<table id="tableLabels" class="table table-bordered"> + <tbody> + <tr> + <td class="col-md-2 modal-form" data-url="{{ path('app_entry_choose_label', {id: entry.id, category: constant("App\\Entity\\Label::LABEL_CATEGORY_GENERAL")}) }}"> + {% for label in entry.headword.generalLabels %}<span class="blink-target">{% include "label/_labelBadge.html.twig" %}</span>{% endfor %} + </td> + </tr> + <tr> + <td class="col-md-2 modal-form" data-url="{{ path('app_entry_choose_label', {id: entry.id, category: constant("App\\Entity\\Label::LABEL_CATEGORY_INSTITUTIONAL")}) }}"> + {% for label in entry.headword.institutionalLabels %}<span class="blink-target">{% include "label/_labelBadge.html.twig" %}</span>{% endfor %} + </td> + </tr> + <tr> + <td class="col-md-2 modal-form" data-url="{{ path('app_entry_choose_label', {id: entry.id, category: constant("App\\Entity\\Label::LABEL_CATEGORY_MILESTONE")}) }}"> + {% for label in entry.headword.milestoneLabels %}<span class="blink-target">{% include "label/_labelBadge.html.twig" %}</span>{% endfor %} + </td> + </tr> + </tbody> +</table> + diff --git a/templates/entry/show.html.twig b/templates/entry/show.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..a81423148cf1ef99684a3f86c73e4ff0ce703c58 --- /dev/null +++ b/templates/entry/show.html.twig @@ -0,0 +1,79 @@ +{% extends 'base.html.twig' %} +{% import 'macros.html.twig' as macros %} +{% block container %}container-fluid{% endblock %} + +{% block title %}{{ entry|capitalize }}{% endblock %} + +{% block body %} + + <div class="row justify-content-center m-lg-5 m-sm-3"> + <div class="col-md-12"> + + <h1 class=""> + {% if not wiktionnaryLexicon %} + <a href="{{ path('app_lexicon_show', {id: entry.lexicon.id}) }}" class="btn btn-dark"><i class="bi bi-arrow-90deg-left"></i></a> + {% endif %} + {{ entry|capitalize }} + + {% set known = entry.headword.knownByUser(app.user) %} + <a title="{{ known ? 'Mot-vedette connu. Cliquer pour modifier'|trans : 'Mot-vedette non connu. Cliquer pour modifier'|trans }}" href="#" + class="ajax-link" data-method="GET" data-url="{{ path('app_headword_toggle_known', {id: entry.headword.id, userId: app.user.id, lexiconId: entry.lexicon.id}) }}"> + {% if known %}<i class="fa fa-circle text-success"></i>{% else %}<i class="fa fa-circle text-warning"></i>{% endif %} + </a> + </h1> + + <ul class="nav nav-tabs mt-3"> + {% for lexicon in app.user.myLexicons %} + {% set entryWithSameHeadwordInThisLexicon = lexicon.getEntryForHeadword(entry.headword) %} + + <li class="nav-item dropdown"> + <a class="nav-link {{ lexicon == entry.lexicon ? 'active' }} {{ entryWithSameHeadwordInThisLexicon ? 'tab-pink' : 'tab-grey' }} dropdown-toggle" data-bs-toggle="dropdown" href="#" role="button" aria-expanded="false">{{ lexicon }}</a> + <ul class="dropdown-menu"> + {% if entryWithSameHeadwordInThisLexicon %} + {% if lexicon != entry.lexicon %} + <li><a class="dropdown-item" href="{{ path('app_entry_show', {id: entryWithSameHeadwordInThisLexicon.id}) }}">{{ "Voir l'entrée dans ce lexique"|trans }}</a></li> + {% endif %} + {% set backUrl = (lexicon == entry.lexicon ? path('app_lexicon_show', {id: entry.lexicon.id}) : path('app_entry_show', {id: entry.id})) %} + <li><a class="dropdown-item" href="#" + data-href="{{ path('app_entry_delete', {id: entryWithSameHeadwordInThisLexicon.id, backUrl: backUrl}) }}" + data-confirm="{{ "Confirmer la suppression ?"|trans }}" data-bs-toggle="modal" data-bs-target="#confirm-dialog"> + {{ "Supprimer l'entrée dans ce lexique"|trans }} + </a></li> + {% endif %} + {% if lexicon != entry.lexicon %} + {% if entryWithSameHeadwordInThisLexicon %} + <li><a class="dropdown-item" href="#" data-href="{{ path('app_entry_copy', {id: entry.id, lexiconId: lexicon.id}) }}" + data-confirm="{{ "Confirmer la copie ? Une entrée similaire existe déjà dans le lexique que vous avez sélectionné. Si vous copiez celle-ci, le contenu de l'autre sera perdue"|trans }}" data-bs-toggle="modal" data-bs-target="#confirm-dialog"> + {{ "Copier l'entrée dans ce lexique"|trans }} + </a></li> + {% else %} + <li><a class="dropdown-item" href="{{ path('app_entry_copy', {id: entry.id, lexiconId: lexicon.id}) }}">{{ "Copier l'entrée dans ce lexique"|trans }}</a></li> + {% endif %} + {% endif %} + </ul> + </li> + + {% endfor %} + + {% if not entry.lexicon.isNewWords %} + <li class="nav-item"> + <a class="nav-link tab-dark-pink" href="#">{{ "Wiktionnaire"|trans }}</a> + </li> + {% endif %} + </ul> + + <div id="tabContent" class="{{ wiktionnaryLexicon ? 'tab-wiktionnary' }}"> + + <div class="row"> + <div class="col-sm-6"> + {% include "entry/_entryLabels.html.twig" %} + + </div> + </div> + + </div> + + </div> + </div> + +{% endblock %} diff --git a/templates/lexicon/show.html.twig b/templates/lexicon/show.html.twig index 42ff4a7131eacbe5cae73644581d3851d476c76c..abbc3351b48cb3cdc7a30d3f02a35da5a7bf163b 100644 --- a/templates/lexicon/show.html.twig +++ b/templates/lexicon/show.html.twig @@ -66,14 +66,14 @@ <tr class="{{ known ? 'headword-known' }}"> <td>{{entry.addingOrder }}</td> <td class="d-flex justify-content-between"> - <label> + <a href="{{ path('app_entry_show', {id: entry.id}) }}"> {# "value" est utilisé par Symfo quand on soumet le form. "data-headword-id" est utilisé par la vue chooseLabel pour injecter du json dans le sliens ajax #} <input class="me-2" type="checkbox" name="form[selected_entries][]" data-headword-id="{{ entry.headword.id }}" value="{{ entry.id }}" id="form_selected_entries_{{ entry.id }}"/> {{ entry }} - </label> - <a title="{{ known ? 'Mot-vedette connu. Cliquer pour modifier'|trans : 'Mot-vedette connu. Cliquer pour modifier'|trans }}" - href="{{ path('app_headword_toggle_known', {id: entry.headword.id, userId: app.user.id, lexiconId: lexicon.id}) }}"> + </a> + <a title="{{ known ? 'Mot-vedette connu. Cliquer pour modifier'|trans : 'Mot-vedette non connu. Cliquer pour modifier'|trans }}" + href="#" class="ajax-link" data-method="GET" data-url="{{ path('app_headword_toggle_known', {id: entry.headword.id, userId: app.user.id, lexiconId: lexicon.id}) }}"> {% if known %}<i class="fa fa-circle text-success"></i>{% else %}<i class="fa fa-circle text-warning"></i>{% endif %} </a> </td> diff --git a/templates/nav.html.twig b/templates/nav.html.twig index 06cb0d84e1e6c9b094b13b8bb97e9d521cc81b23..47c362ad6aafa6cf721342355926618043ee53d5 100644 --- a/templates/nav.html.twig +++ b/templates/nav.html.twig @@ -6,6 +6,11 @@ </button> <div class="collapse navbar-collapse" id="navbarSupportedContent"> + <form class="d-flex" role="search"> + <input class="form-control me-2" type="search" placeholder="{{ "Rechercher"|trans }}" aria-label="Search"> + <button class="btn btn-light me-3" type="submit"><i class="fa fa-search"></i> </button> + </form> + {% if app.user %} <ul class="navbar-nav me-auto mb-2 mb-lg-0">