From b6f93fd21b2c9e2bf11bc9f002608d100df27a36 Mon Sep 17 00:00:00 2001 From: pfleu <fleutotp@gmail.com> Date: Tue, 23 May 2023 23:20:45 +0200 Subject: [PATCH] =?UTF-8?q?Page=20entr=C3=A9e=20:=20tabs=20pour=20naviguer?= =?UTF-8?q?=20d'un=20lexique=20=C3=A0=20l'autre=20et=20copier/supprimer=20?= =?UTF-8?q?l'entr=C3=A9e=20vers=20un=20autre=20lexique.=20Int=C3=A9gration?= =?UTF-8?q?=20panneau=20de=20gestion=20des=20labels.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/packages/twig.yaml | 1 + public/assets/css/app.css | 39 +++++++++- public/assets/js/app.js | 84 +++++++++++--------- src/Controller/EntryController.php | 97 +++++++++++++----------- src/Controller/HeadwordController.php | 3 +- src/Controller/LexiconController.php | 2 +- src/Controller/WiktionnaryController.php | 35 +++++++++ src/Entity/Lexicon.php | 14 ++++ templates/base.html.twig | 20 +++++ templates/entry/_entryLabels.html.twig | 21 +++++ templates/entry/show.html.twig | 79 +++++++++++++++++++ templates/lexicon/show.html.twig | 8 +- templates/nav.html.twig | 5 ++ 13 files changed, 321 insertions(+), 87 deletions(-) create mode 100644 src/Controller/WiktionnaryController.php create mode 100644 templates/entry/_entryLabels.html.twig create mode 100644 templates/entry/show.html.twig diff --git a/config/packages/twig.yaml b/config/packages/twig.yaml index 32e464a..6932c92 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 a4b78a7..1b3b5a1 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 fd5681f..2f1e070 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 bba3a24..da121c1 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 9f3d85a..a35ed01 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 7f77294..c4ee8e0 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 0000000..78a8563 --- /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 7e12d0d..1dac381 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 fc74482..77af615 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 0000000..05cf4c5 --- /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 0000000..a814231 --- /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 42ff4a7..abbc335 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 06cb0d8..47c362a 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"> -- GitLab