Compare commits

...

2 Commits

Author SHA1 Message Date
a622b3c256 Implement vocab delete in Stimulus
TODO: show modal warning before deleting
2024-11-09 11:19:18 +01:00
dce0e1b693 Copy collection; Stimulus controller for vocabs (incomplete) 2024-11-08 17:32:45 +01:00
10 changed files with 270 additions and 140 deletions

View File

@ -0,0 +1,111 @@
import { Controller } from '@hotwired/stimulus';
export default class extends Controller {
static targets = [
'row',
'term',
'input',
'save',
'edit',
'cancel',
];
static values = {
saveUrl: String,
};
/**
* @todo Simpler way?
*/
edit(event) {
const id = event.currentTarget.getAttribute('data-id');
this.saveTargets.forEach((btn, index) => {
if (btn.getAttribute('data-id') === id) {
btn.classList.toggle('is-hidden');
this.inputTargets[index].disabled = false;
this.cancelTargets[index].classList.toggle('is-hidden');
}
});
event.currentTarget.classList.add('is-hidden');
}
async save(event) {
const id = event.currentTarget.getAttribute('data-id');
let term = '';
for (let input of this.inputTargets) {
if (input.getAttribute('data-id') === id) {
term = input.value;
}
}
let data = new FormData;
data.append("_id", id);
data.append("_new_term", term);
const url = event.currentTarget.getAttribute('data-url');
const res = await fetch(url, {
method: "POST",
body: data,
});
if (res.status === 200) {
let notif = document.getElementById('ajax-saved');
notif.classList.toggle('is-hidden');
};
this.cancelTargets.find(btn => btn.getAttribute('data-id') === id)
.classList.toggle('is-hidden');
this.editTargets.find(btn => btn.getAttribute('data-id') === id)
.classList.toggle('is-hidden');
const input = this.inputTargets.find(input => input.getAttribute('data-id') === id)
input.disabled = true;
event.target.classList.toggle('is-hidden');
}
/**
*
* @todo Show modal before deleting! Show delete error (500)!
*/
async delete(event) {
const id = event.currentTarget.getAttribute('data-id');
const url = event.currentTarget.getAttribute('data-url');
let data = new FormData;
data.append("_id", id);
const res = await fetch(url, {
method: "POST",
body: data,
});
if (res.status === 200) {
let notif = document.getElementById('ajax-deleted');
notif.classList.toggle('is-hidden');
this.rowTargets.find(row => row.getAttribute('data-id') === id)
.classList.add('is-hidden');
};
if (res.status === 500) {
let notif = document.getElementById('ajax-error');
notif.classList.toggle('is-hidden');
}
}
cancel(event) {
const id = event.currentTarget.getAttribute('data-id');
this.editTargets.forEach((btn, index) => {
if (btn.getAttribute('data-id') === id) {
btn.classList.toggle('is-hidden');
this.inputTargets[index].disabled = true;
this.saveTargets[index].classList.toggle('is-hidden');
this.cancelTargets[index].classList.toggle('is-hidden');
}
});
}
}

View File

@ -12,6 +12,7 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Core\Exception\AccessDeniedException; use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use DateTimeImmutable;
class BibliographyController extends AbstractController class BibliographyController extends AbstractController
{ {
@ -92,7 +93,7 @@ class BibliographyController extends AbstractController
} }
/** /**
* @todo Permissions! Return JSON with 403 when AJAX * @todo Move clone logic to __clone() in Entity or Repository
*/ */
#[Route('/bibliography/copy/{id<\d+>}', name: 'app_bibliography_copy')] #[Route('/bibliography/copy/{id<\d+>}', name: 'app_bibliography_copy')]
public function copy(Bibliography $bibliography, EntityManagerInterface $em): Response public function copy(Bibliography $bibliography, EntityManagerInterface $em): Response
@ -108,24 +109,26 @@ class BibliographyController extends AbstractController
$user = $this->getUser(); $user = $this->getUser();
$editor = "{$user->getFirstname()} {$user->getLastName()}"; $editor = "{$user->getFirstname()} {$user->getLastName()}";
// TODO Move clone logic to __clone() in Entity or Repository
$copy = clone $bibliography; $copy = clone $bibliography;
$copy->setEditor($editor); $copy->setEditor($editor);
$copy->setOwner($editor); $copy->setOwner($editor);
$copy->setCreator($user->getUsername()); $copy->setCreator($user->getUsername());
$repo = $em->getRepository(Collection::class); $repo = $em->getRepository(Collection::class);
$copy->setCollections( $copy->setCollections(
$repo->findAllByBibliography($bibliography->getId()) $repo->findAllByBibliography($bibliography->getId())
); );
$repo = $em->getRepository(Collector::class); $repo = $em->getRepository(Collector::class);
$copy->setCollectors( $copy->setCollectors(
$repo->findAllByBibliography($bibliography->getId()) $repo->findAllByBibliography($bibliography->getId())
); );
$copy->setCitation("{$bibliography->getCitation()} - Copy"); $copy->setCitation("{$bibliography->getCitation()} - Copy");
$copy->setModifiedAt(new DateTimeImmutable());
$em->persist($copy); $em->persist($copy);
$em->flush(); $em->flush();
$this->addFlash('notice', 'Record copied successfully');
return $this->redirectToRoute('app_bibliography', ['id' => $copy->getId()]); return $this->redirectToRoute('app_bibliography', ['id' => $copy->getId()]);
} }
} }

