Skip to content

Register a Content Planner filter tab in EXT:typo3_pagetree_facets #329

Description

@konradmichalik

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:42pages 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:

  1. composer.json"suggest": { "konradmichalik/typo3-pagetree-facets": "Adds Content Planner filters to the TYPO3 v14 page tree filter" } — never require.
  2. 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.
  3. 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

  • Tab appears in the filter modal only when typo3_pagetree_facets is installed, TYPO3 is v14 and the feature flag is on
  • CP on TYPO3 v13.4 is entirely unaffected; container compiles with and without the extension present
  • status:, assignee:, comments: resolve correctly, including the none cases
  • Comma-separated values OR within a token; separate tokens AND across tokens
  • Status options respect allowed_statuses; disallowed UIDs resolve to no match
  • serialize()/hydrate() round-trip verified for every state the modal can produce
  • Content element statuses surface their parent page (per the resolution of the open question above)
  • If slug-based status: tokens are adopted: new Status field + migration + uniqueness handling, scoped as its own task
  • Functional tests with CSV fixtures for each token; unit tests for serialize/hydrate
  • README section documenting the tokens, the optional dependency, and the one-time extension-configuration save needed for the flag to take effect

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions