diff --git a/assets/ts_src/ajax_ui.ts b/assets/ts_src/ajax_ui.ts index e64ebabc..62626ed6 100644 --- a/assets/ts_src/ajax_ui.ts +++ b/assets/ts_src/ajax_ui.ts @@ -529,6 +529,7 @@ class AjaxUI { 'className': 'mr-2 btn-light', "text": "" }], + "select": $table.data('select') ?? false, "rowCallback": function( row, data, index ) { //Check if we have a level, then change color of this row if (data.level) { @@ -564,6 +565,30 @@ class AjaxUI { $('#part-card-header').html(title.html()); $(document).trigger('ajaxUI:dt_loaded'); + + if($table.data('part_table')) { + //@ts-ignore + $('#dt').on( 'select.dt deselect.dt', function ( e, dt, items ) { + let selected_elements = dt.rows({selected: true}); + let count = selected_elements.count(); + + if(count > 0) { + $('#select_panel').removeClass('d-none'); + } else { + $('#select_panel').addClass('d-none'); + } + + $('#select_count').text(count); + + let selected_ids_string = selected_elements.data().map(function(value, index) { + return value['id']; } + ).join(","); + + $('#select_ids').val(selected_ids_string); + + } ); + } + //Attach event listener to update links after new page selection: $('#dt').on('draw.dt column-visibility.dt', function() { ajaxUI.registerLinks(); diff --git a/src/Controller/PartListsController.php b/src/Controller/PartListsController.php index 73838577..574d4221 100644 --- a/src/Controller/PartListsController.php +++ b/src/Controller/PartListsController.php @@ -48,6 +48,7 @@ use App\Entity\Parts\Footprint; use App\Entity\Parts\Manufacturer; use App\Entity\Parts\Storelocation; use App\Entity\Parts\Supplier; +use App\Services\Parts\PartsTableActionHandler; use Doctrine\ORM\EntityManagerInterface; use Omines\DataTablesBundle\DataTableFactory; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -65,6 +66,36 @@ class PartListsController extends AbstractController $this->entityManager = $entityManager; } + /** + * @Route("/table/action", name="table_action", methods={"POST"}) + */ + public function tableAction(Request $request, PartsTableActionHandler $actionHandler): Response + { + $redirect = $request->request->get('redirect_back'); + $ids = $request->request->get('ids'); + $action = $request->request->get('action'); + + if (!$this->isCsrfTokenValid('table_action', $request->request->get('_token'))) { + $this->addFlash('error', 'csfr_invalid'); + return $this->redirect($redirect); + } + + if ($action === null || $ids === null) { + $this->addFlash('error', 'part.table.actions.no_params_given'); + } else { + $parts = $actionHandler->idStringToArray($ids); + $actionHandler->handleAction($action, $parts, null); + + //Save changes + $this->entityManager->flush(); + + $this->addFlash('success', 'Done'); + } + + + return $this->redirect($redirect); + } + /** * @Route("/category/{id}/parts", name="part_list_category") * diff --git a/src/Services/Parts/PartsTableActionHandler.php b/src/Services/Parts/PartsTableActionHandler.php new file mode 100644 index 00000000..e0921672 --- /dev/null +++ b/src/Services/Parts/PartsTableActionHandler.php @@ -0,0 +1,103 @@ +. + */ + +namespace App\Services\Parts; + + +use App\Entity\Parts\Part; +use Doctrine\ORM\EntityManagerInterface; +use Symfony\Component\Security\Core\Exception\AccessDeniedException; +use Symfony\Component\Security\Core\Security; + +final class PartsTableActionHandler +{ + private $entityManager; + private $security; + + public function __construct(EntityManagerInterface $entityManager, Security $security) + { + $this->entityManager = $entityManager; + $this->security = $security; + } + + /** + * Converts the given array to an array of Parts + * @param string $ids A comma separated list of Part IDs. + * @return Part[] + */ + public function idStringToArray(string $ids): array + { + $id_array = explode(',', $ids); + + $repo = $this->entityManager->getRepository(Part::class); + return $repo->getElementsFromIDArray($id_array); + } + + /** + * @param string $action + * @param Part[] $selected_parts + * @param int|null $target_id + */ + public function handleAction(string $action, array $selected_parts, ?int $target_id): void + { + //Iterate over the parts and apply the action to it: + foreach ($selected_parts as $part) { + if (!$part instanceof Part) { + throw new \InvalidArgumentException('$selected_parts must be an array of Part elements!'); + } + + //We modify parts, so you have to have the permission to modify it + $this->denyAccessUnlessGranted('edit', $part); + + switch ($action) { + case 'favorite': + $part->setFavorite(true); + break; + case 'unfavorite': + $part->setFavorite(false); + break; + case 'delete': + $this->denyAccessUnlessGranted('delete', $part); + $this->entityManager->remove($part); + break; + + default: + throw new \InvalidArgumentException('The given action is unknown! (' . $action . ')'); + } + } + } + + /** + * Throws an exception unless the attributes are granted against the current authentication token and optionally + * supplied subject. + * + * @throws AccessDeniedException + */ + private function denyAccessUnlessGranted($attributes, $subject = null, string $message = 'Access Denied.'): void + { + if (!$this->security->isGranted($attributes, $subject)) { + $exception = new AccessDeniedException($message); + $exception->setAttributes($attributes); + $exception->setSubject($subject); + + throw $exception; + } + } +} \ No newline at end of file diff --git a/templates/Parts/lists/_parts_list.html.twig b/templates/Parts/lists/_parts_list.html.twig index 813b0be6..6dddd8d0 100644 --- a/templates/Parts/lists/_parts_list.html.twig +++ b/templates/Parts/lists/_parts_list.html.twig @@ -1,12 +1,31 @@ +
+ -
-
-
-
-

{% trans %}part_list.loading.caption{% endtrans %}

-
{% trans %}part_list.loading.message{% endtrans %}
+ + + + +
+

elements selected!

+ + + + +
+ +
+
+
+
+

{% trans %}part_list.loading.caption{% endtrans %}

+
{% trans %}part_list.loading.message{% endtrans %}
+
-
+