View File

@ -4,6 +4,7 @@ namespace App\Controller;
use App\Entity\Collection; use App\Entity\Collection;
use App\Entity\Bibliography; use App\Entity\Bibliography;
use App\Entity\Collector;
use App\Security\Voter\RecordVoter; use App\Security\Voter\RecordVoter;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
@ -61,4 +62,45 @@ class CollectionController extends AbstractController
return $this->redirectToRoute('app_collection_landing'); return $this->redirectToRoute('app_collection_landing');
} }
/**
* @todo Move clone logic to __clone() in Entity or Repository
*/
#[Route('/collection/copy/{id<\d+>}', name: 'app_collection_copy')]
public function copy(Collection $collection, EntityManagerInterface $em): Response
{
try {
$this->denyAccessUnlessGranted(RecordVoter::EDIT, $collection);
}
catch (AccessDeniedException) {
$this->addFlash('warning', 'You are not authorized to copy this record');
return $this->redirectToRoute('app_home');
}
$user = $this->getUser();
$editor = "{$user->getFirstname()} {$user->getLastName()}";
$copy = clone $collection;
$copy->setEditor($editor);
$copy->setOwner($editor);
$copy->setCreator($user->getUsername());
$repo = $em->getRepository(Bibliography::class);
$copy->setBibliographies(
$repo->findAllByCollection($collection->getId())
);
/*
$repo = $em->getRepository(Collector::class);
$copy->setCollectors(
$repo->findAllByBibliography($bibliography->getId())
);
*/
$copy->setTitle("{$collection->getTitle()} - Copy");
$copy->setModifiedAt(new \DateTimeImmutable());
$em->persist($copy);
$em->flush();
$this->addFlash('notice', 'Record copied successfully');
return $this->redirectToRoute('app_collection', ['id' => $copy->getId()]);
}
} }

View File

@ -7,6 +7,15 @@
<h1 class="is-size-1 mt-0 has-text-centered">Bibliography</h1> <h1 class="is-size-1 mt-0 has-text-centered">Bibliography</h1>
<h2 class="is-size-3 mt-3 has-text-centered">{{ record.citation }}</h2> <h2 class="is-size-3 mt-3 has-text-centered">{{ record.citation }}</h2>
{% for message in app.flashes('notice') %}
<div class=" mt-4 notification is-success"
data-controller="notification"
data-notification-target="notif">
<button class="delete" data-action="click->notification#close"></button>
{{ message }}
</div>
{% endfor %}
<article class="message is-info mt-3"> <article class="message is-info mt-3">
<div class="message-body"> <div class="message-body">
<p> <p>

View File

@ -8,11 +8,14 @@
<h2 class="is-size-3 mt-3 has-text-centered">Choose action</h2> <h2 class="is-size-3 mt-3 has-text-centered">Choose action</h2>
{% for message in app.flashes('notice') %} {% for message in app.flashes('notice') %}
<div class=" mt-4 notification is-success"> <div class=" mt-4 notification is-success"
<button class="delete"></button> data-controller="notification"
data-notification-target="notif">
<button class="delete" data-action="click->notification#close"></button>
{{ message }} {{ message }}
</div> </div>
{% endfor %} {% endfor %}
<div class="card p-5 mt-6 pt-6 pb-6"> <div class="card p-5 mt-6 pt-6 pb-6">
<div class="columns"> <div class="columns">
<div class="column is-half has-text-centered"> <div class="column is-half has-text-centered">
@ -100,12 +103,4 @@
</div> </div>
</div> </div>
</div> </div>
<script type="text/javascript" defer>
const delBtns = document.querySelectorAll('.delete');
for (let btn of delBtns) {
btn.addEventListener('click', function () {
this.parentElement.classList.add('is-hidden');
})
}
</script>
{% endblock %} {% endblock %}

View File

@ -7,6 +7,15 @@
<h1 class="is-size-1 mt-0 has-text-centered">Collection</h1> <h1 class="is-size-1 mt-0 has-text-centered">Collection</h1>
<h2 class="is-size-3 mt-3 has-text-centered">{{ record.title }}</h2> <h2 class="is-size-3 mt-3 has-text-centered">{{ record.title }}</h2>
{% for message in app.flashes('notice') %}
<div class=" mt-4 notification is-success"
data-controller="notification"
data-notification-target="notif">
<button class="delete" data-action="click->notification#close"></button>
{{ message }}
</div>
{% endfor %}
<article class="message is-info mt-3"> <article class="message is-info mt-3">
<div class="message-body"> <div class="message-body">
<p> <p>
@ -27,12 +36,13 @@
<i class="fa fa-edit"></i> <i class="fa fa-edit"></i>
</span> </span>
</button> </button>
<button class="button is-link"> <a href="{{ path('app_collection_copy', {'id' : record.id}) }}"
class="button is-link">
Copy Copy
<span class="icon ml-2"> <span class="icon ml-2">
<i class="fa fa-copy"></i> <i class="fa fa-copy"></i>
</span> </span>
</button> </a>
<a href="{{ path('app_collection_del', {'id' : record.id}) }}" <a href="{{ path('app_collection_del', {'id' : record.id}) }}"
class="button is-danger" class="button is-danger"
data-delete-record-target="path" data-action="click->delete-record#show"> data-delete-record-target="path" data-action="click->delete-record#show">

View File

@ -8,10 +8,12 @@
<h2 class="is-size-3 mt-3 has-text-centered">Choose action</h2> <h2 class="is-size-3 mt-3 has-text-centered">Choose action</h2>
{% for message in app.flashes('notice') %} {% for message in app.flashes('notice') %}
<div class=" mt-4 notification is-success"> <div class=" mt-4 notification is-success"
<button class="delete"></button> data-controller="notification"
data-notification-target="notif">
<button class="delete" data-action="click->notification#close"></button>
{{ message }} {{ message }}
</div> </div>
{% endfor %} {% endfor %}
<div class="card p-5 mt-6 pt-6 pb-6"> <div class="card p-5 mt-6 pt-6 pb-6">
<div class="columns"> <div class="columns">
@ -100,12 +102,4 @@
</div> </div>
</div> </div>
</div> </div>
<script type="text/javascript" defer>
const delBtns = document.querySelectorAll('.delete');
for (let btn of delBtns) {
btn.addEventListener('click', function () {
this.parentElement.classList.add('is-hidden');
})
}
</script>
{% endblock %} {% endblock %}

View File

@ -8,10 +8,12 @@
<h2 class="is-size-3 mt-3 has-text-centered">Choose action</h2> <h2 class="is-size-3 mt-3 has-text-centered">Choose action</h2>
{% for message in app.flashes('notice') %} {% for message in app.flashes('notice') %}
<div class=" mt-4 notification is-success"> <div class=" mt-4 notification is-success"
<button class="delete"></button> data-controller="notification"
data-notification-target="notif">
<button class="delete" data-action="click->notification#close"></button>
{{ message }} {{ message }}
</div> </div>
{% endfor %} {% endfor %}
<div class="card p-5 mt-6 pt-6 pb-6"> <div class="card p-5 mt-6 pt-6 pb-6">
<div class="columns"> <div class="columns">
@ -100,12 +102,4 @@
</div> </div>
</div> </div>
</div> </div>
<script type="text/javascript" defer>
const delBtns = document.querySelectorAll('.delete');
for (let btn of delBtns) {
btn.addEventListener('click', function () {
this.parentElement.classList.add('is-hidden');
})
}
</script>
{% endblock %} {% endblock %}

View File

@ -12,7 +12,8 @@
<div class="card" style="max-width: 40vw; margin: 0 auto;"> <div class="card" style="max-width: 40vw; margin: 0 auto;">
{% if error %} {% if error %}
<div class="notification is-danger is-light" data-controller="notification" data-notification-target="notif"> <div class="notification is-danger is-light" data-controller="notification"
data-notification-target="notif">
<button class="delete" data-action="click->notification#close"></button> <button class="delete" data-action="click->notification#close"></button>
Wrong user name and/or password. Please retry Wrong user name and/or password. Please retry
</div> </div>

View File

@ -3,7 +3,7 @@
{% block title %}Vocab - Functional context | ArCOA{% endblock %} {% block title %}Vocab - Functional context | ArCOA{% endblock %}
{% block rightpanel %} {% block rightpanel %}
<div class="container" style="max-width: 50vw"> <div class="container" style="max-width: 50vw" data-controller="vocabs">
<h1 class="is-size-1 mt-0 has-text-centered">Vocabulary</h1> <h1 class="is-size-1 mt-0 has-text-centered">Vocabulary</h1>
<h2 class="is-size-3 mt-4 has-text-centered">Functional context</h2> <h2 class="is-size-3 mt-4 has-text-centered">Functional context</h2>
@ -28,11 +28,31 @@
</form> </form>
{% endif %} {% endif %}
<div class="notification is-success is-hidden mt-5" id="ajax-success"> <div class="notification is-success is-hidden mt-5"
</div> data-controller="notification"
data-notification-target="notif"
id="ajax-saved">
<button class="delete" data-action="click->notification#close"></button>
Term saved successfully
</div>
<div class="notification is-success is-hidden mt-5"
data-controller="notification"
data-notification-target="notif"
id="ajax-deleted">
<button class="delete" data-action="click->notification#close"></button>
Term deleted successfully
</div>
<div class="notification is-danger is-light is-hidden mt-5"
data-controller="notification"
data-notification-target="notif"
id="ajax-error">
<button class="delete" data-action="click->notification#close"></button>
The term could not be deleted because it's related to existing records
</div>
{% for message in app.flashes('notice') %} {% for message in app.flashes('notice') %}
<div class="notification is-success mt-5" id="server-success"> <div class="notification is-success mt-5" data-controller="notification"
<button class="delete"></button> data-notification-target="notif">
<button class="delete" data-action="click->notification#close"></button>
{{ message }} {{ message }}
</div> </div>
{% endfor %} {% endfor %}
@ -40,26 +60,47 @@
<table class="table is-fullwidth" id="terms"> <table class="table is-fullwidth" id="terms">
<tr><th>Term</th>{% if is_granted('ROLE_ADMIN') or is_granted('ROLE_REVISOR') %}<th>Actions</th>{% endif %}</tr> <tr><th>Term</th>{% if is_granted('ROLE_ADMIN') or is_granted('ROLE_REVISOR') %}<th>Actions</th>{% endif %}</tr>
{% for term in terms %} {% for term in terms %}
<tr data-row-id="{{ term.id }}"> <tr data-vocabs-target="row"
data-id="{{ term.id }}">
<td> <td>
<input class="input" type="text" value="{{ term.term }}" disabled data-term-id="{{ term.id }}" /> <input class="input" type="text" value="{{ term.term }}" disabled
data-id="{{ term.id }}" data-vocabs-target="input" />
</td> </td>
{% if is_granted('ROLE_ADMIN') or is_granted('ROLE_REVISOR') %} {% if is_granted('ROLE_ADMIN') or is_granted('ROLE_REVISOR') %}
<td> <td>
<div class="buttons"> <div class="buttons">
<button class="button is-primary is-hidden" data-id-save="{{ term.id }}"> <button class="button is-primary is-hidden"
data-vocabs-target="save"
data-action="click->vocabs#save"
data-url="{{ path('app_vocab_func_context_upd') }}"
data-id="{{ term.id }}">
Save Save
<span class="icon ml-2"> <span class="icon ml-2">
<i class="fa fa-save"></i> <i class="fa fa-save"></i>
</span> </span>
</button> </button>
<button class="button is-link" data-id-edit="{{ term.id }}"> <button class="button is-hidden"
data-id="{{ term.id }}"
data-vocabs-target="cancel"
data-action="click->vocabs#cancel">
Cancel
<span class="icon ml-2">
<i class="fa fa-times"></i>
</span>
</button>
<button class="button is-link"
data-vocabs-target="edit"
data-id="{{ term.id }}"
data-action="click->vocabs#edit">
Edit Edit
<span class="icon ml-2"> <span class="icon ml-2">
<i class="fa fa-edit"></i> <i class="fa fa-edit"></i>
</span> </span>
</button> </button>
<button class="button is-danger" data-id-delete="{{ term.id }}"> <button class="button is-danger"
data-id="{{ term.id }}"
data-action="click->vocabs#delete"
data-url="{{ path('app_vocab_func_context_del') }}">
Delete Delete
<span class="icon ml-2"> <span class="icon ml-2">
<i class="fa fa-trash"></i> <i class="fa fa-trash"></i>
@ -71,97 +112,27 @@
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>
</div>
<div class="modal" data-vocabs-target="modal">
<div class="modal-background"></div>
<div class="modal-card">
<header class="modal-card-head">
<span class="icon is-large has-text-warning">
<i class="fa fa-warning fa-2x"></i>
</span>
<p class="modal-card-title has-text-danger pl-2"><strong>Delete term?</strong></p>
<button class="delete" aria-label="close"></button>
</header>
<section class="modal-card-body">
<p class="is-size-5">This term will be permanently deleted. Proceed?</p>
</section>
<footer class="modal-card-foot">
<div class="buttons is-right">
<button class="button is-link" data-action="click->vocabs#delete">Confirm</button>
<button class="button is-light" id="cancel">Cancel</button>
</div>
</footer>
</div>
</div> </div>
</div> </div>
<script type="text/javascript" defer>
const terms = document.querySelector('#terms');
const serverNotice = document.querySelector('#server-success');
if (serverNotice) {
serverNotice.querySelector('.delete').addEventListener('click', () => {
serverNotice.remove();
});
}
terms.addEventListener('click', async (event) => {
const clicked = event.target;
if (clicked.getAttribute('data-id-delete')) {
const termId = clicked.getAttribute('data-id-delete');
const data = new FormData;
data.append("_id", termId);
const res = await fetch("{{ path('app_vocab_func_context_del') }}", {
method: "POST",
body: data,
});
const notice = document.querySelector('#ajax-success');
if (res.status === 200) {
notice.innerHTML = `
<button class="delete"></button>
Term deleted successfully
`;
notice.classList.remove('is-hidden');
notice.querySelector('.delete').addEventListener('click', () => {
notice.classList.add('is-hidden');
});
const row = document.querySelector(`tr[data-row-id="${termId}"]`);
row.remove();
}
}
if (clicked.getAttribute('data-id-edit')) {
const termId = clicked.getAttribute('data-id-edit');
const saveBtn = document.querySelector(`button[data-id-save="${termId}"]`);
const input = document.querySelector(`input[data-term-id="${termId}"]`);
input.disabled = input.disabled ? false : true;
saveBtn.classList.toggle('is-hidden');
clicked.classList.toggle('is-link');
if (!clicked.classList.contains('is-link')) {
clicked.innerHTML = `
Cancel
<span class="icon ml-2">
<i class="fa fa-times"></i>
</span>
`;
} else {
clicked.innerHTML = `
Edit
<span class="icon ml-2">
<i class="fa fa-edit"></i>
</span>
`;
}
const data = new FormData;
data.append("_id", termId);
saveBtn.addEventListener('click', async () => {
data.append("_new_term", input.value);
const res = await fetch("{{ path('app_vocab_func_context_upd') }}", {
method: "POST",
body: data,
});
const notice = document.querySelector('#ajax-success');
if (res.status === 200) {
notice.innerHTML = `
<button class="delete"></button>
Term updated successfully
`;
notice.classList.remove('is-hidden');
notice.querySelector('.delete').addEventListener('click', () => {
notice.classList.add('is-hidden');
});
}
});
}
});
</script>
{% endblock %} {% endblock %}