diff --git a/bundle/API/Repository/Events/Tags/BeforeHideTagEvent.php b/bundle/API/Repository/Events/Tags/BeforeHideTagEvent.php new file mode 100644 index 00000000..a6a90d96 --- /dev/null +++ b/bundle/API/Repository/Events/Tags/BeforeHideTagEvent.php @@ -0,0 +1,18 @@ +tag; + } +} diff --git a/bundle/API/Repository/Events/Tags/BeforeRevealTagEvent.php b/bundle/API/Repository/Events/Tags/BeforeRevealTagEvent.php new file mode 100644 index 00000000..27a00d95 --- /dev/null +++ b/bundle/API/Repository/Events/Tags/BeforeRevealTagEvent.php @@ -0,0 +1,18 @@ +tag; + } +} diff --git a/bundle/API/Repository/Events/Tags/HideTagEvent.php b/bundle/API/Repository/Events/Tags/HideTagEvent.php new file mode 100644 index 00000000..6a82c37a --- /dev/null +++ b/bundle/API/Repository/Events/Tags/HideTagEvent.php @@ -0,0 +1,18 @@ +tag; + } +} diff --git a/bundle/API/Repository/Events/Tags/RevealTagEvent.php b/bundle/API/Repository/Events/Tags/RevealTagEvent.php new file mode 100644 index 00000000..1b710de9 --- /dev/null +++ b/bundle/API/Repository/Events/Tags/RevealTagEvent.php @@ -0,0 +1,18 @@ +tag; + } +} diff --git a/bundle/API/Repository/TagsService.php b/bundle/API/Repository/TagsService.php index 9d4f4008..7a0e8ac5 100644 --- a/bundle/API/Repository/TagsService.php +++ b/bundle/API/Repository/TagsService.php @@ -76,7 +76,7 @@ public function loadTagByUrl(string $url, array $languages): Tag; * * @return \Netgen\TagsBundle\API\Repository\Values\Tags\TagList */ - public function loadTagChildren(?Tag $tag = null, int $offset = 0, int $limit = -1, ?array $languages = null, bool $useAlwaysAvailable = true): TagList; + public function loadTagChildren(?Tag $tag = null, int $offset = 0, int $limit = -1, ?array $languages = null, bool $useAlwaysAvailable = true, ?bool $showHiddenTags = null): TagList; /** * Returns the number of children of a tag object. @@ -89,7 +89,7 @@ public function loadTagChildren(?Tag $tag = null, int $offset = 0, int $limit = * * @return int */ - public function getTagChildrenCount(?Tag $tag = null, ?array $languages = null, bool $useAlwaysAvailable = true): int; + public function getTagChildrenCount(?Tag $tag = null, ?array $languages = null, bool $useAlwaysAvailable = true, ?bool $showHiddenTags = null): int; /** * Loads tags by specified keyword. @@ -104,7 +104,7 @@ public function getTagChildrenCount(?Tag $tag = null, ?array $languages = null, * * @return \Netgen\TagsBundle\API\Repository\Values\Tags\TagList */ - public function loadTagsByKeyword(string $keyword, string $language, bool $useAlwaysAvailable = true, int $offset = 0, int $limit = -1): TagList; + public function loadTagsByKeyword(string $keyword, string $language, bool $useAlwaysAvailable = true, int $offset = 0, int $limit = -1, ?bool $showHiddenTags = null): TagList; /** * Returns the number of tags by specified keyword. @@ -117,7 +117,7 @@ public function loadTagsByKeyword(string $keyword, string $language, bool $useAl * * @return int */ - public function getTagsByKeywordCount(string $keyword, string $language, bool $useAlwaysAvailable = true): int; + public function getTagsByKeywordCount(string $keyword, string $language, bool $useAlwaysAvailable = true, ?bool $showHiddenTags = null): int; /** * Search for tags. @@ -132,7 +132,7 @@ public function getTagsByKeywordCount(string $keyword, string $language, bool $u * * @return \Netgen\TagsBundle\API\Repository\Values\Tags\SearchResult */ - public function searchTags(string $searchString, string $language, bool $useAlwaysAvailable = true, int $offset = 0, int $limit = -1): SearchResult; + public function searchTags(string $searchString, string $language, bool $useAlwaysAvailable = true, int $offset = 0, int $limit = -1, ?bool $showHiddenTags = null): SearchResult; /** * Loads synonyms of a tag object. @@ -148,7 +148,7 @@ public function searchTags(string $searchString, string $language, bool $useAlwa * * @return \Netgen\TagsBundle\API\Repository\Values\Tags\TagList */ - public function loadTagSynonyms(Tag $tag, int $offset = 0, int $limit = -1, ?array $languages = null, bool $useAlwaysAvailable = true): TagList; + public function loadTagSynonyms(Tag $tag, int $offset = 0, int $limit = -1, ?array $languages = null, bool $useAlwaysAvailable = true, ?bool $showHiddenTags = null): TagList; /** * Returns the number of synonyms of a tag object. @@ -162,7 +162,7 @@ public function loadTagSynonyms(Tag $tag, int $offset = 0, int $limit = -1, ?arr * * @return int */ - public function getTagSynonymCount(Tag $tag, ?array $languages = null, bool $useAlwaysAvailable = true): int; + public function getTagSynonymCount(Tag $tag, ?array $languages = null, bool $useAlwaysAvailable = true, ?bool $showHiddenTags = null): int; /** * Loads content related to $tag. @@ -222,6 +222,8 @@ public function addSynonym(SynonymCreateStruct $synonymCreateStruct): Tag; /** * Converts $tag to a synonym of $mainTag. * + * If $tag was hidden, it will remain hidden after converting + * * @throws \Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException If either of specified tags is not found * @throws \Ibexa\Contracts\Core\Repository\Exceptions\UnauthorizedException If the current user is not allowed to convert tag to synonym * @throws \Ibexa\Contracts\Core\Repository\Exceptions\InvalidArgumentException If either one of the tags is a synonym @@ -286,6 +288,26 @@ public function newSynonymCreateStruct(int $mainTagId, string $mainLanguageCode) */ public function newTagUpdateStruct(): TagUpdateStruct; + /** + * Hides $tag. + * + * If $tag is a synonym, only the synonym is hidden. + * + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\UnauthorizedException If the current user is not allowed to hide this tag + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException If the specified tag is not found + */ + public function hideTag(Tag $tag): void; + + /** + * Reveal $tag. + * + * If $tag is a synonym, only the synonym is revealed. + * + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\UnauthorizedException If the current user is not allowed to reveal this tag + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException If the specified tag is not found + */ + public function revealTag(Tag $tag): void; + /** * Allows tags API execution to be performed with full access sand-boxed. * diff --git a/bundle/API/Repository/Values/Tags/Tag.php b/bundle/API/Repository/Values/Tags/Tag.php index 99a6714e..f2aade6e 100644 --- a/bundle/API/Repository/Values/Tags/Tag.php +++ b/bundle/API/Repository/Values/Tags/Tag.php @@ -28,6 +28,8 @@ * @property-read bool $alwaysAvailable Indicates if the Tag object is shown in the main language if it is not present in an other requested language * @property-read string $mainLanguageCode The main language code of the Tag object * @property-read string[] $languageCodes List of languages in this Tag object + * @property-read bool $isHidden Indicates if the Tag object is visible or not + * @property-read bool $isInvisible Indicates if the Tag object is located under another hidden Tag object */ final class Tag extends ValueObject { @@ -104,6 +106,17 @@ final class Tag extends ValueObject */ protected ?string $prioritizedLanguageCode; + /** + * Indicates that the Tag is hidden. + */ + protected bool $isHidden; + + /** + * Indicates that the Tag object is not visible, being either hidden itself, + * or implicitly hidden by parent or ancestor Tag object. + */ + protected bool $isInvisible; + /** * Construct object optionally with a set of properties. * diff --git a/bundle/Controller/Admin/FieldController.php b/bundle/Controller/Admin/FieldController.php index 50c7ac24..4e0b74da 100644 --- a/bundle/Controller/Admin/FieldController.php +++ b/bundle/Controller/Admin/FieldController.php @@ -37,6 +37,10 @@ public function autoCompleteAction(Request $request): JsonResponse $searchResult = $this->tagsService->searchTags( $request->query->get('searchString') ?? '', $request->query->get('locale') ?? '', + true, + 0, + -1, + $this->configResolver->getParameter('autocomplete_provide_hidden_tags', 'netgen_tags'), ); $data = $data = $this->filterTags($searchResult->tags, $subTreeLimit, $hideRootTag); diff --git a/bundle/Controller/Admin/TagController.php b/bundle/Controller/Admin/TagController.php index 7d9030df..fdc47bdf 100644 --- a/bundle/Controller/Admin/TagController.php +++ b/bundle/Controller/Admin/TagController.php @@ -11,7 +11,7 @@ use Netgen\TagsBundle\Core\Pagination\Pagerfanta\SearchTagsAdapter; use Netgen\TagsBundle\Form\Type\CopyTagsType; use Netgen\TagsBundle\Form\Type\LanguageSelectType; -use Netgen\TagsBundle\Form\Type\MoveTagsType; +use Netgen\TagsBundle\Form\Type\MultiselectTagsType; use Netgen\TagsBundle\Form\Type\TagConvertType; use Netgen\TagsBundle\Form\Type\TagCreateType; use Netgen\TagsBundle\Form\Type\TagMergeType; @@ -500,9 +500,79 @@ public function childrenAction(Request $request, ?Tag $tag = null): Response ); } + if ($request->request->has('HideTagsAction')) { + return $this->redirectToRoute( + 'netgen_tags_admin_tag_hide_tags', + [ + 'parentId' => $tag?->id ?? 0, + ], + ); + } + + if ($request->request->has('RevealTagsAction')) { + return $this->redirectToRoute( + 'netgen_tags_admin_tag_reveal_tags', + [ + 'parentId' => $tag?->id ?? 0, + ], + ); + } + return $this->redirect($request->getPathInfo()); } + public function hideAction(Request $request, Tag $tag): Response + { + $this->denyAccessUnlessGranted('ibexa:tags:hide'); + + if ($request->request->has('HideTagButton')) { + if (!$this->isCsrfTokenValid('netgen_tags_admin', (string) ($request->request->get('_csrf_token') ?? ''))) { + $this->addFlashMessage('errors', 'invalid_csrf_token'); + + return $this->redirectToTag($tag); + } + + $this->tagsService->hideTag($tag); + + $this->addFlashMessage('success', 'tag_hidden', ['%tagKeyword%' => $tag->keyword]); + + return $this->redirectToTag($tag); + } + + return $this->render( + '@NetgenTags/admin/tag/hide.html.twig', + [ + 'tag' => $tag, + ], + ); + } + + public function revealAction(Request $request, Tag $tag): Response + { + $this->denyAccessUnlessGranted('ibexa:tags:hide'); + + if ($request->request->has('RevealTagButton')) { + if (!$this->isCsrfTokenValid('netgen_tags_admin', (string) ($request->request->get('_csrf_token') ?? ''))) { + $this->addFlashMessage('errors', 'invalid_csrf_token'); + + return $this->redirectToTag($tag); + } + + $this->tagsService->revealTag($tag); + + $this->addFlashMessage('success', 'tag_revealed', ['%tagKeyword%' => $tag->keyword]); + + return $this->redirectToTag($tag); + } + + return $this->render( + '@NetgenTags/admin/tag/reveal.html.twig', + [ + 'tag' => $tag, + ], + ); + } + /** * This method is called from a form containing all children tags of a tag. * It shows a confirmation view. @@ -524,13 +594,14 @@ public function moveTagsAction(Request $request, ?Tag $parentTag = null): Respon } $form = $this->createForm( - MoveTagsType::class, + MultiselectTagsType::class, [ 'parentTag' => $parentTag instanceof Tag ? $parentTag->id : 0, ], [ 'tags' => $tags, 'action' => $request->getPathInfo(), + 'show_parent_field' => true, ], ); @@ -666,6 +737,110 @@ public function deleteTagsAction(Request $request, ?Tag $parentTag = null): Resp ); } + public function hideTagsAction(Request $request, ?Tag $parentTag = null): Response + { + $this->denyAccessUnlessGranted('ibexa:tags:hide'); + + $tagIds = (array) $request->request->get( + 'Tags', + $request->hasSession() ? $request->getSession()->get('ngtags_tag_ids') : [], + ); + + if (count($tagIds) === 0) { + return $this->redirectToTag($parentTag); + } + + $tags = []; + foreach ($tagIds as $tagId) { + $tags[] = $this->tagsService->loadTag((int) $tagId); + } + + $form = $this->createForm( + MultiselectTagsType::class, + [ + 'parentTag' => $parentTag instanceof Tag ? $parentTag->id : 0, + ], + [ + 'tags' => $tags, + 'action' => $request->getPathInfo(), + 'show_parent_field' => false, + ], + ); + + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + foreach ($tags as $tagObject) { + $this->tagsService->hideTag($tagObject); + } + + $this->addFlashMessage('success', 'tags_hidden'); + + return $this->redirectToTag($parentTag); + } + + return $this->render( + '@NetgenTags/admin/tag/hide_tags.html.twig', + [ + 'parentTag' => $parentTag, + 'tags' => $tags, + 'form' => $form->createView(), + ], + ); + } + + public function revealTagsAction(Request $request, ?Tag $parentTag = null): Response + { + $this->denyAccessUnlessGranted('ibexa:tags:hide'); + + $tagIds = (array) $request->request->get( + 'Tags', + $request->hasSession() ? $request->getSession()->get('ngtags_tag_ids') : [], + ); + + if (count($tagIds) === 0) { + return $this->redirectToTag($parentTag); + } + + $tags = []; + foreach ($tagIds as $tagId) { + $tags[] = $this->tagsService->loadTag((int) $tagId); + } + + $form = $this->createForm( + MultiselectTagsType::class, + [ + 'parentTag' => $parentTag instanceof Tag ? $parentTag->id : 0, + ], + [ + 'tags' => $tags, + 'action' => $request->getPathInfo(), + 'show_parent_field' => false, + ], + ); + + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + foreach ($tags as $tagObject) { + $this->tagsService->revealTag($tagObject); + } + + $this->addFlashMessage('success', 'tags_revealed'); + + return $this->redirectToTag($parentTag); + } + + return $this->render( + '@NetgenTags/admin/tag/reveal_tags.html.twig', + [ + 'parentTag' => $parentTag, + 'tags' => $tags, + 'form' => $form->createView(), + ], + ); + } + public function searchTagsAction(Request $request): Response { $this->denyAccessUnlessGranted('ibexa:tags:read'); diff --git a/bundle/Controller/Admin/TreeController.php b/bundle/Controller/Admin/TreeController.php index e74a32d0..51b56c91 100644 --- a/bundle/Controller/Admin/TreeController.php +++ b/bundle/Controller/Admin/TreeController.php @@ -12,6 +12,7 @@ use Symfony\Contracts\Translation\TranslatorInterface; use function htmlspecialchars; +use function mb_strtolower; use function str_replace; use const ENT_HTML401; @@ -44,6 +45,8 @@ public function __construct( 'merge_tag' => $this->translator->trans('tag.tree.merge_tag', [], 'netgen_tags_admin'), 'convert_tag' => $this->translator->trans('tag.tree.convert_tag', [], 'netgen_tags_admin'), 'add_synonym' => $this->translator->trans('tag.tree.add_synonym', [], 'netgen_tags_admin'), + 'hide_tag' => $this->translator->trans('tag.tree.hide_tag', [], 'netgen_tags_admin'), + 'reveal_tag' => $this->translator->trans('tag.tree.reveal_tag', [], 'netgen_tags_admin'), ]; $this->treeLinks = [ @@ -55,6 +58,8 @@ public function __construct( 'merge_tag' => $this->router->generate('netgen_tags_admin_tag_merge', ['tagId' => ':tagId']), 'convert_tag' => $this->router->generate('netgen_tags_admin_tag_convert', ['tagId' => ':tagId']), 'add_synonym' => $this->router->generate('netgen_tags_admin_synonym_add_select', ['mainTagId' => ':mainTagId']), + 'hide_tag' => $this->router->generate('netgen_tags_admin_tag_hide', ['tagId' => ':tagId']), + 'reveal_tag' => $this->router->generate('netgen_tags_admin_tag_reveal', ['tagId' => ':tagId']), ]; } @@ -119,13 +124,13 @@ private function getRootTreeData(): array */ private function getTagTreeData(Tag $tag, bool $isRoot = false): array { - $synonymCount = $this->tagsService->getTagSynonymCount($tag); - return [ 'id' => $tag->id, 'parent' => $isRoot ? '#' : $tag->parentTagId, - 'text' => $synonymCount > 0 ? $this->escape($tag->keyword) . ' (+' . $synonymCount . ')' : $this->escape($tag->keyword), + 'text' => $this->formatTagTreeText($tag), 'children' => $this->tagsService->getTagChildrenCount($tag) > 0, + 'hidden' => $tag->isHidden, + 'invisible' => $tag->isInvisible, 'a_attr' => [ 'href' => str_replace(':tagId', (string) $tag->id, $this->treeLinks['show_tag']), 'rel' => $tag->id, @@ -165,6 +170,16 @@ private function getTagTreeData(Tag $tag, bool $isRoot = false): array 'url' => str_replace(':tagId', (string) $tag->id, $this->treeLinks['convert_tag']), 'text' => $this->treeLabels['convert_tag'], ], + [ + 'name' => 'hide_tag', + 'url' => str_replace(':tagId', (string) $tag->id, $this->treeLinks['hide_tag']), + 'text' => $this->treeLabels['hide_tag'], + ], + [ + 'name' => 'reveal_tag', + 'url' => str_replace(':tagId', (string) $tag->id, $this->treeLinks['reveal_tag']), + 'text' => $this->treeLabels['reveal_tag'], + ], ], ], ]; @@ -174,4 +189,23 @@ private function escape(string $string): string { return htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401, 'UTF-8'); } + + private function formatTagTreeText(Tag $tag): string + { + $synonymCount = $this->tagsService->getTagSynonymCount($tag); + + $result = $tag->keyword; + + if ($tag->isHidden) { + $result .= ' (' . mb_strtolower($this->translator->trans('tag.hidden', [], 'netgen_tags_admin')) . ')'; + } elseif ($tag->isInvisible) { + $result .= ' (' . $this->translator->trans('tag.hidden_by_parent', [], 'netgen_tags_admin') . ')'; + } + + if ($synonymCount > 0) { + $result .= ' (+' . $synonymCount . ')'; + } + + return $this->escape($result); + } } diff --git a/bundle/Controller/TagViewController.php b/bundle/Controller/TagViewController.php index 1ae15b3e..3ca17483 100644 --- a/bundle/Controller/TagViewController.php +++ b/bundle/Controller/TagViewController.php @@ -14,6 +14,10 @@ final class TagViewController extends Controller */ public function viewAction(TagView $view): TagView { + if ($view->getTag()->isInvisible) { + throw $this->createNotFoundException(); + } + return $view; } } diff --git a/bundle/Core/Event/TagsService.php b/bundle/Core/Event/TagsService.php index 816a3470..21a380d9 100644 --- a/bundle/Core/Event/TagsService.php +++ b/bundle/Core/Event/TagsService.php @@ -38,39 +38,39 @@ public function loadTagByUrl(string $url, array $languages): Tag return $this->service->loadTagByUrl($url, $languages); } - public function loadTagChildren(?Tag $tag = null, int $offset = 0, int $limit = -1, ?array $languages = null, bool $useAlwaysAvailable = true): TagList + public function loadTagChildren(?Tag $tag = null, int $offset = 0, int $limit = -1, ?array $languages = null, bool $useAlwaysAvailable = true, ?bool $showHiddenTags = null): TagList { - return $this->service->loadTagChildren($tag, $offset, $limit, $languages, $useAlwaysAvailable); + return $this->service->loadTagChildren($tag, $offset, $limit, $languages, $useAlwaysAvailable, $showHiddenTags); } - public function getTagChildrenCount(?Tag $tag = null, ?array $languages = null, bool $useAlwaysAvailable = true): int + public function getTagChildrenCount(?Tag $tag = null, ?array $languages = null, bool $useAlwaysAvailable = true, ?bool $showHiddenTags = null): int { - return $this->service->getTagChildrenCount($tag, $languages, $useAlwaysAvailable); + return $this->service->getTagChildrenCount($tag, $languages, $useAlwaysAvailable, $showHiddenTags); } - public function loadTagsByKeyword(string $keyword, string $language, bool $useAlwaysAvailable = true, int $offset = 0, int $limit = -1): TagList + public function loadTagsByKeyword(string $keyword, string $language, bool $useAlwaysAvailable = true, int $offset = 0, int $limit = -1, ?bool $showHiddenTags = null): TagList { - return $this->service->loadTagsByKeyword($keyword, $language, $useAlwaysAvailable, $offset, $limit); + return $this->service->loadTagsByKeyword($keyword, $language, $useAlwaysAvailable, $offset, $limit, $showHiddenTags); } - public function getTagsByKeywordCount(string $keyword, string $language, bool $useAlwaysAvailable = true): int + public function getTagsByKeywordCount(string $keyword, string $language, bool $useAlwaysAvailable = true, ?bool $showHiddenTags = null): int { - return $this->service->getTagsByKeywordCount($keyword, $language, $useAlwaysAvailable); + return $this->service->getTagsByKeywordCount($keyword, $language, $useAlwaysAvailable, $showHiddenTags); } - public function searchTags(string $searchString, string $language, bool $useAlwaysAvailable = true, int $offset = 0, int $limit = -1): SearchResult + public function searchTags(string $searchString, string $language, bool $useAlwaysAvailable = true, int $offset = 0, int $limit = -1, ?bool $showHiddenTags = null): SearchResult { - return $this->service->searchTags($searchString, $language, $useAlwaysAvailable, $offset, $limit); + return $this->service->searchTags($searchString, $language, $useAlwaysAvailable, $offset, $limit, $showHiddenTags); } - public function loadTagSynonyms(Tag $tag, int $offset = 0, int $limit = -1, ?array $languages = null, bool $useAlwaysAvailable = true): TagList + public function loadTagSynonyms(Tag $tag, int $offset = 0, int $limit = -1, ?array $languages = null, bool $useAlwaysAvailable = true, ?bool $showHiddenTags = null): TagList { - return $this->service->loadTagSynonyms($tag, $offset, $limit, $languages, $useAlwaysAvailable); + return $this->service->loadTagSynonyms($tag, $offset, $limit, $languages, $useAlwaysAvailable, $showHiddenTags); } - public function getTagSynonymCount(Tag $tag, ?array $languages = null, bool $useAlwaysAvailable = true): int + public function getTagSynonymCount(Tag $tag, ?array $languages = null, bool $useAlwaysAvailable = true, ?bool $showHiddenTags = null): int { - return $this->service->getTagSynonymCount($tag, $languages, $useAlwaysAvailable); + return $this->service->getTagSynonymCount($tag, $languages, $useAlwaysAvailable, $showHiddenTags); } public function getRelatedContent(Tag $tag, int $offset = 0, int $limit = -1, bool $returnContentInfo = true, array $additionalCriteria = [], array $sortClauses = []): array @@ -226,6 +226,32 @@ public function newTagUpdateStruct(): TagUpdateStruct return $this->service->newTagUpdateStruct(); } + public function hideTag(Tag $tag): void + { + $beforeEvent = new Events\BeforeHideTagEvent($tag); + + if ($this->eventDispatcher->dispatch($beforeEvent)->isPropagationStopped()) { + return; + } + + $this->service->hideTag($tag); + + $this->eventDispatcher->dispatch(new Events\HideTagEvent($tag)); + } + + public function revealTag(Tag $tag): void + { + $beforeEvent = new Events\BeforeRevealTagEvent($tag); + + if ($this->eventDispatcher->dispatch($beforeEvent)->isPropagationStopped()) { + return; + } + + $this->service->revealTag($tag); + + $this->eventDispatcher->dispatch(new Events\RevealTagEvent($tag)); + } + public function sudo(callable $callback, ?TagsServiceInterface $outerTagsService = null): mixed { return $this->service->sudo($callback, $outerTagsService ?? $this); diff --git a/bundle/Core/Persistence/Cache/TagsHandler.php b/bundle/Core/Persistence/Cache/TagsHandler.php index eadecf1c..ef2dd1e9 100644 --- a/bundle/Core/Persistence/Cache/TagsHandler.php +++ b/bundle/Core/Persistence/Cache/TagsHandler.php @@ -155,42 +155,42 @@ public function loadTagByKeywordAndParentId(string $keyword, int $parentTagId, ? return $this->tagsHandler->loadTagByKeywordAndParentId($keyword, $parentTagId, $translations, $useAlwaysAvailable); } - public function loadChildren(int $tagId, int $offset = 0, int $limit = -1, ?array $translations = null, bool $useAlwaysAvailable = true): array + public function loadChildren(int $tagId, int $offset = 0, int $limit = -1, ?array $translations = null, bool $useAlwaysAvailable = true, ?bool $showHiddenTags = null): array { $this->logger->logCall(__METHOD__, ['tag' => $tagId, 'translations' => $translations, 'useAlwaysAvailable' => $useAlwaysAvailable]); - return $this->tagsHandler->loadChildren($tagId, $offset, $limit, $translations, $useAlwaysAvailable); + return $this->tagsHandler->loadChildren($tagId, $offset, $limit, $translations, $useAlwaysAvailable, $showHiddenTags); } - public function getChildrenCount(int $tagId, ?array $translations = null, bool $useAlwaysAvailable = true): int + public function getChildrenCount(int $tagId, ?array $translations = null, bool $useAlwaysAvailable = true, ?bool $showHiddenTags = null): int { $this->logger->logCall(__METHOD__, ['tag' => $tagId, 'translations' => $translations, 'useAlwaysAvailable' => $useAlwaysAvailable]); - return $this->tagsHandler->getChildrenCount($tagId, $translations, $useAlwaysAvailable); + return $this->tagsHandler->getChildrenCount($tagId, $translations, $useAlwaysAvailable, $showHiddenTags); } - public function loadTagsByKeyword(string $keyword, string $translation, bool $useAlwaysAvailable = true, int $offset = 0, int $limit = -1): array + public function loadTagsByKeyword(string $keyword, string $translation, bool $useAlwaysAvailable = true, int $offset = 0, int $limit = -1, ?bool $showHiddenTags = null): array { $this->logger->logCall(__METHOD__, ['keyword' => $keyword, 'translation' => $translation, 'useAlwaysAvailable' => $useAlwaysAvailable]); - return $this->tagsHandler->loadTagsByKeyword($keyword, $translation, $useAlwaysAvailable, $offset, $limit); + return $this->tagsHandler->loadTagsByKeyword($keyword, $translation, $useAlwaysAvailable, $offset, $limit, $showHiddenTags); } - public function getTagsByKeywordCount(string $keyword, string $translation, bool $useAlwaysAvailable = true): int + public function getTagsByKeywordCount(string $keyword, string $translation, bool $useAlwaysAvailable = true, ?bool $showHiddenTags = null): int { $this->logger->logCall(__METHOD__, ['keyword' => $keyword, 'translation' => $translation, 'useAlwaysAvailable' => $useAlwaysAvailable]); - return $this->tagsHandler->getTagsByKeywordCount($keyword, $translation, $useAlwaysAvailable); + return $this->tagsHandler->getTagsByKeywordCount($keyword, $translation, $useAlwaysAvailable, $showHiddenTags); } - public function searchTags(string $searchString, string $translation, bool $useAlwaysAvailable = true, int $offset = 0, int $limit = -1): SearchResult + public function searchTags(string $searchString, string $translation, bool $useAlwaysAvailable = true, int $offset = 0, int $limit = -1, ?bool $showHiddenTags = null): SearchResult { $this->logger->logCall(__METHOD__, ['searchString' => $searchString, 'translation' => $translation, 'useAlwaysAvailable' => $useAlwaysAvailable]); - return $this->tagsHandler->searchTags($searchString, $translation, $useAlwaysAvailable, $offset, $limit); + return $this->tagsHandler->searchTags($searchString, $translation, $useAlwaysAvailable, $offset, $limit, $showHiddenTags); } - public function loadSynonyms(int $tagId, int $offset = 0, int $limit = -1, ?array $translations = null, bool $useAlwaysAvailable = true): array + public function loadSynonyms(int $tagId, int $offset = 0, int $limit = -1, ?array $translations = null, bool $useAlwaysAvailable = true, ?bool $showHiddenTags = null): array { // Method caches all synonyms in cache and only uses offset / limit to slice the cached result $translationsKey = count($translations ?? []) === 0 @@ -208,7 +208,7 @@ public function loadSynonyms(int $tagId, int $offset = 0, int $limit = -1, ?arra $this->logger->logCall(__METHOD__, ['tag' => $tagId, 'translations' => $translations, 'useAlwaysAvailable' => $useAlwaysAvailable]); $tagInfo = $this->loadTagInfo($tagId); - $synonyms = $this->tagsHandler->loadSynonyms($tagId, 0, -1, $translations, $useAlwaysAvailable); + $synonyms = $this->tagsHandler->loadSynonyms($tagId, 0, -1, $translations, $useAlwaysAvailable, $showHiddenTags); $cacheItem->set($synonyms); $cacheTags = [$this->getCacheTags($tagInfo->id, $tagInfo->pathString)]; @@ -221,11 +221,11 @@ public function loadSynonyms(int $tagId, int $offset = 0, int $limit = -1, ?arra return array_slice($synonyms, $offset, $limit > -1 ? $limit : null); } - public function getSynonymCount(int $tagId, ?array $translations = null, bool $useAlwaysAvailable = true): int + public function getSynonymCount(int $tagId, ?array $translations = null, bool $useAlwaysAvailable = true, ?bool $showHiddenTags = null): int { $this->logger->logCall(__METHOD__, ['tag' => $tagId, 'translations' => $translations, 'useAlwaysAvailable' => $useAlwaysAvailable]); - return $this->tagsHandler->getSynonymCount($tagId, $translations, $useAlwaysAvailable); + return $this->tagsHandler->getSynonymCount($tagId, $translations, $useAlwaysAvailable, $showHiddenTags); } public function create(CreateStruct $createStruct): Tag @@ -304,6 +304,22 @@ public function deleteTag(int $tagId): void $this->cache->invalidateTags(['tag-path-' . $tagId]); } + public function hideTag(int $tagId): void + { + $this->logger->logCall(__METHOD__, ['tag' => $tagId]); + $this->tagsHandler->hideTag($tagId); + + $this->cache->invalidateTags(['tag-path-' . $tagId]); + } + + public function revealTag(int $tagId): void + { + $this->logger->logCall(__METHOD__, ['tag' => $tagId]); + $this->tagsHandler->revealTag($tagId); + + $this->cache->invalidateTags(['tag-path-' . $tagId]); + } + /** * Return relevant cache tags so cache can be purged reliably. * diff --git a/bundle/Core/Persistence/Legacy/Tags/Gateway.php b/bundle/Core/Persistence/Legacy/Tags/Gateway.php index 9ad839d1..184c88e7 100644 --- a/bundle/Core/Persistence/Legacy/Tags/Gateway.php +++ b/bundle/Core/Persistence/Legacy/Tags/Gateway.php @@ -40,36 +40,36 @@ abstract public function getFullTagDataByKeywordAndParentId(string $keyword, int * * If $limit = -1 all children starting at $offset are returned. */ - abstract public function getChildren(int $tagId, int $offset = 0, int $limit = -1, ?array $translations = null, bool $useAlwaysAvailable = true): array; + abstract public function getChildren(int $tagId, int $offset = 0, int $limit = -1, ?array $translations = null, bool $useAlwaysAvailable = true, ?bool $showHiddenTags = null): array; /** * Returns how many tags exist below tag identified by $tagId. */ - abstract public function getChildrenCount(int $tagId, ?array $translations = null, bool $useAlwaysAvailable = true): int; + abstract public function getChildrenCount(int $tagId, ?array $translations = null, bool $useAlwaysAvailable = true, ?bool $showHiddenTags = null): int; /** * Returns data for tags identified by given $keyword. * * If $limit = -1 all tags starting at $offset are returned. */ - abstract public function getTagsByKeyword(string $keyword, string $translation, bool $useAlwaysAvailable = true, bool $exactMatch = true, int $offset = 0, int $limit = -1): array; + abstract public function getTagsByKeyword(string $keyword, string $translation, bool $useAlwaysAvailable = true, bool $exactMatch = true, int $offset = 0, int $limit = -1, ?bool $showHiddenTags = null): array; /** * Returns how many tags exist with $keyword. */ - abstract public function getTagsByKeywordCount(string $keyword, string $translation, bool $useAlwaysAvailable = true, bool $exactMatch = true): int; + abstract public function getTagsByKeywordCount(string $keyword, string $translation, bool $useAlwaysAvailable = true, ?bool $showHiddenTags = null, bool $exactMatch = true): int; /** * Returns data for synonyms of the tag identified by given $tagId. * * If $limit = -1 all synonyms starting at $offset are returned. */ - abstract public function getSynonyms(int $tagId, int $offset = 0, int $limit = -1, ?array $translations = null, bool $useAlwaysAvailable = true): array; + abstract public function getSynonyms(int $tagId, int $offset = 0, int $limit = -1, ?array $translations = null, bool $useAlwaysAvailable = true, ?bool $showHiddenTags = null): array; /** * Returns how many synonyms exist for a tag identified by $tagId. */ - abstract public function getSynonymCount(int $tagId, ?array $translations = null, bool $useAlwaysAvailable = true): int; + abstract public function getSynonymCount(int $tagId, ?array $translations = null, bool $useAlwaysAvailable = true, ?bool $showHiddenTags = null): int; /** * Moves the synonym identified by $synonymId to tag identified by $mainTagData. @@ -112,4 +112,14 @@ abstract public function moveSubtree(array $sourceTagData, ?array $destinationPa * If $tagId is a synonym, only the synonym is deleted. */ abstract public function deleteTag(int $tagId): void; + + /** + * Hides tag identified by $tagId. + */ + abstract public function hideTag(int $tagId): void; + + /** + * Reveals tag identified by $tagId. + */ + abstract public function revealTag(int $tagId): void; } diff --git a/bundle/Core/Persistence/Legacy/Tags/Gateway/DoctrineDatabase.php b/bundle/Core/Persistence/Legacy/Tags/Gateway/DoctrineDatabase.php index 02a857dd..8ff19be7 100644 --- a/bundle/Core/Persistence/Legacy/Tags/Gateway/DoctrineDatabase.php +++ b/bundle/Core/Persistence/Legacy/Tags/Gateway/DoctrineDatabase.php @@ -126,9 +126,16 @@ public function getFullTagDataByKeywordAndParentId(string $keyword, int $parentI return $query->execute()->fetchAll(FetchMode::ASSOCIATIVE); } - public function getChildren(int $tagId, int $offset = 0, int $limit = -1, ?array $translations = null, bool $useAlwaysAvailable = true): array + public function getChildren(int $tagId, int $offset = 0, int $limit = -1, ?array $translations = null, bool $useAlwaysAvailable = true, ?bool $showHiddenTags = null): array { $tagIdsQuery = $this->createTagIdsQuery($translations, $useAlwaysAvailable); + + if ($showHiddenTags !== null && $showHiddenTags === false) { + $tagIdsQuery->andWhere( + $tagIdsQuery->expr()->neq('is_invisible', '1'), + ); + } + $tagIdsQuery->andWhere( $tagIdsQuery->expr()->andX( $tagIdsQuery->expr()->eq( @@ -154,6 +161,13 @@ public function getChildren(int $tagId, int $offset = 0, int $limit = -1, ?array } $query = $this->createTagFindQuery($translations, $useAlwaysAvailable); + + if ($showHiddenTags !== null && $showHiddenTags === false) { + $query->andWhere( + $query->expr()->neq('is_invisible', '1'), + ); + } + $query->andWhere( $query->expr()->in( 'eztags.id', @@ -166,9 +180,16 @@ public function getChildren(int $tagId, int $offset = 0, int $limit = -1, ?array return $query->execute()->fetchAll(FetchMode::ASSOCIATIVE); } - public function getChildrenCount(int $tagId, ?array $translations = null, bool $useAlwaysAvailable = true): int + public function getChildrenCount(int $tagId, ?array $translations = null, bool $useAlwaysAvailable = true, ?bool $showHiddenTags = null): int { $query = $this->createTagCountQuery($translations, $useAlwaysAvailable); + + if ($showHiddenTags !== null && $showHiddenTags === false) { + $query->andWhere( + $query->expr()->neq('is_invisible', '1'), + ); + } + $query->andWhere( $query->expr()->andX( $query->expr()->eq( @@ -184,11 +205,17 @@ public function getChildrenCount(int $tagId, ?array $translations = null, bool $ return (int) $rows[0]['count']; } - public function getTagsByKeyword(string $keyword, string $translation, bool $useAlwaysAvailable = true, bool $exactMatch = true, int $offset = 0, int $limit = -1): array + public function getTagsByKeyword(string $keyword, string $translation, bool $useAlwaysAvailable = true, bool $exactMatch = true, int $offset = 0, int $limit = -1, ?bool $showHiddenTags = null): array { $databasePlatform = $this->connection->getDatabasePlatform(); $tagIdsQuery = $this->createTagIdsQuery([$translation], $useAlwaysAvailable); + if ($showHiddenTags !== null && $showHiddenTags === false) { + $tagIdsQuery->andWhere( + $tagIdsQuery->expr()->neq('is_invisible', '1'), + ); + } + $tagIdsQuery->andWhere( $exactMatch ? $tagIdsQuery->expr()->eq( @@ -222,6 +249,12 @@ public function getTagsByKeyword(string $keyword, string $translation, bool $use $query = $this->createTagFindQuery([$translation], $useAlwaysAvailable); + if ($showHiddenTags !== null && $showHiddenTags === false) { + $query->andWhere( + $query->expr()->neq('is_invisible', '1'), + ); + } + $query->andWhere( $query->expr()->in( 'eztags.id', @@ -234,11 +267,17 @@ public function getTagsByKeyword(string $keyword, string $translation, bool $use return $query->execute()->fetchAll(FetchMode::ASSOCIATIVE); } - public function getTagsByKeywordCount(string $keyword, string $translation, bool $useAlwaysAvailable = true, bool $exactMatch = true): int + public function getTagsByKeywordCount(string $keyword, string $translation, bool $useAlwaysAvailable = true, ?bool $showHiddenTags = null, bool $exactMatch = true): int { $databasePlatform = $this->connection->getDatabasePlatform(); $query = $this->createTagCountQuery([$translation, $useAlwaysAvailable]); + if ($showHiddenTags !== null && $showHiddenTags === false) { + $query->andWhere( + $query->expr()->neq('is_invisible', '1'), + ); + } + $query->andWhere( $exactMatch ? $query->expr()->eq( @@ -260,9 +299,16 @@ public function getTagsByKeywordCount(string $keyword, string $translation, bool return (int) $rows[0]['count']; } - public function getSynonyms(int $tagId, int $offset = 0, int $limit = -1, ?array $translations = null, bool $useAlwaysAvailable = true): array + public function getSynonyms(int $tagId, int $offset = 0, int $limit = -1, ?array $translations = null, bool $useAlwaysAvailable = true, ?bool $showHiddenTags = null): array { $tagIdsQuery = $this->createTagIdsQuery($translations, $useAlwaysAvailable); + + if ($showHiddenTags !== null && $showHiddenTags === false) { + $tagIdsQuery->andWhere( + $tagIdsQuery->expr()->neq('is_invisible', '1'), + ); + } + $tagIdsQuery->andWhere( $tagIdsQuery->expr()->eq( 'eztags.main_tag_id', @@ -284,6 +330,13 @@ public function getSynonyms(int $tagId, int $offset = 0, int $limit = -1, ?array } $query = $this->createTagFindQuery($translations, $useAlwaysAvailable); + + if ($showHiddenTags !== null && $showHiddenTags === false) { + $query->andWhere( + $query->expr()->neq('is_invisible', '1'), + ); + } + $query->andWhere( $query->expr()->in( 'eztags.id', @@ -294,9 +347,16 @@ public function getSynonyms(int $tagId, int $offset = 0, int $limit = -1, ?array return $query->execute()->fetchAll(FetchMode::ASSOCIATIVE); } - public function getSynonymCount(int $tagId, ?array $translations = null, bool $useAlwaysAvailable = true): int + public function getSynonymCount(int $tagId, ?array $translations = null, bool $useAlwaysAvailable = true, ?bool $showHiddenTags = null): int { $query = $this->createTagCountQuery($translations, $useAlwaysAvailable); + + if ($showHiddenTags !== null && $showHiddenTags === false) { + $query->andWhere( + $query->expr()->neq('is_invisible', '1'), + ); + } + $query->andWhere( $query->expr()->eq( 'eztags.main_tag_id', @@ -372,7 +432,14 @@ public function create(CreateStruct $createStruct, ?array $parentTag = null): in )->setValue( 'language_mask', ':language_mask', - )->setParameter('parent_id', $parentTag !== null ? (int) $parentTag['id'] : 0, Types::INTEGER) + )->setValue( + 'is_hidden', + ':is_hidden', + )->setValue( + 'is_invisible', + ':is_invisible', + ) + ->setParameter('parent_id', $parentTag !== null ? (int) $parentTag['id'] : 0, Types::INTEGER) ->setParameter('main_tag_id', 0, Types::INTEGER) ->setParameter('modified', time(), Types::INTEGER) ->setParameter('keyword', $createStruct->keywords[$createStruct->mainLanguageCode], Types::STRING) @@ -392,7 +459,8 @@ public function create(CreateStruct $createStruct, ?array $parentTag = null): in is_bool($createStruct->alwaysAvailable) ? $createStruct->alwaysAvailable : true, ), Types::INTEGER, - ); + )->setParameter('is_hidden', 0, Types::INTEGER) + ->setParameter('is_invisible', $parentTag !== null ? $parentTag['is_invisible'] : 0, Types::INTEGER); $query->execute(); @@ -612,6 +680,10 @@ public function convertToSynonym(int $tagId, array $mainTagData): void ->setParameter('depth', $mainTagData['depth'], Types::INTEGER) ->setParameter('path_string', $this->getSynonymPathString($tagId, $mainTagData['path_string']), Types::STRING); + if ($mainTagData['is_hidden'] === 0 && $mainTagData['is_invisible'] === 1) { + $query->set('is_invisible', '0'); + } + $query->execute(); } @@ -844,6 +916,117 @@ public function deleteTag(int $tagId): void $query->execute(); } + public function hideTag(int $tagId): void + { + $query = $this->connection->createQueryBuilder(); + $query + ->update('eztags') + ->set('is_hidden', '1') + ->where( + $query->expr()->eq('id', ':tag_id'), + )->setParameter('tag_id', $tagId, Types::INTEGER); + + $query->execute(); + + $query = $this->connection->createQueryBuilder(); + $query + ->update('eztags') + ->set('is_invisible', '1') + ->where( + $query->expr()->like('path_string', ':path_string'), + ) + ->setParameter('path_string', '%/' . $tagId . '/%', Types::STRING); + + $query->execute(); + } + + public function revealTag(int $tagId): void + { + // check if any ancestor is hidden + $basicTagData = $this->getBasicTagData($tagId); + $pathArray = explode('/', trim($basicTagData['path_string'], '/')); + array_pop($pathArray); + + $shouldRemainInvisible = false; + if ($pathArray !== [] && $basicTagData['main_tag_id'] === 0) { + $query = $this->connection->createQueryBuilder(); + $query + ->select('COUNT(id)') + ->from('eztags') + ->where($query->expr()->eq('is_hidden', '1')) + ->andWhere($query->expr()->in('id', ':parent_ids')) + ->setParameter('parent_ids', $pathArray, Connection::PARAM_INT_ARRAY); + + $hiddenAncestorsCount = (int) $query->execute()->fetch(FetchMode::COLUMN); + $shouldRemainInvisible = $hiddenAncestorsCount > 0; + } + + // update current tag to not be hidden + $query = $this->connection->createQueryBuilder(); + $query + ->update('eztags') + ->set('is_hidden', '0') + ->where( + $query->expr()->eq('id', ':tag_id'), + )->setParameter('tag_id', $tagId, Types::INTEGER); + + $query->execute(); + + // find descendant tags that should remain invisible + $query = $this->connection->createQueryBuilder(); + $query + ->select('id') + ->from('eztags') + ->where($query->expr()->eq('is_hidden', '1')) + ->andWhere( + $query->expr()->like('path_string', ':path_string'), + ) + ->setParameter('path_string', '%/' . $tagId . '/%', Types::STRING); + + $hiddenDescendantTags = array_map( + static fn (array $row) => $row['id'], + $query->execute()->fetchAll(FetchMode::ASSOCIATIVE), + ); + + $tagsToRemainInvisible = []; + if ($hiddenDescendantTags !== []) { + foreach ($hiddenDescendantTags as $hiddenDescendantTag) { + $query = $this->connection->createQueryBuilder(); + $query + ->select('id') + ->from('eztags') + ->where( + $query->expr()->like('path_string', ':path_string'), + ) + ->setParameter('path_string', '%/' . $hiddenDescendantTag . '/%', Types::STRING); + + foreach ($query->execute()->fetchAll(FetchMode::ASSOCIATIVE) as $row) { + $tagsToRemainInvisible[] = $row['id']; + } + } + } + + // if at least one ancestor is hidden, remain invisible + if ($shouldRemainInvisible !== true) { + $query = $this->connection->createQueryBuilder(); + $query + ->update('eztags') + ->set('is_invisible', '0') + ->where( + $query->expr()->like('path_string', ':path_string'), + ) + ->setParameter('path_string', '%/' . $tagId . '/%', Types::STRING); + + if ($tagsToRemainInvisible !== []) { + $query->andWhere( + $query->expr()->notIn('id', $tagsToRemainInvisible), + ); + } + + $query->execute(); + } + } + private function createTagIdsQuery(?array $translations = null, bool $useAlwaysAvailable = true): QueryBuilder { $query = $this->connection->createQueryBuilder(); @@ -930,6 +1113,8 @@ private function createTagFindQuery(?array $translations = null, bool $useAlways 'eztags.remote_id', 'eztags.main_language_id', 'eztags.language_mask', + 'eztags.is_hidden', + 'eztags.is_invisible', // Tag keywords 'eztags_keyword.keyword', 'eztags_keyword.locale', diff --git a/bundle/Core/Persistence/Legacy/Tags/Gateway/ExceptionConversion.php b/bundle/Core/Persistence/Legacy/Tags/Gateway/ExceptionConversion.php index f982ea2f..1b67ff29 100644 --- a/bundle/Core/Persistence/Legacy/Tags/Gateway/ExceptionConversion.php +++ b/bundle/Core/Persistence/Legacy/Tags/Gateway/ExceptionConversion.php @@ -71,10 +71,10 @@ public function getFullTagDataByKeywordAndParentId(string $keyword, int $parentI } } - public function getChildren(int $tagId, int $offset = 0, int $limit = -1, ?array $translations = null, bool $useAlwaysAvailable = true): array + public function getChildren(int $tagId, int $offset = 0, int $limit = -1, ?array $translations = null, bool $useAlwaysAvailable = true, ?bool $showHiddenTags = null): array { try { - return $this->innerGateway->getChildren($tagId, $offset, $limit, $translations, $useAlwaysAvailable); + return $this->innerGateway->getChildren($tagId, $offset, $limit, $translations, $useAlwaysAvailable, $showHiddenTags); } catch (DBALException $e) { throw new RuntimeException('Database error', 0, $e); } catch (PDOException $e) { @@ -82,10 +82,10 @@ public function getChildren(int $tagId, int $offset = 0, int $limit = -1, ?array } } - public function getChildrenCount(int $tagId, ?array $translations = null, bool $useAlwaysAvailable = true): int + public function getChildrenCount(int $tagId, ?array $translations = null, bool $useAlwaysAvailable = true, ?bool $showHiddenTags = null): int { try { - return $this->innerGateway->getChildrenCount($tagId, $translations, $useAlwaysAvailable); + return $this->innerGateway->getChildrenCount($tagId, $translations, $useAlwaysAvailable, $showHiddenTags); } catch (DBALException $e) { throw new RuntimeException('Database error', 0, $e); } catch (PDOException $e) { @@ -93,10 +93,10 @@ public function getChildrenCount(int $tagId, ?array $translations = null, bool $ } } - public function getTagsByKeyword(string $keyword, string $translation, bool $useAlwaysAvailable = true, bool $exactMatch = true, int $offset = 0, int $limit = -1): array + public function getTagsByKeyword(string $keyword, string $translation, bool $useAlwaysAvailable = true, bool $exactMatch = true, int $offset = 0, int $limit = -1, ?bool $showHiddenTags = null): array { try { - return $this->innerGateway->getTagsByKeyword($keyword, $translation, $useAlwaysAvailable, $exactMatch, $offset, $limit); + return $this->innerGateway->getTagsByKeyword($keyword, $translation, $useAlwaysAvailable, $exactMatch, $offset, $limit, $showHiddenTags); } catch (DBALException $e) { throw new RuntimeException('Database error', 0, $e); } catch (PDOException $e) { @@ -104,10 +104,10 @@ public function getTagsByKeyword(string $keyword, string $translation, bool $use } } - public function getTagsByKeywordCount(string $keyword, string $translation, bool $useAlwaysAvailable = true, bool $exactMatch = true): int + public function getTagsByKeywordCount(string $keyword, string $translation, bool $useAlwaysAvailable = true, ?bool $showHiddenTags = null, bool $exactMatch = true): int { try { - return $this->innerGateway->getTagsByKeywordCount($keyword, $translation, $useAlwaysAvailable, $exactMatch); + return $this->innerGateway->getTagsByKeywordCount($keyword, $translation, $useAlwaysAvailable, $showHiddenTags, $exactMatch); } catch (DBALException $e) { throw new RuntimeException('Database error', 0, $e); } catch (PDOException $e) { @@ -115,10 +115,10 @@ public function getTagsByKeywordCount(string $keyword, string $translation, bool } } - public function getSynonyms(int $tagId, int $offset = 0, int $limit = -1, ?array $translations = null, bool $useAlwaysAvailable = true): array + public function getSynonyms(int $tagId, int $offset = 0, int $limit = -1, ?array $translations = null, bool $useAlwaysAvailable = true, ?bool $showHiddenTags = null): array { try { - return $this->innerGateway->getSynonyms($tagId, $offset, $limit, $translations, $useAlwaysAvailable); + return $this->innerGateway->getSynonyms($tagId, $offset, $limit, $translations, $useAlwaysAvailable, $showHiddenTags); } catch (DBALException $e) { throw new RuntimeException('Database error', 0, $e); } catch (PDOException $e) { @@ -126,10 +126,10 @@ public function getSynonyms(int $tagId, int $offset = 0, int $limit = -1, ?array } } - public function getSynonymCount(int $tagId, ?array $translations = null, bool $useAlwaysAvailable = true): int + public function getSynonymCount(int $tagId, ?array $translations = null, bool $useAlwaysAvailable = true, ?bool $showHiddenTags = null): int { try { - return $this->innerGateway->getSynonymCount($tagId, $translations, $useAlwaysAvailable); + return $this->innerGateway->getSynonymCount($tagId, $translations, $useAlwaysAvailable, $showHiddenTags); } catch (DBALException $e) { throw new RuntimeException('Database error', 0, $e); } catch (PDOException $e) { @@ -224,4 +224,26 @@ public function deleteTag(int $tagId): void throw new RuntimeException('Database error', 0, $e); } } + + public function hideTag(int $tagId): void + { + try { + $this->innerGateway->hideTag($tagId); + } catch (DBALException $e) { + throw new RuntimeException('Database error', 0, $e); + } catch (PDOException $e) { + throw new RuntimeException('Database error', 0, $e); + } + } + + public function revealTag(int $tagId): void + { + try { + $this->innerGateway->revealTag($tagId); + } catch (DBALException $e) { + throw new RuntimeException('Database error', 0, $e); + } catch (PDOException $e) { + throw new RuntimeException('Database error', 0, $e); + } + } } diff --git a/bundle/Core/Persistence/Legacy/Tags/Handler.php b/bundle/Core/Persistence/Legacy/Tags/Handler.php index 3414804b..36d4002d 100644 --- a/bundle/Core/Persistence/Legacy/Tags/Handler.php +++ b/bundle/Core/Persistence/Legacy/Tags/Handler.php @@ -89,34 +89,34 @@ public function loadTagByKeywordAndParentId(string $keyword, int $parentTagId, ? return reset($tag); } - public function loadChildren(int $tagId, int $offset = 0, int $limit = -1, ?array $translations = null, bool $useAlwaysAvailable = true): array + public function loadChildren(int $tagId, int $offset = 0, int $limit = -1, ?array $translations = null, bool $useAlwaysAvailable = true, ?bool $showHiddenTags = null): array { - $tags = $this->gateway->getChildren($tagId, $offset, $limit, $translations, $useAlwaysAvailable); + $tags = $this->gateway->getChildren($tagId, $offset, $limit, $translations, $useAlwaysAvailable, $showHiddenTags); return $this->mapper->extractTagListFromRows($tags); } - public function getChildrenCount(int $tagId, ?array $translations = null, bool $useAlwaysAvailable = true): int + public function getChildrenCount(int $tagId, ?array $translations = null, bool $useAlwaysAvailable = true, ?bool $showHiddenTags = null): int { - return $this->gateway->getChildrenCount($tagId, $translations, $useAlwaysAvailable); + return $this->gateway->getChildrenCount($tagId, $translations, $useAlwaysAvailable, $showHiddenTags); } - public function loadTagsByKeyword(string $keyword, string $translation, bool $useAlwaysAvailable = true, int $offset = 0, int $limit = -1): array + public function loadTagsByKeyword(string $keyword, string $translation, bool $useAlwaysAvailable = true, int $offset = 0, int $limit = -1, ?bool $showHiddenTags = null): array { - $tags = $this->gateway->getTagsByKeyword($keyword, $translation, $useAlwaysAvailable, true, $offset, $limit); + $tags = $this->gateway->getTagsByKeyword($keyword, $translation, $useAlwaysAvailable, true, $offset, $limit, $showHiddenTags); return $this->mapper->extractTagListFromRows($tags); } - public function getTagsByKeywordCount(string $keyword, string $translation, bool $useAlwaysAvailable = true): int + public function getTagsByKeywordCount(string $keyword, string $translation, bool $useAlwaysAvailable = true, ?bool $showHiddenTags = null): int { - return $this->gateway->getTagsByKeywordCount($keyword, $translation, $useAlwaysAvailable); + return $this->gateway->getTagsByKeywordCount($keyword, $translation, $useAlwaysAvailable, $showHiddenTags); } - public function searchTags(string $searchString, string $translation, bool $useAlwaysAvailable = true, int $offset = 0, int $limit = -1): SearchResult + public function searchTags(string $searchString, string $translation, bool $useAlwaysAvailable = true, int $offset = 0, int $limit = -1, ?bool $showHiddenTags = null): SearchResult { - $tags = $this->gateway->getTagsByKeyword($searchString, $translation, $useAlwaysAvailable, false, $offset, $limit); - $totalCount = $this->gateway->getTagsByKeywordCount($searchString, $translation, $useAlwaysAvailable, false); + $tags = $this->gateway->getTagsByKeyword($searchString, $translation, $useAlwaysAvailable, false, $offset, $limit, $showHiddenTags); + $totalCount = $this->gateway->getTagsByKeywordCount($searchString, $translation, $useAlwaysAvailable, $showHiddenTags, false); return new SearchResult( [ @@ -126,16 +126,16 @@ public function searchTags(string $searchString, string $translation, bool $useA ); } - public function loadSynonyms(int $tagId, int $offset = 0, int $limit = -1, ?array $translations = null, bool $useAlwaysAvailable = true): array + public function loadSynonyms(int $tagId, int $offset = 0, int $limit = -1, ?array $translations = null, bool $useAlwaysAvailable = true, ?bool $showHiddenTags = null): array { - $tags = $this->gateway->getSynonyms($tagId, $offset, $limit, $translations, $useAlwaysAvailable); + $tags = $this->gateway->getSynonyms($tagId, $offset, $limit, $translations, $useAlwaysAvailable, $showHiddenTags); return $this->mapper->extractTagListFromRows($tags); } - public function getSynonymCount(int $tagId, ?array $translations = null, bool $useAlwaysAvailable = true): int + public function getSynonymCount(int $tagId, ?array $translations = null, bool $useAlwaysAvailable = true, ?bool $showHiddenTags = null): int { - return $this->gateway->getSynonymCount($tagId, $translations, $useAlwaysAvailable); + return $this->gateway->getSynonymCount($tagId, $translations, $useAlwaysAvailable, $showHiddenTags); } public function create(CreateStruct $createStruct): Tag @@ -219,6 +219,18 @@ public function deleteTag(int $tagId): void $this->gateway->deleteTag($tagInfo->id); } + public function hideTag(int $tagId): void + { + $tagInfo = $this->loadTagInfo($tagId); + $this->gateway->hideTag($tagInfo->id); + } + + public function revealTag(int $tagId): void + { + $tagInfo = $this->loadTagInfo($tagId); + $this->gateway->revealTag($tagInfo->id); + } + /** * Copies tag object identified by $sourceData into destination identified by $destinationParentData. * diff --git a/bundle/Core/Persistence/Legacy/Tags/Mapper.php b/bundle/Core/Persistence/Legacy/Tags/Mapper.php index 024eecdf..d8c4f796 100644 --- a/bundle/Core/Persistence/Legacy/Tags/Mapper.php +++ b/bundle/Core/Persistence/Legacy/Tags/Mapper.php @@ -35,6 +35,8 @@ public function createTagInfoFromRow(array $row): TagInfo $tagInfo->alwaysAvailable = (bool) ((int) $row['language_mask'] & 1); $tagInfo->mainLanguageCode = $this->languageHandler->load($row['main_language_id'])->languageCode; $tagInfo->languageIds = $this->languageMaskGenerator->extractLanguageIdsFromMask((int) $row['language_mask']); + $tagInfo->isHidden = (bool) $row['is_hidden']; + $tagInfo->isInvisible = (bool) $row['is_invisible']; return $tagInfo; } @@ -60,6 +62,8 @@ public function extractTagListFromRows(array $rows): array $tag->alwaysAvailable = (bool) ((int) $row['language_mask'] & 1); $tag->mainLanguageCode = $this->languageHandler->load($row['main_language_id'])->languageCode; $tag->languageIds = $this->languageMaskGenerator->extractLanguageIdsFromMask((int) $row['language_mask']); + $tag->isHidden = (bool) $row['is_hidden']; + $tag->isInvisible = (bool) $row['is_invisible']; $tagList[$tagId] = $tag; } diff --git a/bundle/Core/Repository/TagsMapper.php b/bundle/Core/Repository/TagsMapper.php index 2d94ff7e..7ad0311f 100644 --- a/bundle/Core/Repository/TagsMapper.php +++ b/bundle/Core/Repository/TagsMapper.php @@ -67,6 +67,8 @@ public function buildTagDomainList(array $spiTags, array $prioritizedLanguages = 'mainLanguageCode' => $spiTag->mainLanguageCode, 'languageCodes' => $languageCodes, 'prioritizedLanguageCode' => $prioritizedLanguageCode, + 'isHidden' => $spiTag->isHidden, + 'isInvisible' => $spiTag->isInvisible, ], ); } diff --git a/bundle/Core/Repository/TagsService.php b/bundle/Core/Repository/TagsService.php index 0574906d..0bcbe569 100644 --- a/bundle/Core/Repository/TagsService.php +++ b/bundle/Core/Repository/TagsService.php @@ -152,7 +152,7 @@ public function loadTagByUrl(string $url, array $languages): Tag return $this->mapper->buildTagDomainObject($spiTag, $languages); } - public function loadTagChildren(?Tag $tag = null, int $offset = 0, int $limit = -1, ?array $languages = null, bool $useAlwaysAvailable = true): TagList + public function loadTagChildren(?Tag $tag = null, int $offset = 0, int $limit = -1, ?array $languages = null, bool $useAlwaysAvailable = true, ?bool $showHiddenTags = null): TagList { if ($this->hasAccess('tags', 'read') === false) { throw new UnauthorizedException('tags', 'read'); @@ -164,6 +164,7 @@ public function loadTagChildren(?Tag $tag = null, int $offset = 0, int $limit = $limit, $languages, $useAlwaysAvailable, + $showHiddenTags, ); $tags = []; @@ -174,7 +175,7 @@ public function loadTagChildren(?Tag $tag = null, int $offset = 0, int $limit = return new TagList($tags); } - public function getTagChildrenCount(?Tag $tag = null, ?array $languages = null, bool $useAlwaysAvailable = true): int + public function getTagChildrenCount(?Tag $tag = null, ?array $languages = null, bool $useAlwaysAvailable = true, ?bool $showHiddenTags = null): int { if ($this->hasAccess('tags', 'read') === false) { throw new UnauthorizedException('tags', 'read'); @@ -184,16 +185,17 @@ public function getTagChildrenCount(?Tag $tag = null, ?array $languages = null, $tag?->id ?? 0, $languages, $useAlwaysAvailable, + $showHiddenTags, ); } - public function loadTagsByKeyword(string $keyword, string $language, bool $useAlwaysAvailable = true, int $offset = 0, int $limit = -1): TagList + public function loadTagsByKeyword(string $keyword, string $language, bool $useAlwaysAvailable = true, int $offset = 0, int $limit = -1, ?bool $showHiddenTags = null): TagList { if ($this->hasAccess('tags', 'read') === false) { throw new UnauthorizedException('tags', 'read'); } - $spiTags = $this->tagsHandler->loadTagsByKeyword($keyword, $language, $useAlwaysAvailable, $offset, $limit); + $spiTags = $this->tagsHandler->loadTagsByKeyword($keyword, $language, $useAlwaysAvailable, $offset, $limit, $showHiddenTags); $tags = []; foreach ($spiTags as $spiTag) { @@ -203,16 +205,16 @@ public function loadTagsByKeyword(string $keyword, string $language, bool $useAl return new TagList($tags); } - public function getTagsByKeywordCount(string $keyword, string $language, bool $useAlwaysAvailable = true): int + public function getTagsByKeywordCount(string $keyword, string $language, bool $useAlwaysAvailable = true, ?bool $showHiddenTags = null): int { if ($this->hasAccess('tags', 'read') === false) { throw new UnauthorizedException('tags', 'read'); } - return $this->tagsHandler->getTagsByKeywordCount($keyword, $language, $useAlwaysAvailable); + return $this->tagsHandler->getTagsByKeywordCount($keyword, $language, $useAlwaysAvailable, $showHiddenTags); } - public function searchTags(string $searchString, string $language, bool $useAlwaysAvailable = true, int $offset = 0, int $limit = -1): SearchResult + public function searchTags(string $searchString, string $language, bool $useAlwaysAvailable = true, int $offset = 0, int $limit = -1, ?bool $showHiddenTags = null): SearchResult { if ($this->hasAccess('tags', 'read') === false) { throw new UnauthorizedException('tags', 'read'); @@ -224,6 +226,7 @@ public function searchTags(string $searchString, string $language, bool $useAlwa $useAlwaysAvailable, $offset, $limit, + $showHiddenTags, ); $tags = []; @@ -239,7 +242,7 @@ public function searchTags(string $searchString, string $language, bool $useAlwa ); } - public function loadTagSynonyms(Tag $tag, int $offset = 0, int $limit = -1, ?array $languages = null, bool $useAlwaysAvailable = true): TagList + public function loadTagSynonyms(Tag $tag, int $offset = 0, int $limit = -1, ?array $languages = null, bool $useAlwaysAvailable = true, ?bool $showHiddenTags = null): TagList { if ($this->hasAccess('tags', 'read') === false) { throw new UnauthorizedException('tags', 'read'); @@ -255,6 +258,7 @@ public function loadTagSynonyms(Tag $tag, int $offset = 0, int $limit = -1, ?arr $limit, $languages, $useAlwaysAvailable, + $showHiddenTags, ); $tags = []; @@ -265,7 +269,7 @@ public function loadTagSynonyms(Tag $tag, int $offset = 0, int $limit = -1, ?arr return new TagList($tags); } - public function getTagSynonymCount(Tag $tag, ?array $languages = null, bool $useAlwaysAvailable = true): int + public function getTagSynonymCount(Tag $tag, ?array $languages = null, bool $useAlwaysAvailable = true, ?bool $showHiddenTags = null): int { if ($this->hasAccess('tags', 'read') === false) { throw new UnauthorizedException('tags', 'read'); @@ -279,6 +283,7 @@ public function getTagSynonymCount(Tag $tag, ?array $languages = null, bool $use $tag->id, $languages, $useAlwaysAvailable, + $showHiddenTags, ); } @@ -751,6 +756,42 @@ public function newTagUpdateStruct(): TagUpdateStruct return new TagUpdateStruct(); } + public function hideTag(Tag $tag): void + { + if ($this->hasAccess('tags', 'hide') === false) { + throw new UnauthorizedException('tags', 'hide'); + } + + $this->repository->beginTransaction(); + + try { + $this->tagsHandler->hideTag($tag->id); + $this->repository->commit(); + } catch (Exception $e) { + $this->repository->rollback(); + + throw $e; + } + } + + public function revealTag(Tag $tag): void + { + if ($this->hasAccess('tags', 'hide') === false) { + throw new UnauthorizedException('tags', 'hide'); + } + + $this->repository->beginTransaction(); + + try { + $this->tagsHandler->revealTag($tag->id); + $this->repository->commit(); + } catch (Exception $e) { + $this->repository->rollback(); + + throw $e; + } + } + public function sudo(callable $callback, ?TagsServiceInterface $outerTagsService = null): mixed { ++$this->sudoNestingLevel; diff --git a/bundle/Core/SiteAccessAware/TagsService.php b/bundle/Core/SiteAccessAware/TagsService.php index 37d36a94..e7731905 100644 --- a/bundle/Core/SiteAccessAware/TagsService.php +++ b/bundle/Core/SiteAccessAware/TagsService.php @@ -5,6 +5,7 @@ namespace Netgen\TagsBundle\Core\SiteAccessAware; use Ibexa\Contracts\Core\Repository\LanguageResolver; +use Ibexa\Contracts\Core\SiteAccess\ConfigResolverInterface; use Netgen\TagsBundle\API\Repository\TagsService as TagsServiceInterface; use Netgen\TagsBundle\API\Repository\Values\Tags\SearchResult; use Netgen\TagsBundle\API\Repository\Values\Tags\SynonymCreateStruct; @@ -15,7 +16,11 @@ final class TagsService implements TagsServiceInterface { - public function __construct(private TagsServiceInterface $innerService, private LanguageResolver $languageResolver) {} + public function __construct( + private TagsServiceInterface $innerService, + private LanguageResolver $languageResolver, + private ConfigResolverInterface $configResolver, + ) {} public function loadTag(int $tagId, ?array $languages = null, bool $useAlwaysAvailable = true): Tag { @@ -52,7 +57,7 @@ public function loadTagByUrl(string $url, array $languages): Tag ); } - public function loadTagChildren(?Tag $tag = null, int $offset = 0, int $limit = -1, ?array $languages = null, bool $useAlwaysAvailable = true): TagList + public function loadTagChildren(?Tag $tag = null, int $offset = 0, int $limit = -1, ?array $languages = null, bool $useAlwaysAvailable = true, ?bool $showHiddenTags = null): TagList { return $this->innerService->loadTagChildren( $tag, @@ -60,19 +65,21 @@ public function loadTagChildren(?Tag $tag = null, int $offset = 0, int $limit = $limit, $this->languageResolver->getPrioritizedLanguages($languages), $this->languageResolver->getUseAlwaysAvailable($useAlwaysAvailable), + $showHiddenTags ?? $this->configResolver->getParameter('show_hidden_tags', 'netgen_tags'), ); } - public function getTagChildrenCount(?Tag $tag = null, ?array $languages = null, bool $useAlwaysAvailable = true): int + public function getTagChildrenCount(?Tag $tag = null, ?array $languages = null, bool $useAlwaysAvailable = true, ?bool $showHiddenTags = null): int { return $this->innerService->getTagChildrenCount( $tag, $this->languageResolver->getPrioritizedLanguages($languages), $this->languageResolver->getUseAlwaysAvailable($useAlwaysAvailable), + $showHiddenTags ?? $this->configResolver->getParameter('show_hidden_tags', 'netgen_tags'), ); } - public function loadTagsByKeyword(string $keyword, string $language, bool $useAlwaysAvailable = true, int $offset = 0, int $limit = -1): TagList + public function loadTagsByKeyword(string $keyword, string $language, bool $useAlwaysAvailable = true, int $offset = 0, int $limit = -1, ?bool $showHiddenTags = null): TagList { return $this->innerService->loadTagsByKeyword( $keyword, @@ -80,19 +87,21 @@ public function loadTagsByKeyword(string $keyword, string $language, bool $useAl $this->languageResolver->getUseAlwaysAvailable($useAlwaysAvailable), $offset, $limit, + $showHiddenTags ?? $this->configResolver->getParameter('show_hidden_tags', 'netgen_tags'), ); } - public function getTagsByKeywordCount(string $keyword, string $language, bool $useAlwaysAvailable = true): int + public function getTagsByKeywordCount(string $keyword, string $language, bool $useAlwaysAvailable = true, ?bool $showHiddenTags = null): int { return $this->innerService->getTagsByKeywordCount( $keyword, $language, $this->languageResolver->getUseAlwaysAvailable($useAlwaysAvailable), + $showHiddenTags ?? $this->configResolver->getParameter('show_hidden_tags', 'netgen_tags'), ); } - public function searchTags(string $searchString, string $language, bool $useAlwaysAvailable = true, int $offset = 0, int $limit = -1): SearchResult + public function searchTags(string $searchString, string $language, bool $useAlwaysAvailable = true, int $offset = 0, int $limit = -1, ?bool $showHiddenTags = null): SearchResult { return $this->innerService->searchTags( $searchString, @@ -100,10 +109,11 @@ public function searchTags(string $searchString, string $language, bool $useAlwa $this->languageResolver->getUseAlwaysAvailable($useAlwaysAvailable), $offset, $limit, + $showHiddenTags ?? $this->configResolver->getParameter('show_hidden_tags', 'netgen_tags'), ); } - public function loadTagSynonyms(Tag $tag, int $offset = 0, int $limit = -1, ?array $languages = null, bool $useAlwaysAvailable = true): TagList + public function loadTagSynonyms(Tag $tag, int $offset = 0, int $limit = -1, ?array $languages = null, bool $useAlwaysAvailable = true, ?bool $showHiddenTags = null): TagList { return $this->innerService->loadTagSynonyms( $tag, @@ -111,15 +121,17 @@ public function loadTagSynonyms(Tag $tag, int $offset = 0, int $limit = -1, ?arr $limit, $this->languageResolver->getPrioritizedLanguages($languages), $this->languageResolver->getUseAlwaysAvailable($useAlwaysAvailable), + $showHiddenTags ?? $this->configResolver->getParameter('show_hidden_tags', 'netgen_tags'), ); } - public function getTagSynonymCount(Tag $tag, ?array $languages = null, bool $useAlwaysAvailable = true): int + public function getTagSynonymCount(Tag $tag, ?array $languages = null, bool $useAlwaysAvailable = true, ?bool $showHiddenTags = null): int { return $this->innerService->getTagSynonymCount( $tag, $this->languageResolver->getPrioritizedLanguages($languages), $this->languageResolver->getUseAlwaysAvailable($useAlwaysAvailable), + $showHiddenTags ?? $this->configResolver->getParameter('show_hidden_tags', 'netgen_tags'), ); } @@ -188,6 +200,16 @@ public function newTagUpdateStruct(): TagUpdateStruct return $this->innerService->newTagUpdateStruct(); } + public function hideTag(Tag $tag): void + { + $this->innerService->hideTag($tag); + } + + public function revealTag(Tag $tag): void + { + $this->innerService->revealTag($tag); + } + public function sudo(callable $callback, ?TagsServiceInterface $outerTagsService = null): mixed { return $this->innerService->sudo($callback, $outerTagsService ?? $this); diff --git a/bundle/DependencyInjection/Configuration.php b/bundle/DependencyInjection/Configuration.php index dca24605..5b77ca7d 100644 --- a/bundle/DependencyInjection/Configuration.php +++ b/bundle/DependencyInjection/Configuration.php @@ -155,6 +155,14 @@ public function getConfigTreeBuilder(): TreeBuilder ->defaultValue(25) ->end() ->end() + ->end() + ->booleanNode('show_hidden_tags') + ->info('Whether to show hidden tags or not') + ->defaultTrue() + ->end() + ->booleanNode('autocomplete_provide_hidden_tags') + ->info('When searching for a tag to add it to some content, should autocomplete show hidden tags or not') + ->defaultFalse() ->end(); return $treeBuilder; diff --git a/bundle/DependencyInjection/NetgenTagsExtension.php b/bundle/DependencyInjection/NetgenTagsExtension.php index b2c59365..df1b7f8d 100644 --- a/bundle/DependencyInjection/NetgenTagsExtension.php +++ b/bundle/DependencyInjection/NetgenTagsExtension.php @@ -121,6 +121,9 @@ static function (array $config, string $scope, ContextualizerInterface $c): void $c->setContextualParameter('admin.tree_limit', $scope, $config['admin']['tree_limit']); $c->setContextualParameter('admin.related_content_limit', $scope, $config['admin']['related_content_limit']); $c->setContextualParameter('field.autocomplete_limit', $scope, $config['field']['autocomplete_limit']); + + $c->setContextualParameter('show_hidden_tags', $scope, $config['show_hidden_tags']); + $c->setContextualParameter('autocomplete_provide_hidden_tags', $scope, $config['autocomplete_provide_hidden_tags']); }, ); diff --git a/bundle/Form/Type/MoveTagsType.php b/bundle/Form/Type/MultiselectTagsType.php similarity index 56% rename from bundle/Form/Type/MoveTagsType.php rename to bundle/Form/Type/MultiselectTagsType.php index 9590a2fb..4078ec92 100644 --- a/bundle/Form/Type/MoveTagsType.php +++ b/bundle/Form/Type/MultiselectTagsType.php @@ -10,7 +10,7 @@ use function array_map; -final class MoveTagsType extends AbstractType +final class MultiselectTagsType extends AbstractType { public function configureOptions(OptionsResolver $resolver): void { @@ -27,22 +27,26 @@ public function configureOptions(OptionsResolver $resolver): void } return true; - }); + }) + ->setDefault('show_parent_field', true) + ->setAllowedTypes('show_parent_field', 'bool'); } public function buildForm(FormBuilderInterface $builder, array $options): void { - $builder - ->add( - 'parentTag', - TagTreeType::class, - [ - 'label' => 'tag.parent_tag', - 'disableSubtree' => array_map( - static fn (Tag $tag): int => $tag->id, - $options['tags'], - ), - ], - ); + if ($options['show_parent_field'] === true) { + $builder + ->add( + 'parentTag', + TagTreeType::class, + [ + 'label' => 'tag.parent_tag', + 'disableSubtree' => array_map( + static fn (Tag $tag): int => $tag->id, + $options['tags'], + ), + ], + ); + } } } diff --git a/bundle/Resources/config/papi.yaml b/bundle/Resources/config/papi.yaml index 9c5d41b6..8541dbc1 100644 --- a/bundle/Resources/config/papi.yaml +++ b/bundle/Resources/config/papi.yaml @@ -17,6 +17,7 @@ services: arguments: - "@netgen_tags.event.service.tags" - "@ibexa.helper.language_resolver" + - "@ibexa.config.resolver" netgen_tags.api.service.tags.mapper: class: Netgen\TagsBundle\Core\Repository\TagsMapper diff --git a/bundle/Resources/config/policies.yaml b/bundle/Resources/config/policies.yaml index 1bedf021..4e76fe98 100644 --- a/bundle/Resources/config/policies.yaml +++ b/bundle/Resources/config/policies.yaml @@ -9,3 +9,4 @@ tags: deletesynonym: ~ makesynonym: ~ merge: ~ + hide: ~ diff --git a/bundle/Resources/config/routing/admin/tag.yaml b/bundle/Resources/config/routing/admin/tag.yaml index 8f06b8cf..416be269 100644 --- a/bundle/Resources/config/routing/admin/tag.yaml +++ b/bundle/Resources/config/routing/admin/tag.yaml @@ -38,6 +38,16 @@ netgen_tags_admin_tag_delete_tags: controller: netgen_tags.admin.controller.tag:deleteTagsAction methods: [GET, POST] +netgen_tags_admin_tag_hide_tags: + path: /hide/{parentId} + controller: netgen_tags.admin.controller.tag:hideTagsAction + methods: [GET, POST] + +netgen_tags_admin_tag_reveal_tags: + path: /reveal/{parentId} + controller: netgen_tags.admin.controller.tag:revealTagsAction + methods: [GET, POST] + netgen_tags_admin_tag_update_select: path: /{tagId}/update controller: netgen_tags.admin.controller.tag:updateTagSelectAction @@ -72,3 +82,13 @@ netgen_tags_admin_tag_children: path: /{tagId}/children controller: netgen_tags.admin.controller.tag:childrenAction methods: [POST] + +netgen_tags_admin_tag_hide: + path: /{tagId}/hide + controller: netgen_tags.admin.controller.tag:hideAction + methods: [GET, POST] + +netgen_tags_admin_tag_reveal: + path: /{tagId}/reveal + controller: netgen_tags.admin.controller.tag:revealAction + methods: [GET, POST] diff --git a/bundle/Resources/public/admin/js/app.js b/bundle/Resources/public/admin/js/app.js index c90f1245..72bf709e 100644 --- a/bundle/Resources/public/admin/js/app.js +++ b/bundle/Resources/public/admin/js/app.js @@ -79,11 +79,37 @@ jQuery.noConflict(); } } }).on("open_node.jstree", function (event, data) { + var route = self.$tree.data('route'); + if (self.disableSubtree !== '') { self.disableNode(self.disableSubtree); } + + if (data.node && + data.node.original && + (data.node.original.hidden === true || data.node.original.invisible === true)) { + + if (['netgen_tags_admin_tag_convert', 'netgen_tags_admin_tag_merge'].indexOf(route) === -1) { + self.disableNode(data.node.id); + } + } + + if (data.node && data.node.children) { + data.node.children.forEach(function(childId) { + var childNode = self.$tree.jstree(true).get_node(childId); + if (childNode && + childNode.original && + (childNode.original.hidden === true || childNode.original.invisible === true)) { + + if (['netgen_tags_admin_tag_convert', 'netgen_tags_admin_tag_merge'].indexOf(route) === -1) { + self.disableNode(childId); + } + } + }); + } }).on('click', '.jstree-anchor', function (event) { var selectedNode = $(this).jstree(true).get_node($(this)); + var route = self.$tree.data('route'); if (self.disableSubtree !== '') { self.disableSubtree = self.disableSubtree.toString().split(','); @@ -104,6 +130,14 @@ jQuery.noConflict(); if (!self.settings.modal) { document.location.href = selectedNode.a_attr.href; } else { + if (selectedNode.original && + (selectedNode.original.hidden === true || selectedNode.original.invisible === true)) { + + if (['netgen_tags_admin_tag_convert', 'netgen_tags_admin_tag_merge'].indexOf(route) === -1) { + return false; + } + } + self.$el.find('input.tag-id').val(selectedNode.id); if (selectedNode.text === undefined || selectedNode.id == '0') { @@ -345,7 +379,29 @@ function ngTagsInit(jQuery){ }); $enabledInputs.on('change', function(e){ var name = e.currentTarget.dataset.enable; - $('input[data-enable="' + name + '"]:checked').length ? $('[data-enabler="' + name + '"]').removeAttr('disabled') : $('[data-enabler="' + name + '"]').attr('disabled', 'disabled'); + + var $checkedBoxes = $('input[data-enable="' + name + '"]:checked'); + var $allButtons = $('[data-enabler="' + name + '"]'); + + $allButtons.prop('disabled', !$checkedBoxes.length); + + if (name === 'Tags') { + var $hideButton = $('button[name="HideTagsAction"]'); + var $revealButton = $('button[name="RevealTagsAction"]'); + + var hasHidden = false; + var hasRevealed = false; + $checkedBoxes.each(function() { + var isHidden = $(this).closest('tr').find('td:eq(5)').text().trim() === '1'; + + isHidden ? hasHidden = true : hasRevealed = true; + + if (hasHidden && hasRevealed) return false; + }); + + $hideButton.prop('disabled', hasHidden || !$checkedBoxes.length); + $revealButton.prop('disabled', hasRevealed || !$checkedBoxes.length); + } }); } diff --git a/bundle/Resources/schema/legacy.yaml b/bundle/Resources/schema/legacy.yaml index dbdfd035..4bcc885f 100644 --- a/bundle/Resources/schema/legacy.yaml +++ b/bundle/Resources/schema/legacy.yaml @@ -55,6 +55,16 @@ tables: nullable: false options: default: '0' + is_hidden: + type: tinyint + nullable: false + options: + default: '0' + is_invisible: + type: tinyint + nullable: false + options: + default: '0' indexes: idx_eztags_keyword: fields: [keyword] diff --git a/bundle/Resources/sql/mysql/schema.sql b/bundle/Resources/sql/mysql/schema.sql index 6130ee95..9a7197e7 100644 --- a/bundle/Resources/sql/mysql/schema.sql +++ b/bundle/Resources/sql/mysql/schema.sql @@ -9,6 +9,8 @@ CREATE TABLE `eztags` ( `remote_id` varchar(100) NOT NULL default '', `main_language_id` int(11) NOT NULL default '0', `language_mask` int(11) NOT NULL default '0', + `is_hidden` tinyint NOT NULL DEFAULT '0', + `is_invisible` tinyint NOT NULL DEFAULT '0', PRIMARY KEY ( `id` ), KEY `idx_eztags_keyword` ( `keyword`(191) ), KEY `idx_eztags_keyword_id` ( `keyword`(191), `id` ), diff --git a/bundle/Resources/sql/postgresql/schema.sql b/bundle/Resources/sql/postgresql/schema.sql index b86c4705..36aa10d7 100644 --- a/bundle/Resources/sql/postgresql/schema.sql +++ b/bundle/Resources/sql/postgresql/schema.sql @@ -10,6 +10,8 @@ CREATE TABLE eztags ( remote_id varchar(100) NOT NULL default '', main_language_id integer not null default 0, language_mask integer not null default 0, + is_hidden integer not null default 0, + is_invisible integer not null default 0, PRIMARY KEY (id), CONSTRAINT idx_eztags_remote_id UNIQUE (remote_id) ); diff --git a/bundle/Resources/sql/upgrade/mysql/5.4/dbupdate-5.0-to-5.4.sql b/bundle/Resources/sql/upgrade/mysql/5.4/dbupdate-5.0-to-5.4.sql new file mode 100644 index 00000000..54433409 --- /dev/null +++ b/bundle/Resources/sql/upgrade/mysql/5.4/dbupdate-5.0-to-5.4.sql @@ -0,0 +1,3 @@ +ALTER TABLE `eztags` ADD COLUMN `is_hidden` TINYINT NOT NULL DEFAULT 0; + +ALTER TABLE `eztags` ADD COLUMN `is_invisible` TINYINT NOT NULL DEFAULT 0; diff --git a/bundle/Resources/sql/upgrade/postgresql/5.4/dbupdate-5.0-to-5.4.sql b/bundle/Resources/sql/upgrade/postgresql/5.4/dbupdate-5.0-to-5.4.sql new file mode 100644 index 00000000..61508a8f --- /dev/null +++ b/bundle/Resources/sql/upgrade/postgresql/5.4/dbupdate-5.0-to-5.4.sql @@ -0,0 +1,3 @@ +ALTER TABLE `eztags` ADD COLUMN `is_hidden` INTEGER NOT NULL DEFAULT 0; + +ALTER TABLE `eztags` ADD COLUMN `is_invisible` INTEGER NOT NULL DEFAULT 0; diff --git a/bundle/Resources/translations/netgen_tags_admin.en.yml b/bundle/Resources/translations/netgen_tags_admin.en.yml index 34a5ea08..855dd7ec 100644 --- a/bundle/Resources/translations/netgen_tags_admin.en.yml +++ b/bundle/Resources/translations/netgen_tags_admin.en.yml @@ -13,6 +13,9 @@ tag.translations: 'Translations' tag.modified: 'Modified' tag.main_tag: 'Main tag' tag.parent_tag: 'Parent tag' +tag.hidden: 'Hidden' +tag.invisible: 'Invisible' +tag.hidden_by_parent: 'hidden by parent' tag.content.content_id: 'Content ID' tag.content.name: 'Name' @@ -70,6 +73,12 @@ tag.add_synonym.title: 'Add synonym' tag.convert.title: 'Convert to synonym' +tag.hide.title: 'Hide tag' +tag.hide.message: 'Are you sure you want to hide the "%tagKeyword%" tag? All descendant tags that are not explicitly hidden will then become invisible and marked as "hidden by parent".' + +tag.reveal.title: 'Reveal tag' +tag.reveal.message: 'Are you sure you want to reveal the "%tagKeyword%" tag? All of its descendant tags will become visible again, except those that are explicitly hidden themselves or have another hidden ancestor.' + tag.move_tags.title: 'Move tags' tag.move_tags.message: 'Are you sure you want to move the selected tags?' @@ -78,6 +87,12 @@ tag.copy_tags.title: 'Copy tags' tag.delete_tags.title: 'Delete tags' tag.delete_tags.message: 'Are you sure you want to delete the selected tags? All children tags and synonyms will also be deleted and removed from existing objects.' +tag.hide_tags.title: 'Hide tags' +tag.hide_tags.message: 'Are you sure you want to hide the selected tags?' + +tag.reveal_tags.title: 'Reveal tags' +tag.reveal_tags.message: 'Are you sure you want to reveal the selected tags?' + tag.button.save: 'Save' tag.button.discard: 'Discard' tag.button.yes: 'Yes' @@ -89,6 +104,8 @@ tag.button.set_main: 'Set main' tag.button.move_selected: 'Move selected tags' tag.button.copy_selected: 'Copy selected tags' tag.button.delete_selected: 'Remove selected tags' +tag.button.hide_selected: 'Hide selected tags' +tag.button.reveal_selected: 'Reveal selected tags' tag.button.continue: 'Continue' tag.tree.top_level_tags: 'Top level tags' @@ -98,6 +115,8 @@ tag.tree.delete_tag: 'Delete tag' tag.tree.merge_tag: 'Merge tag' tag.tree.convert_tag: 'Convert to synonym' tag.tree.add_synonym: 'Add synonym' +tag.tree.hide_tag: 'Hide tag' +tag.tree.reveal_tag: 'Reveal tag' tag.tree.no_tag_selected: 'no tag' tag.tree.select_tag: 'Select tag' diff --git a/bundle/Resources/translations/netgen_tags_admin.fr.yml b/bundle/Resources/translations/netgen_tags_admin.fr.yml index 2bdf7739..33b42ed5 100644 --- a/bundle/Resources/translations/netgen_tags_admin.fr.yml +++ b/bundle/Resources/translations/netgen_tags_admin.fr.yml @@ -13,6 +13,9 @@ tag.translations: 'Traductions' tag.modified: 'Modifié' tag.main_tag: 'Tag principale' tag.parent_tag: 'Tag parent' +tag.hidden: 'Caché' +tag.invisible: 'Invisible' +tag.hidden_by_parent: 'caché par le parent' tag.content.content_id: 'ID du contenu' tag.content.name: 'Nom' @@ -59,6 +62,12 @@ tag.add_synonym.title: 'Ajouter un synonyme' tag.convert.title: 'Convertir en synonyme' +tag.hide.title: 'Cacher le tag' +tag.hide.message: 'Êtes-vous sûr de vouloir cacher la balise "%tagKeyword%"? Toutes les balises descendantes qui ne sont pas explicitement cachées deviendront alors invisibles et marquées comme "cachées par le parent".' + +tag.reveal.title: 'Révéler le tag' +tag.reveal.message: "Êtes-vous sûr de vouloir révéler la balise '%tagKeyword%' ? Toutes ses balises descendantes redeviendront visibles, à l'exception de celles qui sont explicitement cachées ou qui ont un autre ancêtre caché." + tag.move_tags.title: 'Déplacer les tags' tag.move_tags.message: 'Êtes-vous sûr de vouloir déplacer les tags sélectionnés ?' @@ -67,6 +76,12 @@ tag.copy_tags.title: 'Copier les tags' tag.delete_tags.title: 'Supprimer les tags' tag.delete_tags.message: 'Êtes-vous sûr de vouloir supprimer les tags sélectionnés ? Tous les tag et synonymes enfants seront également supprimés et supprimés des objets existants.' +tag.hide_tags.title: 'Cacher les tags' +tag.hide_tags.message: 'Êtes-vous sûr de vouloir masquer les balises sélectionnées?' + +tag.reveal_tags.title: 'Révéler les tags' +tag.reveal_tags.message: 'Êtes-vous sûr de vouloir révéler les balises sélectionnées?' + tag.button.save: 'Sauvegarder' tag.button.discard: 'Jeter' tag.button.yes: 'Oui' @@ -78,6 +93,8 @@ tag.button.set_main: 'Définir principal' tag.button.move_selected: 'Déplacer les tags sélectionnées' tag.button.copy_selected: 'Copier les tags sélectionnés' tag.button.delete_selected: 'Supprimer les tags sélectionnés' +tag.button.hide_selected: 'Masquer les tags sélectionnés' +tag.button.reveal_selected: 'Désactiver les balises sélectionnées' tag.button.continue: 'Continuer' tag.tree.top_level_tags: 'Tags de premier niveau' @@ -87,6 +104,8 @@ tag.tree.delete_tag: 'Supprimer le tag' tag.tree.merge_tag: 'Fusionner le tag' tag.tree.convert_tag: 'Convertir en synonyme' tag.tree.add_synonym: 'Ajouter un synonyme' +tag.tree.hide_tag: 'Cacher le tag' +tag.tree.reveal_tag: 'Révéler le tag' tag.tree.no_tag_selected: 'pas de tag' tag.tree.select_tag: 'Sélectionner un tag' diff --git a/bundle/Resources/translations/netgen_tags_admin_flash.en.yml b/bundle/Resources/translations/netgen_tags_admin_flash.en.yml index a3971a24..8c59e976 100644 --- a/bundle/Resources/translations/netgen_tags_admin_flash.en.yml +++ b/bundle/Resources/translations/netgen_tags_admin_flash.en.yml @@ -12,6 +12,10 @@ success.tag_converted: 'Tag "%tagKeyword%" has been converted to synonym of tag success.tags_moved: 'Tags have been moved successfully' success.tags_copied: 'Tags have been copied successfully' success.tags_deleted: 'Tags have been deleted successfully' +success.tags_hidden: 'Tags have been hidden successfully' +success.tags_revealed: 'Tags have been revealed successfully' success.translation_removed: 'Translation for locale "%locale%" has been successfully removed' success.main_translation_set: 'Translation for locale "%locale%" has been set as new main translation' success.always_available_set: 'Always available flag has been successfully updated' +success.tag_hidden: 'Tag "%tagKeyword%" has been hidden' +success.tag_revealed: 'Tag "%tagKeyword%" has been revealed' diff --git a/bundle/Resources/translations/netgen_tags_admin_flash.fr.yml b/bundle/Resources/translations/netgen_tags_admin_flash.fr.yml index da2fb94c..43269767 100644 --- a/bundle/Resources/translations/netgen_tags_admin_flash.fr.yml +++ b/bundle/Resources/translations/netgen_tags_admin_flash.fr.yml @@ -8,10 +8,14 @@ success.tag_added: 'Le nouveau tag "%tagKeyword%" a été créé' success.tag_updated: 'Le tag "%tagKeyword%" a été mis à jour' success.tag_deleted: 'Le tag "%tagKeyword%" a été supprimé' success.tag_merged: 'Le tag "%tagKeyword%" a été fusionné avec le tag "%sourceTagKeyword%"' -success.tag_converted: 'Tag "%tagKeyword%" has been converted to synonym of tag "%mainTagKeyword%"' +success.tag_converted: 'Le tag "%tagKeyword%" a été converti en synonyme du tag "%mainTagKeyword%"' success.tags_moved: 'Les tags ont été déplacés avec succès' success.tags_copied: 'Les tags ont été copiés avec succès' success.tags_deleted: 'Les tags ont été supprimées avec succès' +success.tags_hidden: 'Les étiquettes ont été masquées avec succès' +success.tags_revealed: 'Les étiquettes ont été révélées avec succès' success.translation_removed: 'La traduction de la locale "%locale%" a été supprimée avec succès' success.main_translation_set: 'La traduction pour la locale "%locale%" a été définie comme nouvelle traduction principale' success.always_available_set: 'Le drapeau Toujours disponible a été mis à jour avec succès.' +success.tag_hidden: 'Le tag "%tagKeyword%" a été caché' +success.tag_revealed: 'Le tag "%tagKeyword%" a été révélé' diff --git a/bundle/Resources/views/admin/eztags_content_field.html.twig b/bundle/Resources/views/admin/eztags_content_field.html.twig index 535befea..b7775908 100644 --- a/bundle/Resources/views/admin/eztags_content_field.html.twig +++ b/bundle/Resources/views/admin/eztags_content_field.html.twig @@ -1,5 +1,17 @@ +{% trans_default_domain 'netgen_tags_admin' %} + {% block eztags_field %} {% for tag in field.value.tags %} - {{ tag.keyword }}{% if not loop.last %}, {% endif %} + + {{ tag.keyword }} + {% if tag.isHidden %} + ({{ 'tag.hidden'|trans|lower }}) + {% elseif tag.isInvisible %} + ({{ 'tag.hidden_by_parent'|trans }}) + {% endif %} + + {% if not loop.last %}, {% endif %} {% endfor %} {% endblock %} diff --git a/bundle/Resources/views/admin/tag/children.html.twig b/bundle/Resources/views/admin/tag/children.html.twig index 7b21158e..66ba0f0f 100644 --- a/bundle/Resources/views/admin/tag/children.html.twig +++ b/bundle/Resources/views/admin/tag/children.html.twig @@ -5,6 +5,7 @@ {% set can_add = is_granted('ibexa:tags:add', tag is defined ? tag : null) %} {% set can_edit = is_granted('ibexa:tags:edit') %} {% set can_delete = is_granted('ibexa:tags:delete') %} +{% set can_hide = is_granted('ibexa:tags:hide') %}
{{ 'tag.hide.message'|trans({'%tagKeyword%': tag.keyword}) }}
+ + +{% endblock %} diff --git a/bundle/Resources/views/admin/tag/hide_tags.html.twig b/bundle/Resources/views/admin/tag/hide_tags.html.twig new file mode 100644 index 00000000..21970691 --- /dev/null +++ b/bundle/Resources/views/admin/tag/hide_tags.html.twig @@ -0,0 +1,43 @@ +{% extends netgen_tags_admin.pageLayoutTemplate %} + +{% trans_default_domain 'netgen_tags_admin' %} + +{% form_theme form '@NetgenTags/form/tags.html.twig' %} + +{% block content %} +{{ 'tag.hide_tags.message'|trans }}
+ + {{ form_start(form) }} +{{ 'tag.title'|trans }} | +
---|
+ + {% if parentTag is not null %}{{ parentTag.keyword }} / {% endif %}{{ tag.keyword }} + | +
{{ 'tag.reveal.message'|trans({'%tagKeyword%': tag.keyword}) }}
+ + +{% endblock %} diff --git a/bundle/Resources/views/admin/tag/reveal_tags.html.twig b/bundle/Resources/views/admin/tag/reveal_tags.html.twig new file mode 100644 index 00000000..4b4e10f1 --- /dev/null +++ b/bundle/Resources/views/admin/tag/reveal_tags.html.twig @@ -0,0 +1,43 @@ +{% extends netgen_tags_admin.pageLayoutTemplate %} + +{% trans_default_domain 'netgen_tags_admin' %} + +{% form_theme form '@NetgenTags/form/tags.html.twig' %} + +{% block content %} +{{ 'tag.reveal_tags.message'|trans }}
+ + {{ form_start(form) }} +{{ 'tag.title'|trans }} | +
---|
+ + {% if parentTag is not null %}{{ parentTag.keyword }} / {% endif %}{{ tag.keyword }} + | +