Record deletion logic (maybe)
This commit is contained in:
parent
36185d0539
commit
3c2b804498
@ -33,4 +33,3 @@ export default class extends Controller {
|
|||||||
location.href = delPath;
|
location.href = delPath;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,6 +40,7 @@ security:
|
|||||||
- { path: ^/bibliography, roles: ROLE_USER }
|
- { path: ^/bibliography, roles: ROLE_USER }
|
||||||
- { path: ^/collection, roles: ROLE_USER }
|
- { path: ^/collection, roles: ROLE_USER }
|
||||||
- { path: ^/collector, roles: ROLE_USER }
|
- { path: ^/collector, roles: ROLE_USER }
|
||||||
|
- { path: ^/document, roles: ROLE_USER }
|
||||||
- { path: ^/object, roles: ROLE_USER }
|
- { path: ^/object, roles: ROLE_USER }
|
||||||
- { path: ^/site, roles: ROLE_USER }
|
- { path: ^/site, roles: ROLE_USER }
|
||||||
|
|
||||||
|
@ -6,19 +6,19 @@ use App\Entity\Bibliography;
|
|||||||
use App\Entity\Collection;
|
use App\Entity\Collection;
|
||||||
use App\Entity\Collector;
|
use App\Entity\Collector;
|
||||||
use App\Form\BibliographyType;
|
use App\Form\BibliographyType;
|
||||||
//use App\Security\Voter\VocabVoter;
|
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;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
|
||||||
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;
|
||||||
|
|
||||||
class BibliographyController extends AbstractController
|
class BibliographyController extends AbstractController
|
||||||
{
|
{
|
||||||
#[Route('/bibliography/{id<\d+>}', name: 'app_bibliography')]
|
#[Route('/bibliography/{id<\d+>}', name: 'app_bibliography')]
|
||||||
public function index(Bibliography $bibliography, EntityManagerInterface $em): Response
|
public function index(Bibliography $bibliography, EntityManagerInterface $em): Response
|
||||||
{
|
{
|
||||||
|
|
||||||
$repo = $em->getRepository(Collection::class);
|
$repo = $em->getRepository(Collection::class);
|
||||||
$collections = $repo->findAllByBibliography($bibliography->getId());
|
$collections = $repo->findAllByBibliography($bibliography->getId());
|
||||||
$repo = $em->getRepository(Collector::class);
|
$repo = $em->getRepository(Collector::class);
|
||||||
@ -71,15 +71,23 @@ class BibliographyController extends AbstractController
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* @todo Permissions!
|
* @todo Permissions! Return JSON with 403 when AJAX
|
||||||
*/
|
*/
|
||||||
#[Route('/bibliography/delete/{id<\d+>}', name: 'app_bibliography_del')]
|
#[Route('/bibliography/delete/{id<\d+>}', name: 'app_bibliography_del')]
|
||||||
public function delete(Bibliography $bibliography, EntityManagerInterface $em): Response
|
public function delete(Bibliography $bibliography, EntityManagerInterface $em): Response
|
||||||
{
|
{
|
||||||
|
try {
|
||||||
|
$this->denyAccessUnlessGranted(RecordVoter::DELETE, $bibliography);
|
||||||
|
}
|
||||||
|
catch (AccessDeniedException) {
|
||||||
|
$this->addFlash('warning', 'You are not authorized to delete this record');
|
||||||
|
return $this->redirectToRoute('app_home');
|
||||||
|
}
|
||||||
|
|
||||||
$em->remove($bibliography);
|
$em->remove($bibliography);
|
||||||
$em->flush();
|
$em->flush();
|
||||||
|
|
||||||
$this->addFlash('notice', 'Term deleted successfully');
|
$this->addFlash('notice', 'Record deleted successfully');
|
||||||
|
|
||||||
return $this->redirectToRoute('app_bibliography_landing');
|
return $this->redirectToRoute('app_bibliography_landing');
|
||||||
}
|
}
|
||||||
|
@ -4,10 +4,12 @@ namespace App\Controller;
|
|||||||
|
|
||||||
use App\Entity\Collection;
|
use App\Entity\Collection;
|
||||||
use App\Entity\Bibliography;
|
use App\Entity\Bibliography;
|
||||||
|
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;
|
||||||
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;
|
||||||
|
|
||||||
class CollectionController extends AbstractController
|
class CollectionController extends AbstractController
|
||||||
{
|
{
|
||||||
@ -40,4 +42,23 @@ class CollectionController extends AbstractController
|
|||||||
'count' => $count,
|
'count' => $count,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Route('/collection/delete/{id<\d+>}', name: 'app_collection_del')]
|
||||||
|
public function delete(Collection $collection, EntityManagerInterface $em): Response
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$this->denyAccessUnlessGranted(RecordVoter::DELETE, $collection);
|
||||||
|
}
|
||||||
|
catch (AccessDeniedException) {
|
||||||
|
$this->addFlash('warning', 'You are not authorized to delete this record');
|
||||||
|
return $this->redirectToRoute('app_home');
|
||||||
|
}
|
||||||
|
|
||||||
|
$em->remove($collection);
|
||||||
|
$em->flush();
|
||||||
|
|
||||||
|
$this->addFlash('notice', 'Record deleted successfully');
|
||||||
|
|
||||||
|
return $this->redirectToRoute('app_collection_landing');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,10 +5,12 @@ namespace App\Controller;
|
|||||||
use App\Entity\Collector;
|
use App\Entity\Collector;
|
||||||
use App\Entity\Collection;
|
use App\Entity\Collection;
|
||||||
use App\Entity\Bibliography;
|
use App\Entity\Bibliography;
|
||||||
|
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;
|
||||||
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;
|
||||||
|
|
||||||
class CollectorController extends AbstractController
|
class CollectorController extends AbstractController
|
||||||
{
|
{
|
||||||
@ -40,4 +42,23 @@ class CollectorController extends AbstractController
|
|||||||
'count' => $count,
|
'count' => $count,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Route('/collector/delete/{id<\d+>}', name: 'app_collector_del')]
|
||||||
|
public function delete(Collector $collector, EntityManagerInterface $em): Response
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$this->denyAccessUnlessGranted(RecordVoter::DELETE, $collector);
|
||||||
|
}
|
||||||
|
catch (AccessDeniedException) {
|
||||||
|
$this->addFlash('warning', 'You are not authorized to delete this record');
|
||||||
|
return $this->redirectToRoute('app_home');
|
||||||
|
}
|
||||||
|
|
||||||
|
$em->remove($collector);
|
||||||
|
$em->flush();
|
||||||
|
|
||||||
|
$this->addFlash('notice', 'Record deleted successfully');
|
||||||
|
|
||||||
|
return $this->redirectToRoute('app_collector_landing');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Entity;
|
namespace App\Entity;
|
||||||
|
|
||||||
|
use App\RecordInterface;
|
||||||
use App\Repository\BibliographyRepository;
|
use App\Repository\BibliographyRepository;
|
||||||
use DateTimeImmutable;
|
use DateTimeImmutable;
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
@ -11,7 +12,7 @@ use Doctrine\Common\Collections\Collection as DoctrineCollection;
|
|||||||
|
|
||||||
#[ORM\Entity(repositoryClass: BibliographyRepository::class)]
|
#[ORM\Entity(repositoryClass: BibliographyRepository::class)]
|
||||||
#[ORM\Table(name: 'bibliography')]
|
#[ORM\Table(name: 'bibliography')]
|
||||||
class Bibliography
|
class Bibliography implements RecordInterface
|
||||||
{
|
{
|
||||||
#[ORM\Id]
|
#[ORM\Id]
|
||||||
#[ORM\GeneratedValue]
|
#[ORM\GeneratedValue]
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Entity;
|
namespace App\Entity;
|
||||||
|
|
||||||
|
use App\RecordInterface;
|
||||||
use App\Repository\CollectionRepository;
|
use App\Repository\CollectionRepository;
|
||||||
use DateTimeImmutable;
|
use DateTimeImmutable;
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
@ -11,7 +12,7 @@ use Doctrine\Common\Collections\Collection as DoctrineCollection;
|
|||||||
|
|
||||||
#[ORM\Entity(repositoryClass: CollectionRepository::class)]
|
#[ORM\Entity(repositoryClass: CollectionRepository::class)]
|
||||||
#[ORM\Table(name: 'collection')]
|
#[ORM\Table(name: 'collection')]
|
||||||
class Collection
|
class Collection implements RecordInterface
|
||||||
{
|
{
|
||||||
#[ORM\Id]
|
#[ORM\Id]
|
||||||
#[ORM\GeneratedValue]
|
#[ORM\GeneratedValue]
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Entity;
|
namespace App\Entity;
|
||||||
|
|
||||||
|
use App\RecordInterface;
|
||||||
use App\Repository\CollectorRepository;
|
use App\Repository\CollectorRepository;
|
||||||
use DateTimeImmutable;
|
use DateTimeImmutable;
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
@ -11,7 +12,7 @@ use Doctrine\Common\Collections\Collection as DoctrineCollection;
|
|||||||
|
|
||||||
#[ORM\Entity(repositoryClass: CollectorRepository::class)]
|
#[ORM\Entity(repositoryClass: CollectorRepository::class)]
|
||||||
#[ORM\Table(name: 'collector')]
|
#[ORM\Table(name: 'collector')]
|
||||||
class Collector
|
class Collector implements RecordInterface
|
||||||
{
|
{
|
||||||
#[ORM\Id]
|
#[ORM\Id]
|
||||||
#[ORM\GeneratedValue]
|
#[ORM\GeneratedValue]
|
||||||
|
7
src/RecordInterface.php
Normal file
7
src/RecordInterface.php
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App;
|
||||||
|
|
||||||
|
interface RecordInterface
|
||||||
|
{
|
||||||
|
}
|
48
src/Security/Voter/RecordVoter.php
Normal file
48
src/Security/Voter/RecordVoter.php
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Security\Voter;
|
||||||
|
|
||||||
|
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||||
|
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||||
|
use Symfony\Component\Security\Core\User\UserInterface;
|
||||||
|
|
||||||
|
final class RecordVoter extends Voter
|
||||||
|
{
|
||||||
|
public const EDIT = 'RECORD_EDIT';
|
||||||
|
public const DELETE = 'RECORD_DELETE';
|
||||||
|
public const VIEW = 'RECORD_VIEW';
|
||||||
|
|
||||||
|
protected function supports(string $attribute, mixed $subject): bool
|
||||||
|
{
|
||||||
|
return in_array($attribute, [self::EDIT, self::VIEW, self::DELETE])
|
||||||
|
&& $subject instanceof \App\RecordInterface;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
|
||||||
|
{
|
||||||
|
$user = $token->getUser();
|
||||||
|
|
||||||
|
// if the user is anonymous, do not grant access
|
||||||
|
if (!$user instanceof UserInterface) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$roles = $user->getRoles();
|
||||||
|
|
||||||
|
// TODO: Better way to check roles?
|
||||||
|
switch ($attribute) {
|
||||||
|
case self::EDIT:
|
||||||
|
case self::DELETE:
|
||||||
|
return in_array('ROLE_ADMIN', $roles)
|
||||||
|
|| in_array('ROLE_REVISOR', $roles);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case self::VIEW:
|
||||||
|
return ! in_array('ROLE_READER', $roles);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,7 +3,7 @@
|
|||||||
{% block title %}Bibliography | ArCOA{% endblock %}
|
{% block title %}Bibliography | ArCOA{% endblock %}
|
||||||
|
|
||||||
{% block rightpanel %}
|
{% block rightpanel %}
|
||||||
<div class="container" style="max-width: 60vw">
|
<div class="container" style="max-width: 60vw" data-controller="delete-record">
|
||||||
<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">Choose action</h2>
|
<h2 class="is-size-3 mt-3 has-text-centered">Choose action</h2>
|
||||||
|
|
||||||
@ -66,16 +66,39 @@
|
|||||||
<i class="fa fa-edit"></i>
|
<i class="fa fa-edit"></i>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="button is-small is-danger" title="Delete record">
|
<a href="{{ path('app_bibliography_del', {'id' : record.id}) }}"
|
||||||
|
class="button is-small is-danger" title="Delete record"
|
||||||
|
data-delete-record-target="path" data-action="click->delete-record#show">
|
||||||
<span class="icon">
|
<span class="icon">
|
||||||
<i class="fa fa-trash"></i>
|
<i class="fa fa-trash"></i>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
|
<div class="modal" data-delete-record-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 record?</strong></p>
|
||||||
|
<button class="delete" aria-label="close"></button>
|
||||||
|
</header>
|
||||||
|
<section class="modal-card-body">
|
||||||
|
<p class="is-size-5">This record 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->delete-record#delete">Confirm</button>
|
||||||
|
<button class="button is-light" id="cancel">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script type="text/javascript" defer>
|
<script type="text/javascript" defer>
|
||||||
const delBtns = document.querySelectorAll('.delete');
|
const delBtns = document.querySelectorAll('.delete');
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
{% block title %}Collection | ArCOA{% endblock %}
|
{% block title %}Collection | ArCOA{% endblock %}
|
||||||
|
|
||||||
{% block rightpanel %}
|
{% block rightpanel %}
|
||||||
<div class="container" style="max-width: 60vw">
|
<div class="container" style="max-width: 60vw" data-controller="delete-record">
|
||||||
<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">Choose action</h2>
|
<h2 class="is-size-3 mt-3 has-text-centered">Choose action</h2>
|
||||||
|
|
||||||
@ -66,16 +66,39 @@
|
|||||||
<i class="fa fa-edit"></i>
|
<i class="fa fa-edit"></i>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="button is-small is-danger" title="Delete record">
|
<a href="{{ path('app_collection_del', {'id' : record.id}) }}"
|
||||||
|
class="button is-small is-danger" title="Delete record"
|
||||||
|
data-delete-record-target="path" data-action="click->delete-record#show">
|
||||||
<span class="icon">
|
<span class="icon">
|
||||||
<i class="fa fa-trash"></i>
|
<i class="fa fa-trash"></i>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
|
<div class="modal" data-delete-record-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 record?</strong></p>
|
||||||
|
<button class="delete" aria-label="close"></button>
|
||||||
|
</header>
|
||||||
|
<section class="modal-card-body">
|
||||||
|
<p class="is-size-5">This record 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->delete-record#delete">Confirm</button>
|
||||||
|
<button class="button is-light" id="cancel">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script type="text/javascript" defer>
|
<script type="text/javascript" defer>
|
||||||
const delBtns = document.querySelectorAll('.delete');
|
const delBtns = document.querySelectorAll('.delete');
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
{% block title %}Collector | ArCOA{% endblock %}
|
{% block title %}Collector | ArCOA{% endblock %}
|
||||||
|
|
||||||
{% block rightpanel %}
|
{% block rightpanel %}
|
||||||
<div class="container" style="max-width: 60vw">
|
<div class="container" style="max-width: 60vw" data-controller="delete-record">
|
||||||
<h1 class="is-size-1 mt-0 has-text-centered">Collector</h1>
|
<h1 class="is-size-1 mt-0 has-text-centered">Collector</h1>
|
||||||
<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>
|
||||||
|
|
||||||
@ -66,16 +66,39 @@
|
|||||||
<i class="fa fa-edit"></i>
|
<i class="fa fa-edit"></i>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="button is-small is-danger" title="Delete record">
|
<a href="{{ path('app_collector_del', {'id' : record.id}) }}"
|
||||||
|
class="button is-small is-danger" title="Delete record"
|
||||||
|
data-delete-record-target="path" data-action="click->delete-record#show">
|
||||||
<span class="icon">
|
<span class="icon">
|
||||||
<i class="fa fa-trash"></i>
|
<i class="fa fa-trash"></i>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
|
<div class="modal" data-delete-record-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 record?</strong></p>
|
||||||
|
<button class="delete" aria-label="close"></button>
|
||||||
|
</header>
|
||||||
|
<section class="modal-card-body">
|
||||||
|
<p class="is-size-5">This record 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->delete-record#delete">Confirm</button>
|
||||||
|
<button class="button is-light" id="cancel">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script type="text/javascript" defer>
|
<script type="text/javascript" defer>
|
||||||
const delBtns = document.querySelectorAll('.delete');
|
const delBtns = document.querySelectorAll('.delete');
|
||||||
|
Loading…
Reference in New Issue
Block a user