Ajout déplacement blocs
This commit is contained in:
parent
9277154e0c
commit
8d07c38d32
|
|
@ -197,7 +197,8 @@ public class Bloc extends ParamBD {
|
|||
}
|
||||
}
|
||||
|
||||
public static void supprimerBloc(int idBloc, int idU) {
|
||||
public static boolean supprimerBloc(int idBloc, int idU) {
|
||||
boolean reponse = false;
|
||||
try {
|
||||
Connection connexion = DriverManager.getConnection(bdURL, bdLogin, bdPassword);
|
||||
String sql = " DELETE FROM bloc"
|
||||
|
|
@ -209,6 +210,30 @@ public class Bloc extends ParamBD {
|
|||
pst.setInt(2, idU);
|
||||
pst.executeUpdate();
|
||||
|
||||
pst.close();
|
||||
connexion.close();
|
||||
reponse = true;
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return reponse;
|
||||
}
|
||||
|
||||
public static void miseAJourOrdreBloc(int idBloc, int ordre) {
|
||||
try {
|
||||
Connection connexion = DriverManager.getConnection(bdURL, bdLogin, bdPassword);
|
||||
String sql = " UPDATE bloc"
|
||||
+ " SET ordre = ?"
|
||||
+ " WHERE id = ?"
|
||||
+ ";";
|
||||
PreparedStatement pst = connexion.prepareStatement(sql);
|
||||
pst.setInt(1, ordre);
|
||||
pst.setInt(2, idBloc);
|
||||
pst.executeUpdate();
|
||||
|
||||
pst.close();
|
||||
connexion.close();
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,86 @@
|
|||
package projet;
|
||||
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.annotation.WebServlet;
|
||||
import jakarta.servlet.http.HttpServlet;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
|
||||
import javax.json.Json;
|
||||
import javax.json.JsonArray;
|
||||
import javax.json.JsonObject;
|
||||
import javax.json.JsonReader;
|
||||
|
||||
|
||||
@WebServlet("/SauvegarderOrdre")
|
||||
public class SauvegarderOrdre extends HttpServlet {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
|
||||
public SauvegarderOrdre() {
|
||||
super();
|
||||
}
|
||||
|
||||
|
||||
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
|
||||
HttpSession session = request.getSession();
|
||||
Utilisateur u = (Utilisateur) session.getAttribute("utilisateur");
|
||||
|
||||
if(u != null) {
|
||||
response.sendRedirect("AfficherPage");
|
||||
}else {
|
||||
response.sendRedirect("/Projet/");
|
||||
}
|
||||
}
|
||||
|
||||
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
|
||||
response.setContentType("application/json");
|
||||
response.setCharacterEncoding("UTF-8");
|
||||
|
||||
HttpSession session = request.getSession();
|
||||
Utilisateur u = (Utilisateur) session.getAttribute("utilisateur");
|
||||
|
||||
if (u == null) {
|
||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
response.getWriter().write("{\"error\": \"Utilisateur non connecté\"}");
|
||||
return;
|
||||
}
|
||||
|
||||
try (BufferedReader reader = request.getReader()) {
|
||||
StringBuilder json = new StringBuilder();
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
json.append(line);
|
||||
}
|
||||
|
||||
JsonReader jsonReader = Json.createReader(new StringReader(json.toString()));
|
||||
JsonArray jsonArray = jsonReader.readArray();
|
||||
|
||||
for (int i = 0; i < jsonArray.size(); i++) {
|
||||
JsonObject jsonObject = jsonArray.getJsonObject(i);
|
||||
|
||||
if (!jsonObject.containsKey("id") || !jsonObject.containsKey("ordre")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int id = Integer.parseInt(jsonObject.getString("id"));
|
||||
int ordre = jsonObject.getInt("ordre");
|
||||
|
||||
Bloc.miseAJourOrdreBloc(id, ordre);
|
||||
}
|
||||
|
||||
response.setStatus(HttpServletResponse.SC_OK);
|
||||
response.getWriter().write("{\"success\": true}");
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
|
||||
response.getWriter().write("{\"error\": \"Données invalides ou erreur serveur\"}");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -29,22 +29,35 @@ public class SupprimerBloc extends HttpServlet {
|
|||
}
|
||||
|
||||
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
|
||||
response.setContentType("application/json");
|
||||
response.setCharacterEncoding("UTF-8");
|
||||
|
||||
HttpSession session = request.getSession();
|
||||
Utilisateur u = (Utilisateur) session.getAttribute("utilisateur");
|
||||
|
||||
if(u != null) {
|
||||
if (u == null) {
|
||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
response.getWriter().write("{\"error\": \"Non autorisé\"}");
|
||||
return;
|
||||
}
|
||||
|
||||
String idBlocStr = request.getParameter("blocId");
|
||||
|
||||
if (idBlocStr == null || idBlocStr.isEmpty()) {
|
||||
response.sendRedirect("AfficherPage");
|
||||
} else {
|
||||
int idBloc = Integer.parseInt(idBlocStr);
|
||||
Bloc.supprimerBloc(idBloc, u.getId());
|
||||
response.sendRedirect("AfficherPage");
|
||||
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
|
||||
response.getWriter().write("{\"error\": \"ID manquant\"}");
|
||||
return;
|
||||
}
|
||||
|
||||
int idBloc = Integer.parseInt(idBlocStr);
|
||||
boolean success = Bloc.supprimerBloc(idBloc, u.getId());
|
||||
|
||||
if (success) {
|
||||
response.setStatus(HttpServletResponse.SC_OK);
|
||||
response.getWriter().write("{\"success\": true}");
|
||||
} else {
|
||||
response.sendRedirect("/Projet/");
|
||||
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
|
||||
response.getWriter().write("{\"error\": \"Suppression interdite\"}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,37 @@
|
|||
<jsp:param name="titre" value="Accueil" />
|
||||
</jsp:include>
|
||||
|
||||
<div class="section">
|
||||
<div class="container">
|
||||
<div class="columns is-centered">
|
||||
<div class="column is-two-thirds">
|
||||
<div class="box has-background-light">
|
||||
<h1 class="title is-3 icon-text">
|
||||
<span class="icon has-text-info"><i class="fas fa-pencil-alt"></i></span>
|
||||
<span>Bienvenue sur notre plateforme de prise de notes collaborative</span>
|
||||
</h1>
|
||||
<p class="content">
|
||||
Ce site vous permet de créer facilement des pages de contenu interactives en utilisant du <strong>Markdown</strong>, d’organiser vos idées en blocs, et de collaborer en temps réel avec d’autres utilisateurs.
|
||||
</p>
|
||||
<ul>
|
||||
<li><strong>Éditeur intelligent :</strong> avec support des titres, listes, citations, et mise en forme automatique.</li>
|
||||
<li><strong>Partage de pages :</strong> invitez vos collaborateurs avec un accès en lecture ou en édition.</li>
|
||||
<li><strong>Sauvegarde en temps réel :</strong> vos modifications sont automatiquement enregistrées et partagées.</li>
|
||||
</ul>
|
||||
<p class="content mt-5">
|
||||
Une fois votre travail terminé, vous pouvez <strong>exporter vos pages</strong> dans plusieurs formats :
|
||||
</p>
|
||||
<ul>
|
||||
<li>HTML — pour une intégration facile sur le web</li>
|
||||
<li>LaTeX — idéal pour des documents scientifiques ou académiques</li>
|
||||
<li>Markdown — pour garder un format brut et portable</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<c:set var="erreur" value="${erreur}" />
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
<c:when test="${not empty page.titre}">
|
||||
<div class="is-flex is-justify-content-space-between is-align-items mb-4">
|
||||
<h2 class="block">
|
||||
<a href = "AfficherPage"><i class="fa-solid fa-house-chimney"></i></a> >
|
||||
<c:forEach var="entry" items="${hierarchie}">
|
||||
<a href="AfficherPage?id=${entry.key}">${entry.value}</a> >
|
||||
</c:forEach>
|
||||
|
|
@ -87,7 +88,87 @@
|
|||
</div>
|
||||
</c:when>
|
||||
<c:otherwise>
|
||||
<p>Pas encore de page choisie.</p>
|
||||
<a href = "AfficherPage"><i class="fa-solid fa-house-chimney"></i></a>
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<h1 class="title is-3 icon-text">
|
||||
<span>Rappel des Instructions</span>
|
||||
</h1>
|
||||
|
||||
<div class="box">
|
||||
<h2 class="title is-4 icon-text">
|
||||
<span class="icon has-text-info"><i class="fas fa-terminal"></i></span>
|
||||
<span>Commandes Slash</span>
|
||||
</h2>
|
||||
<p>Dans un bloc, tapez une commande commençant par <code>/</code> :</p>
|
||||
<ul>
|
||||
<li><code>/h1</code>, <code>/h2</code>, <code>/h3</code> → transforme le bloc en titre de niveau 1, 2 ou 3</li>
|
||||
<li><code>/ul</code> → transforme en liste à puces</li>
|
||||
<li><code>/ol</code> → transforme en liste numérotée</li>
|
||||
<li><code>/code</code> → transforme en bloc de code (optionnel : nom du langage)</li>
|
||||
<li><code>/hr</code> → insère une ligne de séparation</li>
|
||||
<li><code>/page [titre]</code> → crée une nouvelle page liée avec le titre donné</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<h2 class="title is-4 icon-text">
|
||||
<span class="icon has-text-link"><i class="fas fa-heading"></i></span>
|
||||
<span>Markdown <em>en début de bloc</em> (type de contenu)</span>
|
||||
</h2>
|
||||
<p>Si le bloc commence par un de ces symboles :</p>
|
||||
<ul>
|
||||
<li><code># Titre</code> → Titre de niveau 1</li>
|
||||
<li><code>## Sous-titre</code> → Titre de niveau 2</li>
|
||||
<li><code>### Titre 3</code> → Titre de niveau 3</li>
|
||||
<li><code>``` code </code> → Bloc de code avec mise en forme monospaced</li>
|
||||
<li><code>+ Élément</code> → Liste à puces</li>
|
||||
<li><code>1. Élément</code> → Liste numérotée</li>
|
||||
<li><code>> Citation</code> → Citation classique</li>
|
||||
<li><code>>! Attention</code> → Bloc d'alerte/info</li>
|
||||
<li><code>>i Note</code> → Bloc d'information</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<h2 class="title is-4 icon-text">
|
||||
<span class="icon has-text-warning"><i class="fas fa-pen-fancy"></i></span>
|
||||
<span>Markdown <em>dans un bloc</em> (mise en forme)</span>
|
||||
</h2>
|
||||
<ul>
|
||||
<li><code>`texte`</code> → <code><code></code> : monospaced/code</li>
|
||||
<li><code>**gras**</code> → <strong>gras</strong></li>
|
||||
<li><code>*italique*</code> → <em>italique</em></li>
|
||||
<li><code>$$math$$ </code> → active le rendu mathématique (MathJax)</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<h2 class="title is-4 icon-text">
|
||||
<span class="icon has-text-success"><i class="fas fa-keyboard"></i></span>
|
||||
<span>Raccourcis Clavier</span>
|
||||
</h2>
|
||||
<ul>
|
||||
<li><strong>Entrée</strong> : crée un nouveau bloc (hors liste)</li>
|
||||
<li><strong>Maj + Entrée</strong> : nouvelle ligne dans une liste</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<h2 class="title is-4 icon-text">
|
||||
<span class="icon has-text-danger"><i class="fas fa-tools"></i></span>
|
||||
<span>Fonctionnalités supplémentaires</span>
|
||||
</h2>
|
||||
<ul>
|
||||
<li>Redimensionnement automatique des blocs</li>
|
||||
<li>Rendu MathJax pour les formules</li>
|
||||
<li>Collage propre (suppression du style)</li>
|
||||
<li>Sauvegarde en temps réel via WebSocket</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
</c:otherwise>
|
||||
</c:choose>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -187,8 +187,13 @@ export function addDeleteBloc(button) {
|
|||
body: params
|
||||
}).then(response => {
|
||||
if (response.ok) {
|
||||
blocContainer.remove(); // Supprime visuellement le bloc
|
||||
blocContainer.remove();
|
||||
ajouterBlocVideSiBesoin();
|
||||
|
||||
window.socketBloc.send(JSON.stringify({
|
||||
action: "deleteBloc",
|
||||
blocId,
|
||||
}));
|
||||
} else {
|
||||
console.error("Erreur lors de la suppression du bloc.");
|
||||
}
|
||||
|
|
@ -204,7 +209,7 @@ export function ajouterBlocVideSiBesoin() {
|
|||
|
||||
const message = document.querySelector('.column.is-half p');
|
||||
if (message && message.textContent.trim() === "Pas encore de page choisie.") {
|
||||
return; // Arrête la fonction ici (se passe après avoir supprimé une page)
|
||||
return;
|
||||
}
|
||||
|
||||
if (allBlocs.length === 0 || allBlocs[allBlocs.length - 1].innerText.trim() !== "") {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,62 @@
|
|||
export function initDragAndDrop(containerSelector, itemSelector = '[draggable]') {
|
||||
const container = document.querySelector(containerSelector);
|
||||
let draggedElement = null;
|
||||
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
|
||||
container.addEventListener('dragstart', e => {
|
||||
if (e.target.matches(itemSelector)) {
|
||||
draggedElement = e.target;
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
e.target.classList.add('dragging');
|
||||
}
|
||||
});
|
||||
|
||||
container.addEventListener('dragover', e => {
|
||||
e.preventDefault();
|
||||
const target = e.target.closest(itemSelector);
|
||||
if (target && draggedElement && target !== draggedElement) {
|
||||
container.insertBefore(draggedElement, target);
|
||||
}
|
||||
});
|
||||
|
||||
container.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
container.addEventListener('dragend', () => {
|
||||
if (draggedElement) {
|
||||
draggedElement.classList.remove('dragging');
|
||||
draggedElement = null;
|
||||
}
|
||||
|
||||
const blocs = Array.from(container.querySelectorAll(itemSelector));
|
||||
const ordre = blocs.map((bloc, index) => {
|
||||
const editor = bloc.querySelector('.editor');
|
||||
return {
|
||||
id: editor.dataset.id,
|
||||
ordre: index + 1
|
||||
};
|
||||
});
|
||||
|
||||
fetch('SauvegarderOrdre', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(ordre)
|
||||
}).then(response => {
|
||||
if (response.ok) {
|
||||
const modifOrdre = {
|
||||
action: "modifierOrdreBloc",
|
||||
order: ordre.map(b => b.id)
|
||||
};
|
||||
window.socketBloc.send(JSON.stringify(modifOrdre));
|
||||
} else {
|
||||
console.error("Erreur lors de la modification de l'ordre des blocs.");
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -71,6 +71,7 @@ export function renderBlocStyle(bloc) {
|
|||
break;
|
||||
|
||||
case 'PAGE':
|
||||
renderPage(bloc);
|
||||
bloc.classList.add('is-page-link');
|
||||
break;
|
||||
|
||||
|
|
@ -112,6 +113,18 @@ export function renderListe(bloc) {
|
|||
bloc.innerHTML = `<${tag}>${items}</${tag}>`;
|
||||
}
|
||||
|
||||
export function renderPage(bloc){
|
||||
const metadata = getBlocDataMetadata(bloc);
|
||||
const pageId = metadata.pageId;
|
||||
|
||||
if (!pageId) return;
|
||||
|
||||
const lien = `AfficherPage?id=${pageId}`;
|
||||
const titre = metadata.title;
|
||||
|
||||
bloc.innerHTML = `<a href="${lien}">${titre}</a>`;
|
||||
}
|
||||
|
||||
export function handleSlashCommand(bloc, texte) {
|
||||
return new Promise((resolve) => {
|
||||
const parts = texte.trim().substring(1).split(" ");
|
||||
|
|
@ -172,7 +185,7 @@ export function handleSlashCommand(bloc, texte) {
|
|||
applyBlocType(bloc, "SEPARATEUR", {}); // ou un type adapté selon ta logique
|
||||
bloc.innerText = ' ';
|
||||
autoResize(bloc);
|
||||
resolve(true);
|
||||
resolve();
|
||||
break;
|
||||
|
||||
default:
|
||||
|
|
@ -189,7 +202,7 @@ export function handleSlashCommand(bloc, texte) {
|
|||
export async function handleMarkdownSyntax(bloc, texte) {
|
||||
// Vérifier si c'est un titre
|
||||
if (texte.startsWith('#')) {
|
||||
const level = texte.split(' ')[0].length; // Compter le nombre de # pour le niveau du titre
|
||||
const level = texte.split(' ')[0].length;
|
||||
if (level >= 1 && level <= 6) {
|
||||
applyBlocType(bloc, "TITRE", { level: level });
|
||||
bloc.textContent = texte.replace(/^#+\s*/, '');
|
||||
|
|
@ -253,9 +266,7 @@ export function setBlocMetadata(bloc, metadata) {
|
|||
const citationTypes = {
|
||||
'>!': 'danger',
|
||||
'>i': 'info',
|
||||
/* '>w': 'warning',
|
||||
'>s': 'success',*/
|
||||
'>': 'normal' // doit rester en dernier
|
||||
'>': 'normal'
|
||||
};
|
||||
|
||||
function detectCitation(texte) {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { initBlocs, ajouterBlocVideSiBesoin, focusDernierBloc } from './bloc.js'
|
|||
import { initTchat } from './tchat.js';
|
||||
import { initPages } from './page.js';
|
||||
import { initSocketBloc } from './socket-bloc.js';
|
||||
import { initDragAndDrop } from './drag-and-drop.js';
|
||||
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
initTchat();
|
||||
|
|
@ -10,6 +11,7 @@ window.addEventListener('DOMContentLoaded', () => {
|
|||
initBlocs();
|
||||
ajouterBlocVideSiBesoin();
|
||||
focusDernierBloc();
|
||||
initDragAndDrop('#md', '.bloc-container');
|
||||
|
||||
document.body.addEventListener('click', function(event) {
|
||||
// Vérifie si l'élément cliqué est un lien dans un .editor
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { autoResize, creerBlocDOM } from './utils.js'
|
||||
import { renderBlocStyle, renderListe } from './functionsBloc.js'
|
||||
|
||||
export function initSocketBloc() {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
|
|
@ -21,7 +20,7 @@ export function initSocketBloc() {
|
|||
const data = JSON.parse(event.data);
|
||||
|
||||
if (data.action === "update") {
|
||||
const blocElement = document.querySelector(`.editor[data-id='${data.blocId}']`);
|
||||
const blocElement = document.querySelector(`[data-id="${data.blocId}"`);
|
||||
if (blocElement) {
|
||||
blocElement.innerHTML = data.content;
|
||||
blocElement.setAttribute('data-metadata', data.metadata);
|
||||
|
|
@ -46,7 +45,7 @@ export function initSocketBloc() {
|
|||
ordre: data.ordre
|
||||
});
|
||||
} else if (data.action === "actualiserMiseEnPageBloc") {
|
||||
const blocElement = document.querySelector(`.editor[data-id='${data.blocId}']`);
|
||||
const blocElement = document.querySelector(`[data-id="${data.blocId}"`);
|
||||
if (blocElement) {
|
||||
blocElement.dataset.type = data.type;
|
||||
blocElement.innerHTML = data.content;
|
||||
|
|
@ -54,6 +53,28 @@ export function initSocketBloc() {
|
|||
blocElement.className = data.classBloc;
|
||||
renderBloc(blocElement, blocElement.dataset.metadata);
|
||||
}
|
||||
} else if (data.action === "modifierOrdreBloc") {
|
||||
const container = document.getElementById("md");
|
||||
const order = data.order;
|
||||
order.forEach(blocId => {
|
||||
|
||||
const blocElement = document.querySelector(`[data-id="${blocId}"]`);
|
||||
if (blocElement) {
|
||||
const blocContainer = blocElement.closest('.bloc-container');
|
||||
if (blocContainer) {
|
||||
container.appendChild(blocContainer);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else if (data.action === "deleteBloc") {
|
||||
const blocId = data.blocId;
|
||||
const blocElement = document.querySelector(`[data-id="${blocId}"]`);
|
||||
if (blocElement) {
|
||||
const blocContainer = blocElement.closest('.bloc-container');
|
||||
if (blocContainer) {
|
||||
blocContainer.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ export function placeCursorAtEnd(element) {
|
|||
export function creerBlocDOM({ blocId, content = "", type = "TEXTE", metadata = "{}", ordre = null }) {
|
||||
const container = document.createElement('div');
|
||||
container.classList.add('field', 'is-grouped', 'is-align-items-flex-start', 'bloc-container');
|
||||
container.setAttribute('draggable', 'true');
|
||||
|
||||
const control = document.createElement('div');
|
||||
control.classList.add('control', 'is-expanded');
|
||||
|
|
|
|||
|
|
@ -89,3 +89,7 @@
|
|||
background-color: #eaf6fb;
|
||||
color: #2980b9;
|
||||
}
|
||||
|
||||
.dragging {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue