Skip to content

NGSTACK-977 tags visibility #169

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 41 commits into
base: master
Choose a base branch
from
Open

Conversation

AntePrkacin
Copy link

Added isHidden and isInvisible fields to Tag objects. Updated the database schema and updated the whole workflow for hiding and unhiding a tag (Service, Mapper, Handler, Gateway, ...).

Added columns 'Hidden' and 'Invisible' to table that show children tags of some tag in admin interface.
In admin interface added button for hiding a tag (or if a tag is already hidden, then this button will unhide it).
Also added buttons 'Hide selected tags' and 'Unhide selected tags' for the checked children tags.

When a tag is hidden, the route /tags/view/Topics/{tag_name} will throw 404 and return information that 'Tag is hidden' or 'Tag is hidden by parent'.

If a tag is hidden, then the user won't be able to add that tag to some 'eztags' content field for some content.
If some content already has a hidden tag, then this tag will have ' (hidden)' appended to it and it will be in gray color.

@AntePrkacin AntePrkacin requested review from emodric and pspanja June 6, 2025 08:58
@AntePrkacin AntePrkacin self-assigned this Jun 6, 2025
/**
* Hides $tag.
*
* @throws \Ibexa\Contracts\Core\Repository\Exceptions\UnauthorizedException If the current user is not allowed to delete this tag
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrong exception text If the current user is not allowed to delete this tag. Shoud be hide instead of delete.

Same goes for unhide.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed PHPDocs: c698f26

Will probably rename 'unhide' to 'reveal', as Petar suggested below

@@ -14,6 +14,14 @@ final class TagViewController extends Controller
*/
public function viewAction(TagView $view): TagView
{
if ($view->getTag()->isHidden) {
throw $this->createNotFoundException('Tag is hidden.');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need the message here. As this is the frontend, we do not want to communicate any revealing info.

As far as the user is concerned, the tag does not exist and that's it.

Also, here, you can only check for visible/invisible since when the tag is hidden, it should automatically be invisible too. That is, it is not possible for the tag to be hidden but not invisible too.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the message: c698f26

@@ -226,6 +226,16 @@ public function newTagUpdateStruct(): TagUpdateStruct
return $this->service->newTagUpdateStruct();
}

public function hideTag(Tag $tag): void
{
$this->service->hideTag($tag);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No event dispatching here and in unhide tag method?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Forgot about them... added them now: 69672a5

$query->expr()->like('path_string', ':path_string'),
)
->andWhere(
$query->expr()->neq('id', ':tag_id'),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When hiding a tag, the hidden root tag should also be invisible, so we don't need this neq condition.

This is the way Ibexa works when hiding content, so there is no need to do this differently here. It will only cause confusion in the long run.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, didn't know that this was how Ibexa works.
Removed the neq: f8bcf8d

$query->expr()->like('path_string', ':path_string'),
)
->andWhere(
$query->expr()->neq('id', ':tag_id'),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same remark about root tag invisibility goes here.

Also, I think we need to consider here what happens when some tag is already hidden explicitly below the tag being unhidden here. In that case, we can't really make tags below the already hidden tag invisible to maintain data consistency. We should see how Ibexa does this and just copy the implementation from there.

Copy link
Author

@AntePrkacin AntePrkacin Jun 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed the tag invisibility in the above comment.

Regarding the problem you mentioned about looking at the descendant tags and leaving them invisible if they have at least one parent that is explicitly hidden, I fixed it here:
8b20a72 54ec1e9 45b2f03

@@ -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) ((int) $row['is_hidden']);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need double cast here. You probably copied the thing from (bool) ((int) $row['language_mask'] & 1), but that has double cast due to bitwise and operator.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the double cast, kept only the bool one: e5b2f5f

@@ -9,3 +9,5 @@ tags:
deletesynonym: ~
makesynonym: ~
merge: ~
hide: ~
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're missing hidesysnonym and unhidesynonym policies here. Also do we really need policies for both hide and unhide?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added hidesynonym and unhidesynonym policies: 7e88bdd

I also thought that just one policy for hiding tags is enough, but I made two just for clarity.
If you want, I can only leave 'hide' and 'hidesynonym' policies, which will mean that the user can both hide and reveal tags.

>
{{ tag.keyword }}
{% if tag.isHidden %}
(hidden)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs translations too.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added translations here for '(hidden)' and '(hidden by parent)': dece52c

@AntePrkacin
Copy link
Author

Added show_hidden config parameter to the configTreeBuilder. By default it is set to true. This parameter can be changed for each site access.

Implemented the following methods of the TagsService class to look at this parameter:

  • loadTagChildren()
  • getTagChildrenCount()
  • loadTagsByKeyword()
  • getTagsByKeywordCount()
  • searchTags()
  • loadTagSynonyms()
  • getTagSynonymCount()

086e954 4af3ecd 0f8c7dd 75858b8

@AntePrkacin AntePrkacin force-pushed the NGSTACK-977-tags-visibility branch from 98cce00 to d09d660 Compare June 10, 2025 09:10
@@ -37,6 +37,7 @@ public function autoCompleteAction(Request $request): JsonResponse
$searchResult = $this->tagsService->searchTags(
$request->query->get('searchString') ?? '',
$request->query->get('locale') ?? '',
showHidden: false,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be controlled through the configuration as well?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hardcoded here false because the autoCompleteAction() method is used for searching a tag to add it in eztags field. If the showHidden parameter wasn't false here, then the user could add a hidden tag to some content by searching for it when editing a content.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, but there might be a use case where you prepare the Tags and Content in advance, and just reveal the Tags at some time point. I'll leave it to @emodric.

cc @Ljudevit for info, maybe you have a use case for this?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like to agree with @AntePrkacin on this one that we should always avoid outputting hidden tags in the autocomplete actions like searching for tags in tag fields, but there can be some quite silly edge-cases where editor would hide the tag so tags/view page is not visible yet, but they would like to start filling in the content with those hidden tags before revealing the tag all-together.

For sure this configuration parameter must be set to false as default, but for those pesky edge-cases, a parameter is needed.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, parameter is definitely needed.

Also, please do not use named parameters, at least not mixed like this, it just looks wrong :D Either change all of them to use named parameters, or none.

{
$this->denyAccessUnlessGranted('ibexa:tags:hide' . ($tag->isSynonym() ? 'synonym' : ''));

$this->tagsService->hideTag($tag);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to check the submitted button and CSRF token here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we do. There is an example in deleteTagAction method.

{
$this->denyAccessUnlessGranted('ibexa:tags:reveal' . ($tag->isSynonym() ? 'synonym' : ''));

$this->tagsService->revealTag($tag);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to check the submitted button and CSRF token here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we do. There is an example in deleteTagAction method.

@@ -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 $showHidden = null): TagList;
Copy link
Member

@pspanja pspanja Jun 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possibly we should have a default value for $showHidden here, instead of resolving it in the Persistence implementation. @emodric I'll leave that up to you :)

(Then the same applies for the similar cases below)

Edit: ignore the above, I think the parameter should not be optional in the Persistence layer, we probably need it to be optional here so we can properly handle it in the siteaccess-aware implementation.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with @pspanja , however, nullable bool seems weird. Either we show or hide hidden tags, there's no third option for null. So bool $showHidden = false is probably good enough.

Copy link
Member

@pspanja pspanja Jun 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@emodric one question though, if here the default is not null, how do we detect explicitly passed value in the siteaccess-aware layer?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm. Good point. Although, do we even have the need for explicit API layer control of this flag? Can we rely only on siteaccess aware config?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would say we do, this is not only for rendering, it might be used in import for example.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Import scripts can be run in admin siteaccess if needed.

It's just weird design, is all, since API interface presumes that somwhere down below, the value we give to the method might be overriden.

But okay, if there are valid usecases, I can live with it.

@pspanja
Copy link
Member

pspanja commented Jun 12, 2025

@AntePrkacin you can add upgrade instructions in doc/UPGRADE.md, including database migration, as was already done here: https://github.com/netgen/TagsBundle/blob/master/doc/UPGRADE.md#notes-on-upgrading-to-ez-platform-22

Use 5.4.0 as the next version.

@AntePrkacin
Copy link
Author

AntePrkacin commented Jun 17, 2025

Updated the doc/UPGRADE.md file with update instructions and sql queries to include is_hidden and is_invisible columns (08ef43d). Correct me if anything needs to be fixed.

@@ -666,6 +706,60 @@ public function deleteTagsAction(Request $request, ?Tag $parentTag = null): Resp
);
}

public function hideTagsAction(Request $request, ?Tag $parentTag = null): Response
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here with CSRF. What we need to do here is create a Symfony multiselect form for selecting the tags in lieu of moveTagsAction method. Same goes for revealTagsAction.

],
],
];
}

private function escape(string $string): string
private function formatTagTreeText(Tag $tag): string
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't mix escaping and formating. That is, leave escape method as is, and just reuse is in the new formatTagTreeText method. After all, all of the text should be escaped AFTER formatting it, and not just the starting text.

@@ -154,6 +161,13 @@ public function getChildren(int $tagId, int $offset = 0, int $limit = -1, ?array
}

$query = $this->createTagFindQuery($translations, $useAlwaysAvailable);

if ($showHidden !== null && $showHidden === false) {
Copy link
Member

@emodric emodric Jun 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If $showHidden were not nullable, as mentioned before, you wouldn't need double check here and in other places.

@@ -155,6 +155,10 @@ public function getConfigTreeBuilder(): TreeBuilder
->defaultValue(25)
->end()
->end()
->end()
->booleanNode('show_hidden')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should be explicit and name this show_hidden_tags.

@@ -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',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We also need MySQL and PostgreSQL upgrade scripts (see bundle/Resources/sql/upgrade folder).

If you're using MySQL, run the commands below:

```sql
ALTER TABLE `eztags` ADD COLUMN `is_hidden` TINYINT NOT NULL DEFAULT 0;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These should go to an upgrade SQL script as mentioned before and just referenced here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants