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..e5e0065b 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,37 @@ 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'); + $target = $request->request->get('target'); + + 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, $target ? (int) $target : null); + + //Save changes + $this->entityManager->flush(); + + $this->addFlash('success', 'part.table.actions.success'); + } + + + return $this->redirect($redirect); + } + /** * @Route("/category/{id}/parts", name="part_list_category") * diff --git a/src/Controller/SelectAPIController.php b/src/Controller/SelectAPIController.php new file mode 100644 index 00000000..851850f7 --- /dev/null +++ b/src/Controller/SelectAPIController.php @@ -0,0 +1,120 @@ +. + */ + +namespace App\Controller; + + +use App\Entity\Base\AbstractStructuralDBElement; +use App\Entity\Parts\Category; +use App\Entity\Parts\Footprint; +use App\Entity\Parts\Manufacturer; +use App\Entity\Parts\MeasurementUnit; +use App\Services\Trees\NodesListBuilder; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Event\ResponseEvent; +use Symfony\Component\Routing\Annotation\Route; +use Symfony\Contracts\Translation\TranslatorInterface; + +/** + * @Route("/select_api") + * @package App\Controller + */ +class SelectAPIController extends AbstractController +{ + private $nodesListBuilder; + private $translator; + + public function __construct(NodesListBuilder $nodesListBuilder, TranslatorInterface $translator) + { + $this->nodesListBuilder = $nodesListBuilder; + $this->translator = $translator; + } + + /** + * @Route("/category", name="select_category") + */ + public function category(): Response + { + return $this->getResponseForClass(Category::class); + } + + /** + * @Route("/footprint", name="select_footprint") + */ + public function footprint(): Response + { + return $this->getResponseForClass(Footprint::class, true); + } + + /** + * @Route("/manufacturer", name="select_manufacturer") + */ + public function manufacturer(): Response + { + return $this->getResponseForClass(Manufacturer::class, true); + } + + /** + * @Route("/measurement_unit", name="select_measurement_unit") + */ + public function measurement_unit(): Response + { + return $this->getResponseForClass(MeasurementUnit::class, true); + } + + protected function getResponseForClass(string $class, bool $include_empty = false): Response + { + $test_obj = new $class; + $this->denyAccessUnlessGranted('read', $test_obj); + + $nodes = $this->nodesListBuilder->typeToNodesList($class); + + $json = $this->buildJSONStructure($nodes); + + if ($include_empty) { + array_unshift($json, [ + 'text' => '', + 'value' => null, + 'data-subtext' => $this->translator->trans('part_list.action.select_null'), + ]); + } + + return $this->json($json); + } + + protected function buildJSONStructure(array $nodes_list): array + { + $entries = []; + + foreach ($nodes_list as $node) { + /** @var AbstractStructuralDBElement $node */ + $entry = [ + 'text' => str_repeat('   ', $node->getLevel()) . htmlspecialchars($node->getName()), + 'value' => $node->getID(), + 'data-subtext' => $node->getParent() ? $node->getParent()->getFullPath() : null, + ]; + + $entries[] = $entry; + } + + return $entries; + } +} \ No newline at end of file diff --git a/src/Services/Parts/PartsTableActionHandler.php b/src/Services/Parts/PartsTableActionHandler.php new file mode 100644 index 00000000..130a6427 --- /dev/null +++ b/src/Services/Parts/PartsTableActionHandler.php @@ -0,0 +1,123 @@ +. + */ + +namespace App\Services\Parts; + + +use App\Entity\Parts\Category; +use App\Entity\Parts\Footprint; +use App\Entity\Parts\Manufacturer; +use App\Entity\Parts\MeasurementUnit; +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; + case 'change_category': + $this->denyAccessUnlessGranted('category.edit', $part); + $part->setCategory($this->entityManager->find(Category::class, $target_id)); + break; + case 'change_footprint': + $this->denyAccessUnlessGranted('footprint.edit', $part); + $part->setFootprint($target_id === null ? null : $this->entityManager->find(Footprint::class, $target_id)); + break; + case 'change_manufacturer': + $this->denyAccessUnlessGranted('manufacturer.edit', $part); + $part->setManufacturer($target_id === null ? null : $this->entityManager->find(Manufacturer::class, $target_id)); + break; + case 'change_unit': + $this->denyAccessUnlessGranted('unit.edit', $part); + $part->setPartUnit($target_id === null ? null : $this->entityManager->find(MeasurementUnit::class, $target_id)); + 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..7436d7f8 100644 --- a/templates/Parts/lists/_parts_list.html.twig +++ b/templates/Parts/lists/_parts_list.html.twig @@ -1,12 +1,90 @@ +
+ -
-
-
-
-

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

-
{% trans %}part_list.loading.message{% endtrans %}
+ + + + +
+ {# #} + {% trans with {'%count%': ''} %}part_list.action.part_count{% endtrans %} + + + + + + +
+ +
+
+
+
+

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

+
{% trans %}part_list.loading.message{% endtrans %}
+
-
+ + \ No newline at end of file diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index 51235b37..1c132884 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -9111,5 +9111,77 @@ Element 3 Edit part + + + part_list.action.action.title + Select action + + + + + part_list.action.action.group.favorite + Favorite status + + + + + part_list.action.action.favorite + Favorite + + + + + part_list.action.action.unfavorite + Unfavorite + + + + + part_list.action.action.group.change_field + Change field + + + + + part_list.action.action.change_category + Change category + + + + + part_list.action.action.change_footprint + Change footprint + + + + + part_list.action.action.change_manufacturer + Change manufacturer + + + + + part_list.action.action.change_unit + Change part unit + + + + + part_list.action.action.delete + Delete + + + + + part_list.action.submit + Submit + + + + + part_list.action.part_count + %count% parts selected! + +