Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
111 commits
Select commit Hold shift + click to select a range
f28c11d
wip
jnptk Mar 23, 2026
04a0f9b
first working
jnptk Mar 30, 2026
5fdf327
make format
jnptk Mar 30, 2026
5f48385
add endpoint to get KeywordIndex indexes
jnptk Mar 30, 2026
ef8cfa3
add pagination
jnptk Mar 31, 2026
a7b577f
missing things for choosing the index thats queried
jnptk Mar 31, 2026
28bd1a5
rename endpoint to [at]keywords
jnptk Apr 1, 2026
f264a46
make endpoint only available for site root
jnptk Apr 1, 2026
a9d8fcf
add manage keywords permission
jnptk Apr 1, 2026
2c358b3
rename action path
jnptk Apr 1, 2026
a32eef3
request data validation
jnptk Apr 1, 2026
e390427
add tests for keywords service get & fix get methods reply
jnptk Apr 8, 2026
d15d190
more tests
jnptk Apr 8, 2026
23e095d
fix query building in get method
jnptk Apr 9, 2026
52fbd94
even more tests
jnptk Apr 9, 2026
04e1092
different fixes & styling
jnptk Apr 13, 2026
49ea914
prepare for to make endpoint traversable + fixes & styling
jnptk Apr 13, 2026
532b9b9
make endpoint traverseable
jnptk Apr 13, 2026
e4d3307
make format
jnptk Apr 13, 2026
8adddf7
fix typo in function import
jnptk Apr 13, 2026
f17560d
various styling fixes
jnptk Apr 14, 2026
d1bb2fd
rework single keyword view to use search endpoint
jnptk Apr 14, 2026
97d943f
fix cursor for buttons outside the table
jnptk Apr 14, 2026
58360fe
typing for km utility
jnptk Apr 14, 2026
dccb6db
implement sorting mechanisms & styling
jnptk Apr 16, 2026
886a0e5
fix tests
jnptk Apr 17, 2026
0d41438
don't return sorted keywords
jnptk Apr 17, 2026
8dcc3dc
support custom KeywordIndexes & more improvements
jnptk Apr 21, 2026
66717a3
remove shadow from buttons
jnptk Apr 21, 2026
99e5130
update confirm modal
jnptk Apr 21, 2026
3c79c07
move modals out of KeywordManager.tsx & more
jnptk Apr 22, 2026
8427764
reuse DeleteModal component
jnptk Apr 23, 2026
ffe68ff
Fix babel.config.js, extract i18n
davisagli Apr 23, 2026
efb3e5a
Run make format
davisagli Apr 23, 2026
ca97f10
Fix storybook config
davisagli Apr 23, 2026
0dd894d
fix typos
jnptk Apr 24, 2026
274e1cd
remove unicode fallback for python 2
jnptk Apr 24, 2026
4f07d0b
remove fallback for archetypes
jnptk Apr 24, 2026
abc1d54
update permission name in config
jnptk Apr 24, 2026
ab4227f
pagination & styling for keyword view
jnptk Apr 24, 2026
5b0910f
add option to filter by type
jnptk Apr 24, 2026
df09bdb
add filtering options in keyword view
jnptk Apr 28, 2026
bf7a769
make keyword view aware of the index a keyword is from
jnptk Apr 29, 2026
3656bb9
update to volto 19.0.0-alpha.33
jnptk May 8, 2026
7e865fe
styling + a bunch of fixes
jnptk May 8, 2026
f07af32
don't reload the entire page when sorting
jnptk May 26, 2026
6655f3e
hide choice of keyword index if there is only one
jnptk May 26, 2026
b6ece78
update locales
jnptk May 27, 2026
4eeaeba
show text if no search results
jnptk May 27, 2026
96a980b
naming
jnptk May 27, 2026
047aaad
remove action column, show delete button on the keyword pills
jnptk May 27, 2026
53ddc84
dynamically get index field from object
jnptk May 27, 2026
ee1d71a
make delete endpoint context aware
jnptk May 29, 2026
65baed6
fix keyword deletion in individual keyword view
jnptk May 29, 2026
2c9f56a
update scaffold, fix missing ts type declarations
jnptk May 29, 2026
f45579c
fix type declarations for react-toastify
jnptk May 29, 2026
048d9f8
fix selected row hover
jnptk May 29, 2026
5b547dd
truncate keyword pills with show more button
jnptk Jun 1, 2026
2039e64
update keyword manager description
jnptk Jun 1, 2026
dd73947
remove unused import
jnptk Jun 1, 2026
18641e1
add loading indicator while bulk keyword changes are being processed
jnptk Jun 1, 2026
ce731d7
align show more button with keyword pills
jnptk Jun 1, 2026
91f6f58
display path & title on separate lines in keyword view
jnptk Jun 1, 2026
f27645d
fix bulk delete doing nothing in keyword view
jnptk Jun 1, 2026
394b486
locales
jnptk Jun 1, 2026
fd7375e
make format
jnptk Jun 1, 2026
41a1c8e
remove direct dependency of react-aria-components
jnptk Jun 2, 2026
1434903
use Table component from react-aria-components (from 'catalog:') & so…
jnptk Jun 5, 2026
9c43d80
few fixes
jnptk Jun 5, 2026
68321d7
show toast after successful/failed actions
jnptk Jun 8, 2026
b7fd33c
rename & merge: use "edit" (pencil) icon instead
jnptk Jun 8, 2026
bff5a0b
fix sorting icon in table header
jnptk Jun 8, 2026
8313f42
style table heading
jnptk Jun 9, 2026
36d7ac1
refactor toasts, add toasts for individual keyword view
jnptk Jun 9, 2026
e0e1dca
remove pagination from keywords view
jnptk Jun 10, 2026
7df21b7
add a11y complient pagination component
jnptk Jun 11, 2026
67d3fa1
refactor table in individual keyword view
jnptk Jun 12, 2026
5a3311a
improvement to the styles
jnptk Jun 15, 2026
c0fd064
border styles
jnptk Jun 15, 2026
34e5b26
remove unused import
jnptk Jun 15, 2026
8d19830
consistent placeholder styling
jnptk Jun 16, 2026
aaa0940
fix table inner text alignment
jnptk Jun 16, 2026
dcf79f0
fix filter button
jnptk Jun 16, 2026
e836c54
remove unused import
jnptk Jun 16, 2026
26a793d
fix services tests
jnptk Jun 16, 2026
282cc3d
add changelog entries
jnptk Jun 16, 2026
5ca5d10
fix keyword index selection
jnptk Jun 23, 2026
3d6ff19
fix search button in individual keyword view
jnptk Jun 23, 2026
5735836
fix multi-select fields in individual keyword view not showing select…
jnptk Jun 23, 2026
82e9a9b
add confimation dialog to single keyword delete button
jnptk Jun 25, 2026
1793f79
more specific texts for rename modal if only one keyword is selected
jnptk Jun 25, 2026
9d82358
improve toast messages & add translations
jnptk Jun 26, 2026
5c8389d
add VLT as dev dependency
jnptk Jun 29, 2026
0e243af
move to scss
jnptk Jun 29, 2026
19983df
fix text wrap with ellipsis in individual keyword filters
jnptk Jun 29, 2026
8b24684
fix color of trash icon in rows
jnptk Jun 29, 2026
e85c028
improve a11y
jnptk Jun 30, 2026
d0e16dd
make format
jnptk Jun 30, 2026
96ccd53
remove box-shadow on buttons
jnptk Jun 30, 2026
8f219c1
fix cursor not pointing in row delete
jnptk Jun 30, 2026
5df2a56
use custom Select to fix popover position in rename modal
jnptk Jun 30, 2026
505b31b
fix overflowing select values
jnptk Jul 17, 2026
0bd4b10
replace temp popover fix with upstream fix
jnptk Jul 17, 2026
38e381f
fix sorting icons
jnptk Jul 17, 2026
e7d5e3f
fix keyword deletion
jnptk Jul 17, 2026
db58601
fix keyword deletion from single item (via pills)
jnptk Jul 20, 2026
adeb3a9
move KeywordList component
jnptk Jul 20, 2026
f1578ba
update error message
jnptk Jul 20, 2026
fb7744e
update README.md
jnptk Jul 20, 2026
4090f00
update README.md
jnptk Jul 21, 2026
62903d6
remove docs workflow
jnptk Jul 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ dependencies = [
"plone.api",
"plone.restapi",
"plone.volto",
"Levenshtein"
]

[project.optional-dependencies]
Expand Down Expand Up @@ -182,4 +183,4 @@ branch = true
parallel = true
omit = [
"src/kitconcept/keywordmanager/locales/*.py",
]
]
22 changes: 22 additions & 0 deletions backend/src/kitconcept/keywordmanager/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
MANAGE_KEYWORDS_PERMISSION = "Manage Keywords"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This should be

Suggested change
MANAGE_KEYWORDS_PERMISSION = "Manage Keywords"
MANAGE_KEYWORDS_PERMISSION = "kitconcept.keywordmanager: Manage Keywords"

to match the permission name in permissions.zcml.

(It doesn't really matter, since this is only used to protect the methods on the tool, and it is always called from trusted Python rather than an untrusted Zope template or script, so the permissions are not enforced.)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I want to off-source the config options here into registry records maybe? For example if a project has a KeywordIndex that you should not be able to manage via the keyword manager you would put it in IGNORE_INDEXES, what do you think?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@jnptk I would only go to registry records here if there's a need for editing it through the web. A project can already do:

from kitconcept.keywordmanager import config
config.IGNORE_INDEXES += ["my_custom_index"]

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

how would one include that python file so plone reads it? I want to write it down in the readme


# Meta type of the keyword indexes. If you're one of those crazy people that use
# custom indexes, you'll want to update this.
META_TYPE = "KeywordIndex"

# indexes of META_TYPE we know we don't want to manage, because bad things(tm)
# will happen
IGNORE_INDEXES = [
"object_provides",
"allowedRolesAndUsers",
"getRawRelatedItems",
"getEventType",
"block_types",
]

# A list of indexes that should always be reindexed when merging or deleting
# keywords on objects. Most people won't need this.
ALWAYS_REINDEX = (
# 'Subject',
"SearchableText",
)
5 changes: 4 additions & 1 deletion backend/src/kitconcept/keywordmanager/configure.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,14 @@
file="permissions.zcml"
/>


<include file="dependencies.zcml" />
<include file="profiles.zcml" />
<include file="permissions.zcml" />

<include package=".services" />

<utility factory=".tool.KeywordManager" />

<!-- -*- extra stuff goes here -*- -->

</configure>
15 changes: 15 additions & 0 deletions backend/src/kitconcept/keywordmanager/interfaces.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,22 @@
"""Module where all interfaces, events and exceptions live."""

from zope.interface import Interface
from zope.publisher.interfaces.browser import IDefaultBrowserLayer


class IBrowserLayer(IDefaultBrowserLayer):
"""Marker interface that defines a browser layer."""


class IKeywordManager(Interface):
"""A utility that allows to manage keywords"""

def change(old_keywords, new_keyword):
"""Updates all objects using the old_keywords.

Objects using the old_keywords will be using the new_keywords
afterwards.
"""

def delete(keywords):
"""Removes the keywords from all objects using it."""
5 changes: 4 additions & 1 deletion backend/src/kitconcept/keywordmanager/permissions.zcml
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
<configure xmlns="http://namespaces.zope.org/zope">

<!-- -*- extra stuff goes here -*- -->
<permission
id="kitconcept.keywordmanager.ManageKeywords"
title="kitconcept.keywordmanager: Manage Keywords"
/>

</configure>
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<rolemap>
<permissions>
<!-- -*- extra stuff goes here -*- -->
<permission acquire="True"
name="kitconcept.keywordmanager: Manage Keywords"
>
<role name="Manager" />
<role name="Site Administrator" />
</permission>
</permissions>
</rolemap>
38 changes: 38 additions & 0 deletions backend/src/kitconcept/keywordmanager/services/configure.zcml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<configure
xmlns="http://namespaces.zope.org/zope"
xmlns:browser="http://namespaces.zope.org/browser"
xmlns:plone="http://namespaces.plone.org/plone"
i18n_domain="plone.keywordmanager"
>

<plone:service
method="GET"
factory=".get.KeywordsGet"
for="plone.base.interfaces.IPloneSiteRoot"
permission="kitconcept.keywordmanager.ManageKeywords"
name="@keywords"
/>
<plone:service
method="DELETE"
factory=".delete.KeywordsDelete"
for="plone.base.interfaces.IPloneSiteRoot"
permission="kitconcept.keywordmanager.ManageKeywords"
name="@keywords"
/>
<plone:service
method="PATCH"
factory=".update.KeywordsPatch"
for="plone.base.interfaces.IPloneSiteRoot"
permission="kitconcept.keywordmanager.ManageKeywords"
name="@keywords"
/>

<plone:service
method="GET"
factory=".get.KeywordIndexesGet"
for="plone.base.interfaces.IPloneSiteRoot"
permission="kitconcept.keywordmanager.ManageKeywords"
name="@keyword-indexes"
/>
Comment thread
jnptk marked this conversation as resolved.

</configure>
21 changes: 21 additions & 0 deletions backend/src/kitconcept/keywordmanager/services/delete.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from kitconcept.keywordmanager.interfaces import IKeywordManager
from plone.restapi.deserializer import json_body
from plone.restapi.services import Service
from zExceptions import BadRequest
from zope.component import getUtility


class KeywordsDelete(Service):
def reply(self):
data = json_body(self.request)
km = getUtility(IKeywordManager)
keywords = data.get("items") or []

if not isinstance(keywords, list):
raise BadRequest("")
if not keywords:
raise BadRequest("")

Comment thread
jnptk marked this conversation as resolved.
km.delete(keywords)

return self.reply_no_content()
32 changes: 32 additions & 0 deletions backend/src/kitconcept/keywordmanager/services/get.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from kitconcept.keywordmanager.interfaces import IKeywordManager
from plone.restapi.batching import HypermediaBatch
from plone.restapi.services import Service
from zope.component import getUtility


class KeywordsGet(Service):
def reply(self):
km = getUtility(IKeywordManager)
query = {"withLengths": True}
if idx := self.request.form.get("idx"):
query = {"indexName": idx}

keywords = km.getKeywords(**query)

batch = HypermediaBatch(self.request, keywords)
items = {{"name": name, "total": count} for name, count in batch}

keywords_data = {
"@id": batch.canonical_url,
"items": items,
"items_total": batch.items_total,
}
if batch.links:
keywords_data["batching"] = batch.links
return keywords_data


class KeywordIndexesGet(Service):
def reply(self):
km = getUtility(IKeywordManager)
return km.getKeywordIndexes()
27 changes: 27 additions & 0 deletions backend/src/kitconcept/keywordmanager/services/update.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from kitconcept.keywordmanager.interfaces import IKeywordManager
from plone.restapi.deserializer import json_body
from plone.restapi.services import Service
from zExceptions import BadRequest
from zope.component import getUtility


class KeywordsPatch(Service):
def reply(self):
data = json_body(self.request)
km = getUtility(IKeywordManager)

new_keyword = data.get("new_keyword") or ""
old_keywords = data.get("old_keywords") or []

if not isinstance(new_keyword, str):
raise BadRequest("")
if not new_keyword:
raise BadRequest("")
if not isinstance(old_keywords, list):
raise BadRequest("")
if not old_keywords:
raise BadRequest("")

km.change(new_keyword, old_keywords)

return self.reply_no_content()
Loading
Loading