Register a Content Planner filter tab in EXT:typo3_pagetree_facets
Context
konradmichalik/typo3-pagetree-facets has been released as 0.1.0. It provides a filterable page tree for TYPO3 v14 built on the core BeforePageTreeIsFilteredEvent, with a guided filter modal and — relevant here — two public extension points:
FilterTabInterface + RegisterFilterTabsEvent — a whole tab with its own token keys and modal UI
FilterOptionInterface + RegisterFilterOptionsEvent — a single extra value under an existing token key
The built-in tabs register through the exact same events, so there is no private shortcut we would be working around.
Filtering the page tree by editorial state (status, assignee, open comments) has been a recurring request for the Content Planner. This is the point where we can deliver it without owning a filter engine or a modal UI ourselves: we contribute a tab, the framework does the rest.
Constraint: typo3_pagetree_facets requires TYPO3 ^14.0, while CP supports ^13.4 || ^14.3. The integration must therefore be an optional dependency that is invisible on v13 and when the extension is not installed.
Goal
When typo3_pagetree_facets is installed on TYPO3 v14, a Content Planner tab appears in the page tree filter modal, allowing editors to narrow the tree to pages by content status, assignee and comment state — combinable with all built-in facets (site:, under:, doktype:, updated: …) through the framework's AND semantics.
Proposed token grammar
| Token |
Values |
Notes |
status: |
<uid>, comma-separated for OR, plus none |
none = pages that have no status assigned |
assignee: |
me, <beUserUid>, none |
maps onto the framework's user-picker field type |
comments: |
open, resolved, todo, mine, none |
todo only offered when FEATURE_COMMENT_TODOS is on |
All three combine with everything else: status:3 assignee:me under:42 → pages in status 3, assigned to me, below page 42.
Open question: should status: accept a slugified title in addition to the UID? UIDs are not portable between installations and make a shared filter link meaningless across systems; a slug (status:in-review) reads better in the tree's search field. The modal would serialise whichever we pick, so this only affects hand-typed and shared tokens. Suggestion: accept both, serialise the slug, resolve UID as a fallback.
Scope note (verified against Classes/Domain/Model/Status.php): Status currently has no slug/identifier field at all — only title, icon, color. Deciding in favour of slugs is not just a serialisation choice; it implies a new DB field, a migration, a uniqueness rule, and backend UI to edit it. If we want this, it should be its own sub-task/acceptance criterion, not a footnote resolved inside the tab implementation.
Implementation sketch
Classes/
Integration/PagetreeFacets/
ContentPlannerTab.php # implements FilterTabInterface
RegisterTabListener.php # on RegisterFilterTabsEvent
getIdentifier() → contentplanner (also the key admins use for tx_typo3pagetreefacets.disableTabs)
getGroup() → state to sit next to the built-in Page state / Activity tabs, or an own section — decide when we see it rendered
getTokenKeys() → ['status', 'assignee', 'comments']
getModalConfiguration() → checkbox-group for status (with the status icon and color per option, description for the label), user-picker for assignee (pin the current user via currentUser), checkbox-group for comments
resolvePageUids() → returns a list<int> of page UIDs; the engine intersects across tokens, the core intersects with PAGE_SHOW permissions and mounts afterwards, so we do not need to replicate permission handling
serialize() / hydrate() must be exact inverses — the modal's token view depends on it
Content element statuses
CP tracks status on tt_content (and further tables via ExtensionUtility::getRecordTables()), but the page tree filter speaks in page UIDs. A content element in status In review should surface its page. Framework helpers already exist for exactly this shape — ContentQueryHelper::getPageUidsWithFieldMatch() and createRecordsExistExpression() (used by the built-in ContentElementTab) — so the tab should resolve both dimensions and union them per token:
- direct match on
pages.tx_ximatypo3contentplanner_status
- pages holding a matching
tt_content record (and any further registered table with a pid)
Open question: union or separate token? A hit on status:3 would be ambiguous — page-level or element-level? Options: (a) union and accept the ambiguity, (b) union plus a modal toggle "include content elements", (c) a second token key. Leaning towards (b), defaulting to on.
Registration and guards
Three layers, all needed:
composer.json → "suggest": { "konradmichalik/typo3-pagetree-facets": "Adds Content Planner filters to the TYPO3 v14 page tree filter" } — never require.
Configuration/Services.php → register the listener manually ($services->set(...)->tag('event.listener', ...), following the existing manual-registration style used for ConfigurableContentStatusWidget), guarded by class_exists(RegisterFilterTabsEvent::class).
Correction: this is not reuse of an existing "v14-only service" pattern — there isn't one for an optional third-party package today. Our one existing v14-only case (AfterFileStorageTreeItemsPreparedListener) still uses a plain #[AsEventListener] attribute, because the event class it references ships in both v13 and v14 core; only the runtime behaviour (the Label DTO) is guarded via VersionUtility::is14OrHigher(), and the file is PHPStan-excluded separately. ConfigurableContentStatusWidget's registration is guarded purely by Typo3Version::getMajorVersion() + $containerBuilder->hasDefinition(...). Here the dependency may not be installed at all, so RegisterFilterTabsEvent may not even be autoloadable — an attribute on it would break container compilation regardless of TYPO3 version. Going attribute-free with a class_exists() guard is the right call, but it's a new pattern for this codebase, not an existing one we're following.
- Feature flag →
enablePagetreeFacetsIntegration (FEATURE_PAGETREE_FACETS_INTEGRATION) via ExtensionUtility::isFeatureEnabled(), checked inside the listener.
Correction on default: the two most comparable existing flags — enableFrontendApi and enableEmbeddableCommentsView (both: optional integration adding new surface) — default to 0 in ext_conf_template.txt. For consistency this flag should default to 0 too. Also note: ExtensionUtility::isFeatureEnabled() checks array_key_exists() on the saved ExtensionConfiguration, and a brand-new template key stays absent from that array — and therefore resolves to false — until an admin opens and saves the extension configuration form once, independent of the template default. Worth stating explicitly in the docs/README so "installed the sibling extension but the tab doesn't show up" doesn't turn into a support question.
Status visibility
be_groups.tx_ximatypo3contentplanner_allowed_statuses restricts which statuses a group may use. The modal must only offer statuses the current user is allowed to see, and a hand-typed status:<uid> outside that set should resolve to no match rather than leaking the existence of the status. FilterContext::$backendUser is passed into both getModalConfiguration() and resolvePageUids(), so this is available without extra plumbing.
Acceptance criteria
Out of scope
- Filtering the file storage tree (
AfterFileStorageTreeItemsPreparedListener territory — separate issue if wanted)
- Any CP-owned filter UI; the modal belongs to the framework
- TYPO3 v13 backport — the underlying core event does not exist there
References
Register a Content Planner filter tab in EXT:typo3_pagetree_facets
Context
konradmichalik/typo3-pagetree-facetshas been released as0.1.0. It provides a filterable page tree for TYPO3 v14 built on the coreBeforePageTreeIsFilteredEvent, with a guided filter modal and — relevant here — two public extension points:FilterTabInterface+RegisterFilterTabsEvent— a whole tab with its own token keys and modal UIFilterOptionInterface+RegisterFilterOptionsEvent— a single extra value under an existing token keyThe built-in tabs register through the exact same events, so there is no private shortcut we would be working around.
Filtering the page tree by editorial state (status, assignee, open comments) has been a recurring request for the Content Planner. This is the point where we can deliver it without owning a filter engine or a modal UI ourselves: we contribute a tab, the framework does the rest.
Constraint:
typo3_pagetree_facetsrequires TYPO3^14.0, while CP supports^13.4 || ^14.3. The integration must therefore be an optional dependency that is invisible on v13 and when the extension is not installed.Goal
When
typo3_pagetree_facetsis installed on TYPO3 v14, a Content Planner tab appears in the page tree filter modal, allowing editors to narrow the tree to pages by content status, assignee and comment state — combinable with all built-in facets (site:,under:,doktype:,updated:…) through the framework's AND semantics.Proposed token grammar
status:<uid>, comma-separated for OR, plusnonenone= pages that have no status assignedassignee:me,<beUserUid>,noneuser-pickerfield typecomments:open,resolved,todo,mine,nonetodoonly offered whenFEATURE_COMMENT_TODOSis onAll three combine with everything else:
status:3 assignee:me under:42→ pages in status 3, assigned to me, below page 42.Open question: should
status:accept a slugified title in addition to the UID? UIDs are not portable between installations and make a shared filter link meaningless across systems; a slug (status:in-review) reads better in the tree's search field. The modal would serialise whichever we pick, so this only affects hand-typed and shared tokens. Suggestion: accept both, serialise the slug, resolve UID as a fallback.Implementation sketch
getIdentifier()→contentplanner(also the key admins use fortx_typo3pagetreefacets.disableTabs)getGroup()→stateto sit next to the built-in Page state / Activity tabs, or an own section — decide when we see it renderedgetTokenKeys()→['status', 'assignee', 'comments']getModalConfiguration()→checkbox-groupfor status (with the statusiconandcolorper option,descriptionfor the label),user-pickerfor assignee (pin the current user viacurrentUser),checkbox-groupfor commentsresolvePageUids()→ returns alist<int>of page UIDs; the engine intersects across tokens, the core intersects withPAGE_SHOWpermissions and mounts afterwards, so we do not need to replicate permission handlingserialize()/hydrate()must be exact inverses — the modal's token view depends on itContent element statuses
CP tracks status on
tt_content(and further tables viaExtensionUtility::getRecordTables()), but the page tree filter speaks in page UIDs. A content element in status In review should surface its page. Framework helpers already exist for exactly this shape —ContentQueryHelper::getPageUidsWithFieldMatch()andcreateRecordsExistExpression()(used by the built-inContentElementTab) — so the tab should resolve both dimensions and union them per token:pages.tx_ximatypo3contentplanner_statustt_contentrecord (and any further registered table with apid)Open question: union or separate token? A hit on
status:3would be ambiguous — page-level or element-level? Options: (a) union and accept the ambiguity, (b) union plus a modal toggle "include content elements", (c) a second token key. Leaning towards (b), defaulting to on.Registration and guards
Three layers, all needed:
composer.json→"suggest": { "konradmichalik/typo3-pagetree-facets": "Adds Content Planner filters to the TYPO3 v14 page tree filter" }— neverrequire.Configuration/Services.php→ register the listener manually ($services->set(...)->tag('event.listener', ...), following the existing manual-registration style used forConfigurableContentStatusWidget), guarded byclass_exists(RegisterFilterTabsEvent::class).Correction: this is not reuse of an existing "v14-only service" pattern — there isn't one for an optional third-party package today. Our one existing v14-only case (
AfterFileStorageTreeItemsPreparedListener) still uses a plain#[AsEventListener]attribute, because the event class it references ships in both v13 and v14 core; only the runtime behaviour (theLabelDTO) is guarded viaVersionUtility::is14OrHigher(), and the file is PHPStan-excluded separately.ConfigurableContentStatusWidget's registration is guarded purely byTypo3Version::getMajorVersion()+$containerBuilder->hasDefinition(...). Here the dependency may not be installed at all, soRegisterFilterTabsEventmay not even be autoloadable — an attribute on it would break container compilation regardless of TYPO3 version. Going attribute-free with aclass_exists()guard is the right call, but it's a new pattern for this codebase, not an existing one we're following.enablePagetreeFacetsIntegration(FEATURE_PAGETREE_FACETS_INTEGRATION) viaExtensionUtility::isFeatureEnabled(), checked inside the listener.Correction on default: the two most comparable existing flags —
enableFrontendApiandenableEmbeddableCommentsView(both: optional integration adding new surface) — default to0inext_conf_template.txt. For consistency this flag should default to0too. Also note:ExtensionUtility::isFeatureEnabled()checksarray_key_exists()on the savedExtensionConfiguration, and a brand-new template key stays absent from that array — and therefore resolves tofalse— until an admin opens and saves the extension configuration form once, independent of the template default. Worth stating explicitly in the docs/README so "installed the sibling extension but the tab doesn't show up" doesn't turn into a support question.Status visibility
be_groups.tx_ximatypo3contentplanner_allowed_statusesrestricts which statuses a group may use. The modal must only offer statuses the current user is allowed to see, and a hand-typedstatus:<uid>outside that set should resolve to no match rather than leaking the existence of the status.FilterContext::$backendUseris passed into bothgetModalConfiguration()andresolvePageUids(), so this is available without extra plumbing.Acceptance criteria
typo3_pagetree_facetsis installed, TYPO3 is v14 and the feature flag is onstatus:,assignee:,comments:resolve correctly, including thenonecasesallowed_statuses; disallowed UIDs resolve to no matchserialize()/hydrate()round-trip verified for every state the modal can producestatus:tokens are adopted: newStatusfield + migration + uniqueness handling, scoped as its own taskOut of scope
AfterFileStorageTreeItemsPreparedListenerterritory — separate issue if wanted)References
Tests/Functional/Fixtures/Extensions/example_tab/README.mdin that repositoryTYPO3\CMS\Backend\Tree\Repository\BeforePageTreeIsFilteredEvent(v14)