Academic Persons Edit 

Extension key

academic_persons_edit

Package name

fgtclb/academic-persons-edit

Version

main

Language

en

Author

FGTCLB

License

This document is published under the Creative Commons BY 4.0 license.

Rendered

Tue, 08 Sep 2026 15:25:07 +0000


This extension extends the Academic Persons extension by connecting person profiles to frontend users. It provides plugins to edit the assigned profiles from the TYPO3 frontend.


Introduction 

What the extension does and the main concepts behind it.

Installation 

Install academic_persons_edit via Composer, the Extension Manager or a TER upload.

Configuration 

Configure the extension and its plugins for your installation.

Profile editing 

Edit fields, synchronization and profile images through dedicated AJAX endpoints.

Templates 

Override and customise the frontend templates.

Known problems 

Known issues and information about them.

Changelog 

Learn about what has changed and which actions are required to upgrade.

What does it do? 

This extension provides the option to connect person profils from EXT_academic_persons to TYPO3 frontend users.

In addition this extension provides plugins to choose and edit an assigned profile. Therefore a frontend user with an assigned person profile has to be logged in.

Installation 

The extension has to be installed like any other TYPO3 CMS extension. You can download and install it using one of the following methods.

Requirements 

Version 3 requires TYPO3 13.4 or TYPO3 14.3, and PHP 8.2 or newer.

Updating an existing installation from 2.4 is a sequence of schema update, upgrade wizards, settings migration and template work, and the order of it matters - two steps destroy data when they are taken too early. It is described once, for both extensions, in Upgrading from 2.4 to 3.0.0 of academic_persons.

Two dependencies are new in this version:

  • typo3/cms-rte-ckeditor, a system extension. The profile editing frontend loads CKEditor 5 from it for its rich text fields: the rich text module imports six bundles, and Configuration/JavaScriptModules.php maps nineteen - those six plus the thirteen they import from each other. It has to be installed and active for them to resolve. The extension does not use the backend rich text configuration in any way - only the shipped JavaScript. Composer resolves it; in a classic installation activate it in the Extension Manager.
  • typo3/html-sanitizer, a Composer library the TYPO3 core already ships with - not an extension, so there is nothing to activate. Every rich text value is sanitized against an allow list on the server before it is stored, and the extension extends the builder of that package to declare it.
Install the stable release
composer require 'fgtclb/academic-persons-edit':'^3'
Copied!
  1. Switch to the module Admin Tools > Extensions.
  2. Switch to Get Extensions.
  3. Search for the extension key academic_persons_edit.
  4. Import the extension from the repository.
  1. Get the current version from TER by downloading the ZIP version. Alternatively, get the ZIP from the GitHub Releases page.
  2. Switch to the module Admin Tools > Extensions.
  3. Enable Upload Extension.
  4. Select or drag the extension ZIP archive and upload the file.

Configuration 

This extension ships its frontend TypoScript and its backend page TSconfig in two forms: as TYPO3 site sets, and as classic static templates plus page TSconfig files that are selected on a page. Both forms read the very same files, so they configure an installation identically.

Pick one of them per site and stay with it — see Do not combine both for what happens otherwise.

What the sets contain 

The extension ships the ProfileEditing component and one aggregate set that keeps the stable extension-level set name.

Set Delivers
fgtclb/academic-persons-edit-profile-editing The assigned-profile list, profile editor, AJAX page type and the page TSconfig that offers ProfileEditing in the backend.
fgtclb/academic-persons-edit ProfileEditing under the stable aggregate name. This is the normal set to use.

Both depend on fgtclb/academic-base-ctype-group, the set of EXT:academic_base that labels the content element group all academic extensions sort their elements into.

The plugin renders partials of EXT:academic_persons, but it reads them from Resources/, not from that extension's TypoScript. There is therefore no set dependency on any fgtclb/academic-persons-… set, and none is needed.

The content element is hidden by default 

EXT:academic_persons_edit hides the editing content type for the whole installation and brings ProfileEditing back per component. Whichever of the two mechanisms below you use, ProfileEditing is the only profile-editing content element offered in the backend.

Include the site set 

Add the set to the config.yaml of the site that should offer the content element:

config/sites/my-site/config.yaml (diff)
 base: 'https://example.com/'
 rootPageId: 1
+dependencies:
+  - fgtclb/academic-persons-edit
Copied!

See also TYPO3 Explained, Using a site set as dependency in a site.

The View action in the assigned-profile list uses the same public detail page as the Academic Persons list plugins. Configure plugin.tx_academicpersons.detailPid in the Academic Persons site settings or TypoScript constants. ProfileEditing copies that value into its own Extbase settings and targets the academicpersons_detail content element; it does not require a second page setting in Academic Persons Edit.

Include static templates 

For an installation that still configures its frontend through sys_template records, the same files are registered as static templates and as selectable page TSconfig files.

Include static TypoScript 

Edit the sys_template record of the site root and add the entry to Include static (from extensions):

Entry Delivers
Academic Persons Edit: Profile editing (academic_persons_edit) The TypoScript and AJAX page type of ProfileEditing.
Academic Persons Edit: All components (academic_persons_edit) Every component this extension ships, in one entry.

Include static page TSconfig 

Edit the page record of the site root, tab Resources, field Page TSconfig, and add the entry:

Entry Delivers
Academic Persons Edit: Profile editing (academic_persons_edit) Makes ProfileEditing selectable and configures its wizard entry.
Academic Persons Edit: All components (academic_persons_edit) Every component this extension ships, in one entry.

The setting is inherited by every page below the one it is set on.

Do not combine both 

A site that uses the site set and the static template reads the shipped files twice. The site set is applied before the sys_template record, so the second read happens after the site settings and after config/sites/<site>/constants.typoscript — and it resets every constant the extension ships a default for back to that default.

Nothing else is damaged: the Constants and Setup fields of the sys_template record, the page TSconfig of a page and the page TSconfig files selected on a page are all applied afterwards and still win. Use one mechanism per site and the question does not arise.

General configuration 

Extension configuration There are some options for global extension configuration:

profile.allowedLanguages

profile.allowedLanguages
type

string

Default
 

A comma-separated list of language IDs. These IDs configure in which languages a persons profile can be translated by a frontend user.

The synchronisation into these languages runs after a profile is auto-created — on frontend user login or through the academic:createprofiles command of EXT:academic_persons — and after every change persisted through ProfileEditing. Left empty, frontend edits do not touch translated profile records at all.

Which fields can be edited 

Which profile fields belong to each visual section, how they are rendered, which are mandatory and which are locked is configured by EXT:academic_persons in Configuration/AcademicPersons/Settings.yaml. The single profile map contains both the public layout and the editable field definitions. Structured records use the documentSections map from the same file.

Consequences worth knowing before reporting a problem:

  • A field configured disabled or readonly is rendered locked. The ProfileEditing JSON endpoint rejects attempts to submit it.
  • First name, Middle name and Last name are locked by default, because profile names are usually owned by the connected frontend user record and synchronised from elsewhere. They are therefore not editable in the frontend form - and not in the TYPO3 record editor either: the same validation set is merged into the TCA of the profile table through TcaValidationMerger , where disabled becomes readOnly . Unlocking a field for the frontend form unlocks it in the backend as well.
  • Document validators are selected by the section's stored record type; validators from sibling sections are never merged as a fallback.
  • The normalized rules are applied to the frontend controls, server-side Extbase validation and the corresponding backend TCA field state.

See Profile editor and validation for the schema, supported validator flags, document aliases, shipped defaults and override rules. The same profile map also controls the public detail layout.

Image processing: WebP 

The ProfileEditing image editor offers the profile image as WebP through the <picture> candidates, with the <img> fallback in the source format. TYPO3 has to be allowed to produce WebP, otherwise rendering a profile that has an image fails with:

Unable to render image uri in "tt_content:1": The extension webp is not
specified in $GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'] as a valid
image file extension and can not be processed.
Copied!

On TYPO3 v13 and v14 webp is part of the default value of $GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'] , so nothing has to be done. An installation that removes it from that list - some restrict the allowed formats deliberately - has to put it back, either in Admin Tools > Settings > Configure Installation-Wide Options > [GFX][imagefile_ext] or in config/system/settings.php.

Permitting the format is not the same as being able to produce it: the configured image processor, GraphicsMagick or ImageMagick, has to be built with WebP support.

Profile editor and validation 

academic-persons/Configuration/AcademicPersons/Settings.yaml is the canonical settings source for academic_persons and academic_persons_edit. It contains one ordered profile schema and the related inline maps:

profile
The public detail layout together with direct Profile fields grouped into visual sections. The structure and details keys configure public rendering; the remaining entries define editable fields and their shared validation metadata.
special
Composed or dedicated components such as the title, image and sync switch.
contracts
The reusable Contract document type. Its ordered fields map drives the Contract editor; contactSections contains the physical-address, email and phone editors and their respective ordered fields.
documentSections
Ordered structured collections. The Contract entry only references type: contracts; ordinary profile-information entries remain inline.

All consumers receive the same normalized settings graph. This keeps public rendering, profile editing, frontend validation and the corresponding backend TCA state aligned. Public layout stays below profile ; the other maps remain technical sections in the same file.

Loading and overrides 

The central factory reads the same relative path from every active package and merges maps at the top level. A site package can therefore override the shared configuration by providing Configuration/AcademicPersons/Settings.yaml. Replacing one of the four maps replaces that complete map; repeat every entry which must remain.

Flush TYPO3 caches after a change so the unified typed settings graph and its cache entry are rebuilt.

Field validators 

Regular fields declare an ordered list of flags:

profile:
  website:
    section: information
    fieldType: input
    renderType: combinedLink
    validators:
      - url
Copied!

Supported flags are:

Flag Effect
required Adds NotEmptyValidator plus the frontend HTML, marker and JSON metadata.
readonly Prevents editing in the frontend editor.
disabled Disables editing and implies readonly. A locked field can never remain required.
email Adds EmailAddressValidator and email input metadata.
url Adds UrlValidator and URL input metadata.
number, tel and date Select the matching frontend input type. date is the exception to that: it selects the date field type, which the profile editor renders as a plain text control showing d.m.Y. A date picker is deliberately not shipped yet, and the native <input type="date"> is not used - see the contract dates.
textarea and html Select text-area input; html also activates sanitized rich text.

Rules remain attached to the configured profile, contact or document section. They are not collected from a sibling section. Only submitted fields are validated during a partial AJAX update.

Profile CKEditor character limit 

A profile field configured as CKEditor may declare its character limit directly on the field:

profile:
  miscellaneous:
    section: aboutme
    fieldType: textarea
    renderType: ckeditor
    characterLimit: 1000
    validators:
      - html
Copied!

A positive integer characterLimit adds a live current / limit counter and prevents the editor from accepting additional visible characters beyond the limit. The partial AJAX update is checked independently by the same server-side Extbase validation. HTML tags do not count. The property is ignored when renderType is not ckeditor or its value is invalid or non-positive. The key is case-sensitive; characterlimit is not normalized.

This deliberately differs from document descriptions, which keep their existing nested editor.limit schema. Both forms normalize to the same typed validation metadata. Character limits do not alter backend TCA or the database schema.

Document validators and aliases 

Document aliases map presentation names to DTO and database properties:

Settings key DTO property Domain/database field
from yearStart year_start
to yearEnd year_end
contracts.fields.validFrom validFrom valid_from
contracts.fields.validTo validTo valid_to
year year year
description bodytext bodytext

The shipped year rules are deliberately dynamic:

documentSections:
  cooperation:
    validators:
      from:
        - number
      to:
        - number
      year:
        - required
        - number
Copied!

Consequently year receives a required attribute and marker while from and to remain optional. Removing or adding required in a site override changes the JSON metadata, the rendered controls, the Extbase validation and the corresponding backend TCA field state together; no field name is hard-coded as mandatory.

The richer description form remains supported:

description:
  editor:
    limit: 500
    type: ckeditor
Copied!

editor.type: ckeditor is normalized to the html validation/input metadata so the editor and the server side sanitizer agree. A positive integer editor.limit additionally defines the maximum number of visible characters. HTML tags do not consume that allowance; entities, non-breaking spaces and repeated whitespace are normalized before counting. The editor returns the limit in its JSON field metadata, displays a live current / limit counter and prevents CKEditor from accepting further characters beyond it. Both the document JSON actions and the Extbase form-data validator enforce the same limit on the server.

editor.limit is frontend validation metadata, not TCA configuration. It is ignored for non-CKEditor controls, and removing it or setting it to a non- positive value disables both the counter and the additional validation without changing backend FormEngine.

Contract form and contact sections 

The order of contracts.fields is the form order returned by the JSON endpoint. Every entry declares its frontend field and render type, validation, option source and helptext together. The shipped organisationalUnits, functionTypes and locations option sources resolve their records through the corresponding repositories. Removing or reordering an entry therefore removes or reorders that control without changing the template.

Physical addresses, email addresses and phone numbers are ordered structural children below contracts.contactSections . Each child owns an ordered fields map and an isolated validation set:

contracts:
  type: contracts
  fields:
    position:
      fieldType: input
      renderType: text
      validators:
        - required
    organisationalUnit:
      fieldType: select
      renderType: select
      options: organisationalUnits
  contactSections:
    physicalAddresses:
      fields:
        country:
          fieldType: input
          renderType: select
          autocomplete: country
          helptext: 'LLL:EXT:site/Resources/Private/Language/locallang.xlf:address.country.helptext'
          validators:
            - required
    emailAddresses:
      fields:
        emailAddress:
          propertyName: email
          fieldType: input
          renderType: email
          autocomplete: email
    phoneNumbers:
      fields:
        phoneNumber:
          fieldType: input
          renderType: phone
          autocomplete: tel

documentSections:
  contracts:
    type: contracts
Copied!

The shipped physical-address country control obtains its localized labels and ISO alpha-2 values from TYPO3's CountryProvider. Keeping fieldType as input preserves the domain extension's existing backend TCA while renderType: select selects the frontend control and its option validation. Every Contract and contact field helptext is rendered as a Bootstrap popover in add/edit mode.

The shipped autocomplete tokens are street-address, postal-code, address-level2, country, email and tel. They describe the purpose of the corresponding control without changing validation or storage.

Backend TCA integration 

The domain extension still owns the base TCA and the column configuration. It applies the normalized validation metadata from the unified settings graph to the relevant Profile, contact and Profile-information TCA fields. Consequently required, readonly and field-type metadata remain consistent between frontend validation, profile editing and FormEngine in TYPO3 13 and TYPO3 14. Character limits remain frontend/server-side metadata and do not change the database schema.

Year row 

year, yearStart and yearEnd each receive col-12 col-md-3. They therefore stack on small screens and share one row from the medium breakpoint. Each is an <input type="number"> carrying the bounds of its TCA range as min, max and step.

Profile editing 

The Profile editing content element first renders all profiles assigned to the authenticated frontend user. Its Edit action opens the selected profile in Profile editing; View opens the public academic_persons Detail plugin on the page configured through plugin.tx_academicpersons.detailPid. This is the same target setting used by the Academic Persons list views. The shipped Resources/Private/Templates/Profile/Index.html editor template contains four independently persisted areas:

  • profile fields using the generic JSON update endpoint,
  • the synchronization checkbox using its own JSON endpoint,
  • the expanding profile-image editor using dedicated upload and delete endpoints, and
  • the structured document sections — contracts, their contacts and the profile-information collections — each with their own form, create, update, delete and sort endpoints.

Fluid renders the markup of that view, and five custom elements own its behavior. The entry point is maintained as TypeScript in Resources/Private/TypeScript/frontend/profile.ts, from which the frontend build generates Resources/Public/JavaScript/frontend/profile.js; it defines those elements and does nothing else. Every editor on the page is an <academic-persons-edit-profile-editing> element that starts itself when the browser upgrades it, so a second plugin on the page — or one that is loaded into the page later — needs no start-up scan and cannot be started twice.

Typed feature modules below Resources/Private/TypeScript/frontend/profile/ separately own common requests and status output, field editing, documents, rich text, synchronization, image editing and sticky positioning; the five elements below …/profile/elements/ drive them. All of them are plain custom elements that depend on no user interface library, and none of them renders markup: each controls what Fluid rendered. No framework is bundled with the extension, and none is loaded from anywhere else either.

All changes are saved through AJAX without reloading the page. Editable fields are discovered across the complete component root, even when the responsive page layout places them in separate data-pe-fields-form elements. Every element renders into the light DOM — none of them has a shadow root — so a project's stylesheet reaches every control they render, exactly as it reaches the Fluid-rendered ones. The inline collapse targets, the two status regions and the client-side templates live in the same component scope.

Assigned profile overview 

ProfileController::listAction() is the default action. It uses the authenticated frontend-user relation and passes {profileListItems} to Resources/Private/Templates/Profile/List.html. Every item contains the Profile object and the title of its sys_language_uid from the current site configuration. The profile column contains a square thumbnail or the shipped placeholder followed by the complete name. Edit passes the explicit profileUid to indexAction(); the action resolves that UID again through the authenticated user's assigned profiles before rendering anything. An unassigned or manipulated UID receives the 403 access denied response of the site, the same one an unauthenticated visitor gets — raised as a propagated response rather than returned by the action, because on TYPO3 v13 the status code of an Extbase plugin response never reaches the frontend response.

The View URI uses extension academicpersons, controller Profile, action detail and plugin Detail. Its target page comes from plugin.tx_academicpersons.detailPid and must contain the academicpersons_detail content element. The ProfileEditing TypoScript copies the Academic Persons constant into plugin.tx_academicpersonsedit.settings; there is no separate editor-specific detail PID. Importing EXT:academic_persons/Configuration/Routes/Detail.yaml is optional and turns the query-string link into a speaking URI.

View data 

The controller assigns the following variables to the Fluid template:

Variable Description
{profile} Explicitly selected profile after its assignment to the authenticated frontend user has been verified.
{profileListItems} Assigned Profiles and their readable site-language labels. This variable belongs to the default list action and list template.
{profileSections} Ordered Fluid view models generated from profile . Every section contains regular fields and inserted composite special items.
{specialFields} Typed special components, including composed title, image and synchronization metadata.
{profileFieldOptions} Options for every configured renderType: select field. The option source remains the matching Profile TCA field.
{documentSections} Ordered structured-section view models derived from documentSections , including mapping, read-only and validation metadata plus the typed records.
{imageAllowedMimeTypes} Comma-separated MIME types accepted by the image input. The server validates the configured values independently.
{editorLanguage} The language code of the current site language, lower-cased and without its region. CKEditor is initialized with it; an empty string when the request carries no site language.
{data} and {record} Current content element data and its record object.

View structure 

The template is intentionally a composition root. The main partial groups are:

Partial group Responsibility
Image/Card.html and Image/Editor.html Profile image heading above the sticky page preview, animated full-width editor, file selection, crop preview and image actions.
Profile/Personal.html, Profile/About.html and Profile/Fields.html Personal-data and about-section boundaries plus ordered iteration of settings-driven profile fields.
Field/Editable.html, Field/Control.html, Field/Select.html, Field/Checkbox.html and Field/Group.html renderType-driven controls, previews, grouped values and persistence actions.
Documents/Sections.html, Documents/Contract.html, Documents/ProfileInformation.html and their *Row.html Structured document sections, their rows, the row actions and the hidden row prototype that is cloned after a create.
Documents/Editor.html The panel of the document editor, as a prototype the element clones; see How the document editor is rendered.
Header.html and StatusToast.html Complete profile-name heading with synchronization/edit-all controls, and scoped status output. The status output is two live regions, one assertive and one polite, because a region's politeness cannot be changed reliably once it is in the accessibility tree. The personal form renders its own Personal data heading. ButtonTemplates.html remains a compatibility fallback for existing template overrides; the shipped read view does not use its button-shaped value controls.

Layout and responsive behavior 

The view uses Bootstrap 5 grid, spacing, typography, background, positioning and form utilities; the Fluid templates contain no inline style declarations. Resources/Public/Css/frontend/profile-editing.css is generated from Resources/Private/Scss/frontend/profile-editing.scss by the repository build and holds only what Bootstrap cannot express: the display: block the custom elements need, a [hidden] rule that outranks Bootstrap's display utilities, three corrections to a surrounding theme (a .section overflow, one frame spacing variable, the stacking of the sticky card), the focus ring of the controls and buttons, the drag states of a sortable list and the enter/leave classes of the two editor transitions. All Bootstrap button controls of the shipped editor carry rounded-0 so their corners remain square.

The focus ring is the one appearance the view takes away from the surrounding theme rather than correcting. Bootstrap draws it with box-shadow, several themes layer further opaque rings behind that one, and a translucent accent ring over an opaque black one reads as a hard dark rectangle around the focused control. A shadow is also the wrong mechanism twice over: it is painted outside the border box, so the overflow: hidden of the two collapse panels cut the ring off the fields and buttons inside them, and forced-colours mode drops shadows altogether while the rules that draw them also set outline: 0, which leaves the appearance in that mode to the browser rather than to the site.

The view therefore draws its own ring as a real outline on :focus-visible — the pseudo-class the theme's own rules use, so the rule sits exactly where the defect is — sunk inside the border box with a negative outline-offset so that no ancestor can clip it, coloured with currentcolor so that it contrasts with whatever the control it sits on is filled with, and it rewrites box-shadow in the same rule so no layered ring and no inset shadow survives underneath. It applies to every input, select, textarea and button the view renders — the five controls of Partials/Profile/Field/Control.html, the image upload of Partials/Profile/Image/Editor.html, the synchronisation switch of Partials/Profile/Header.html and the buttons of every action group — because two focus appearances in one form would be worse than the one being replaced.

:focus-visible is not a synonym for keyboard focus. Measured in Chrome, a pointer click on a checkbox or on the synchronisation switch does not match it and those two keep Bootstrap's soft glow, while a pointer click on a text input or a select does match it and those take the inset ring. Most of what the view renders is in the second group, so the change is visible to a visitor using a mouse and not only to one using the keyboard. The colour is currentcolor, which is the colour a control draws its own text in — but not the colour of a checkbox tick or a switch knob, which Bootstrap paints as a background image with a hardcoded #ffffff. On those two the ring takes the inherited body colour, #212121 on the #577760 of a checked control with the shipped theme, and measures 3.23:1: enough for WCAG 2.1 SC 1.4.11 and no more, so a site with a darker $primary has to check that pair. Plain links are not covered, no theme rule takes their focus ring away. CKEditor 5 is covered by halves: its editable region is a div, so it is outside the rule and keeps the library's own focus styling, while the buttons of its toolbar are button elements below the plugin root and take this ring rather than the library's. That is the same answer as everywhere else - one focus appearance for everything the visitor can focus.

Above the grid, the complete profile name and the synchronization/edit-all controls share one responsive header row. The controls wrap below the name on narrow viewports. On lg and larger viewports the first content row uses a 4 / 8 column split. The profile image block has sticky-top so the image and its edit action stay visible while the profile data scrolls. Below lg both columns stack in document order. The about section follows the complete first row and therefore never overlaps the sticky column.

At runtime initializeStickyImageOffset() reads the visible outer height of #page-header.navbar-fixed-top through getBoundingClientRect().height, adds a 10-pixel visual gap and assigns the result to the top property of data-pe-sticky-image. The measurement itself is observePageHeaderOffset() of academic_persons, which the public detail view uses for its own sticky navigation — one implementation, two callers. A ResizeObserver watches the header's border-box and keeps the offset synchronized whenever the navbar changes height, including height or padding changes caused by a scroll-dependent header state. Environments without it use the window resize event as a fallback. If the matching fixed page header is absent, Bootstrap's regular sticky-top value remains in control.

The two columns live in their own align-items-stretch row. The image column inherits the stretched cross-axis size, giving the sticky image a containing block as tall as the adjacent profile data. The full-width about section keeps its own col-12 in a separate sibling row below it.

The complete profile name is the page's h1 above both columns. Both Fluid and JavaScript use the ordered fields list from special.title . Fluid renders the initial name; data-pe-profile-name-field-ids lets JavaScript recompose the same name after a successful update without reloading the page.

Profile values are rendered as readable text rows with alternating bg-body-tertiary surfaces. The only read-mode action is a borderless pencil button with an accessible label. Name components and the URL/title pair of each link share one preview row and open as one editing group. Only one field or group is ever open: a second pencil, and Edit all, discard the one that is open before they open theirs (Two editing modes). The special name editor retains the established responsive grid (academic title / first name at 4 / 8 and middle / last name at 6 / 6) without putting layout metadata into YAML.

Settings-driven controls 

ProfileSectionProvider converts the typed profile and special settings into an ordered Fluid view model. Section placement itself remains explicit in Templates/Profile/Index.html: the template decides where profileSections.information and profileSections.aboutme appear. It does not enumerate their individual fields.

Profile/Fields.html chooses the field control solely from renderType. Option values are not duplicated in YAML: for a select field, ProfileFieldOptionsService reads the corresponding Profile TCA items. Preview behavior likewise follows the rendered control type. Removing a special component removes its controls; marking the image or synchronization special readonly or disabled also blocks the matching write endpoint, not just its Fluid control.

A translated profile initially uses TYPO3's parent localization state for the image and follows changes to the default-language image. The first upload or deletion in that frontend language changes the image field to custom; the translated profile then owns its FAL reference and can select, replace or delete its image independently. The image controls therefore remain available while editing a translated profile; the endpoint still verifies that the requested profile is assigned to the authenticated frontend user.

The shared Resources/Private/Partials/Profile/Field/Editable.html partial composes three focused, reusable partials:

  • Field/Preview.html renders the text preview and pencil trigger — the trigger that opens this field and closes whichever other one is open,
  • Field/Control.html renders either f:form.textfield or f:form.textarea, including the CKEditor hook, and
  • Field/Actions.html renders delete, cancel and save.

Field/Group.html composes related textfields below one preview. Its data-pe-field-ids value defines which fields open, cancel and save together; data-pe-display-field-ids and data-pe-display-mode control whether the preview joins values (the name) or uses the first non-empty value (link title falling back to its URL).

Helptext buttons are edit controls: field and group previews do not render them. They appear after the corresponding field editor is opened. Document helptexts follow the same rule and are present in add/edit forms, but not in a document editor opened in view mode.

validation.inputType supplies the concrete HTML input type. Checkbox controls save immediately on change. Select controls use the same clear, undo and save actions as text fields. The synchronization switch in Header.html is persisted through its own endpoint.

Requirement Implementation
Telephone number Textfield with inputType: 'tel' or a validation input type of tel.
Website address Textfield with inputType: 'url' or a validation input type of url.
Free text input Default textfield.
Select Field/Select.html; options come from the configured field's TCA items and changes save immediately.
Checkbox Field/Checkbox.html for direct Profile flags. The synchronization special uses its own form and endpoint.
Multiline text Field partial with textarea: true. Passing richText: true additionally turns the textarea into the TYPO3 CKEditor 5 when the field is opened.

Two editing modes 

The profile fields have two mutually exclusive editing modes, and the controls say which one is active.

Single field. The pencil beside a value opens that field — or, for a group, that group — and the three buttons of Field/Actions.html act on it: clear empties the control and keeps it open, undo restores the saved value and closes it, save posts what changed. This is the mode described everywhere else on this page, and the editors of the document and contract panels behave the same way.

Only one editor is open at a time — one field or group, the whole form, one document row or one contact of a contract. Opening another one, and entering full form editing, closes the one that is open first. An editor that still holds the values it was opened with closes silently, exactly as its own Undo or Cancel would close it. An editor with changes does not: the view asks, in a dialog of its own rather than a browser prompt, whether to Save and continue, to Discard changes or to Keep editing. Saving stores exactly what the open editor's own save would store and then opens the other editor; a save the server refuses keeps the visitor in the refused editor with its messages, and opens nothing. Discarding puts the editor back to the stored values and announces it in the polite live region: Unsaved changes were discarded. Keeping — and Escape in the dialog — opens nothing at all. Pressing the pencil of the field or group that is already open changes nothing and keeps what has been typed.

The dialog is the unsaved-changes template of Partials/Profile/UnsavedChanges.html, a <dialog> element the view clones and shows modally; its labels are the extension's own and are overridable with the partial. A site that drops the template leaves the visitor in the editor that is open: nothing is thrown away because a template is missing.

Full form. Edit all opens every editable field of the profile at once. While it is open, every per-field and per-group button group is hidden — including the undo beside an autosaving checkbox — and one bar governs the whole form. Field/FormActions.html renders it at the end of every data-pe-fields-form, and the shipped template has two of those (personal data and about me); each bar acts on all of them.

Control Behavior
Apply Posts every changed field in one request to the generic field update endpoint below. On success the stored values are written back into the controls, the previews and the name heading, the form closes and the focus returns to Edit all.
Undo Restores every field to the value that is stored and keeps the form open. Announced in the polite live region.
Discard Restores every field and closes the form. Pressing Edit all again does the same.

updateAction() validates the complete field map before it persists anything, so an apply either stores all of it or none of it. A refusal reverts nothing: the entered values stay where they were entered, each refused field is marked and described by its own message, the caret goes to the first of them, and the refusal is announced once rather than once per field.

While an apply is on its way to the server, Undo, Discard, Edit all and Escape do nothing. The request cannot be taken back, and reverting under it would leave the stored profile and the editor's own baseline disagreeing without anything on screen saying so.

The same holds for a save beside a single field: because opening an editor now discards one, a pencil, Edit all and the Delete content and Undo beside a field or group do nothing at all while such a save is on its way — not even open the editor that was asked for. All of them work again as soon as the answer has been written back, and while they do not, they say so in the polite live region: Please wait until the change has been saved.

A request that the server accepts and never answers therefore leaves the editors of that profile refusing until the page is reloaded. The frontend deliberately sets no timeout on its requests, so nothing here decides on the visitor's behalf that a slow save has failed.

A pencil pressed while Edit all is open does nothing at all, and says nothing: every field is already open, and the form's own bar is the way out of the mode.

A checkbox that saves on change does not save while the form is open — it is applied with everything else. Without that it would reach the database while the visitor is still deciding and Discard could not take it back. The synchronization switch of Header.html is outside the field forms and keeps saving immediately.

Keyboard and assistive technology 

  • Opening the form puts the caret in the first editable field.
  • The bar stands after the fields in document order, so tabbing on from the last field reaches Apply, Undo, Discard.
  • Escape discards the form, unless the caret is inside a CKEditor instance, where the key closes the editor's own balloon first.
  • Ctrl + Enter applies it, and so does Enter in a text field: the form is submitted and the submission is turned into an apply rather than into a page load.
  • Both keys are handled by the field form itself, so a document, contract or image editor that is open at the same time keeps its own handling of them.
  • The bar is a role="group" with its own accessible name. Edit all carries aria-pressed and names the forms it controls in aria-controls. Results are announced in the two live regions the editor already has. The restored notice and a success are polite; a failure and a refusal of the form are assertive, because the caret has just been moved to the first refused field and a polite message queued behind that is routinely dropped. A refusal beside a single field stays polite — it stands next to the control the visitor is already in.

What an override has to keep 

Profile/Fields.html renders Profile/Field/FormActions at its end; an override of it that drops the line leaves the profile with no way to apply the form. An override of the bar itself keeps the data-pe-form-actions element, its data-pe-form-reverted-message and the three buttons marked data-pe-form-apply, data-pe-form-undo and data-pe-form-discard. Everything else about it — the tags, the classes, the labels, the order — is Fluid, and none of it is spelled in JavaScript. The optional sectionLabel argument names the button group after the section it stands in, so a page with two field forms does not offer two groups of the same name.

Generic field update 

The URL is generated by f:uri.action for updateAction() with page type 1733735 (see The page type of the JSON endpoints). Requests must use POST, Content-Type: application/json and the request header X-Requested-With: XMLHttpRequest (see The request header the endpoints require). Only values changed since the last successful save are sent. An empty string clears a property when its section-local validation permits an empty value; omitted properties remain unchanged.

Partial profile update
{
  "profile": 123,
  "data": {
    "gender": "female",
    "website": ""
  }
}
Copied!

Unknown profile properties, configured read-only/disabled fields and select values not configured in the matching TCA field are rejected. The direct academic/honorific profile.title field is an ordinary configured Profile property; the composed display name is special.title . skipSync is a direct special property. A configured combinedLink additionally enables its matching *Title companion. All other writable Profile properties come from profile . Extbase validation errors are returned in an errors object keyed by property path and are mapped back to the corresponding form controls by the frontend module.

ProfileFieldOptionsService supplies both the presentation options and the strict allow-list validation for every configured select. Thus another select does not require a new Fluid partial, validator service or JavaScript branch.

Rich-text content fields 

The shipped configuration marks five profile properties with renderType: ckeditor . Four are displayed with the information section, while miscellaneous belongs to aboutme :

  • coreCompetences,
  • teachingArea,
  • supervisedDoctoralThesis,
  • supervisedThesis, and
  • miscellaneous.

The editor is TYPO3's shipped CKEditor 5 from the typo3/cms-rte-ckeditor package. The profile/rich-text.js module imports its CKEditor modules through TYPO3's JavaScript import map; it does not load an editor from a CDN. An editor instance is created lazily when a rich-text field is opened. If initialization fails, the original textarea remains available and the component reports an error.

The toolbar intentionally exposes only undo, redo, bold, italic, bulleted lists, numbered lists and links. Editor changes are mirrored into the underlying textarea, so required-field validation, changed-value detection and the existing JSON request remain the single persistence path. CKEditor's initial HTML normalization becomes the local comparison baseline; merely opening an editor therefore does not submit or rewrite legacy content.

Profile fields may configure a positive characterLimit next to renderType: ckeditor. Fluid then adds the limit metadata and an accessible live counter to that field. The shared rich-text module counts normalized visible text, keeps the last accepted value when an addition would exceed the limit and still allows older over-limit content to be shortened. The Extbase validator checks the submitted sanitized value again before the partial update is persisted. The shipped miscellaneous field uses a limit of 1000.

Outside edit mode, each rich-text field renders its formatted content directly and provides a borderless pencil action on the right. Empty values show a localized placeholder. The preview is initially rendered through TYPO3's HTML formatting pipeline and is replaced after a successful save with the sanitized markup returned by the server. The frontend applies the same strict tag, attribute and URI-scheme allowlist without assigning markup through innerHTML.

Each open text or rich-text field has three explicit actions. For renderType: ckeditor only, the action group sits in the label row with Bootstrap's ms-auto utility, leaving the editor itself full width. Other field types retain the action group beside their control. Selects and checkboxes save immediately when their value changes.

  • Delete (data-pe-dismiss) clears the current browser-side draft. The editor stays open and no request is sent.
  • Cancel (data-pe-cancel) restores the last successfully persisted value and closes only that field. No request is sent.
  • Save (data-pe-save) sends that field through the JSON AJAX endpoint. It closes the field only after a successful response or when there is no changed value to persist.

The action group uses Bootstrap utility classes to remain content-sized and align itself to the end of the editor row instead of stretching to the CKEditor height. No additional stylesheet or inline style is required. The Edit all toggle beside the page heading opens both regular fields and grouped rows, receives Bootstrap's active state and changes its label to Close all. Activating it again collapses every editor without saving or discarding browser-side drafts. There is no global footer action area; save and undo remain explicit per-field actions.

The pencil is rendered through TYPO3's core:icon ViewHelper. Template overrides may replace the icon but must retain the button's edit hook, data-pe-for target and accessible label. The profile value itself must not be placed back inside the button.

Server-side sanitization 

Client-side editor configuration is not treated as a security boundary. ProfileUpdateValidationService derives writable properties from the configured profile sections. It passes every configured renderType: ckeditor property through ProfileRichTextSanitizer before validation and persistence. The sanitizer uses TYPO3's allow-list based HTML sanitizer and permits only:

  • the tags p, br, strong, em, ul, ol, li and a;
  • the href attribute on a; and
  • local links and the URI schemes http, https, mailto and tel.

Scripts, event-handler attributes, style attributes, images, unknown tags and unsafe URI schemes are removed. The successful JSON response contains the normalized, sanitized values. The frontend replaces its local editor and preview state with exactly those returned values rather than trusting the submitted markup.

Each AJAX request is validated as a partial profile submission. The validator selects only submitted or explicitly overridden DTO properties from their configured profile sections. A required rule on an omitted sibling field therefore does not block a field update or the dedicated skipSync request. Submitted inline overrides are validated after normalization and sanitization.

The extension requires TYPO3 13.4 or TYPO3 14.3. These constraints include the HTML-sanitizer fixes published with TYPO3-CORE-SA-2026-006. Projects must still keep TYPO3 security updates current.

Editable structured profile sections 

The profile editing view renders the structured records directly below the About me field. ProfileDocumentSectionProvider supplies one ordered view model instead of duplicating relation mapping in Fluid. It reads all sections, including contracts, from the shared settings graph. That graph is built from the active packages' EXT:academic_persons/Configuration/AcademicPersons/Settings.yaml files, so configured order is also presentation order. The same graph supplies the public profile, frontend validation and backend TCA metadata.

For every document section the provider reuses the configured identifier, fieldName, record type, LLL label, readonly state, rowFields, actions and the section-local validations. The heading is translated directly from that label. A newly configured type consequently does not require another section registry in academic_persons_edit. The generic localized empty state is used until an identifier-specific message is added.

contracts contains FGTCLB\AcademicPersons\Domain\Model\Contract objects. Every other collection contains FGTCLB\AcademicPersons\Domain\Model\ProfileInformation objects. Contract rows show the configured values, which are start date and position in the shipped settings. Profile-information rows render only the configured values in their declared order. The aliases from, to and description map to yearStart, yearEnd and bodytext. All sections remain visible when empty and display a localized empty state.

The three year properties year, yearStart and yearEnd are four digit integers stored in nullable int columns. Their add/edit control is an <input type="number"> carrying min="0", max="9999" and step="1" - the bounds of the TCA range of the same columns, which the endpoint enforces again on every submission, so a client that ignores them is refused rather than clamped. Nothing about a year is formatted and nothing follows a locale.

The three controls each use col-12 col-md-3 and share one responsive row on medium and larger viewports. Their HTML and server-side required states come from the same validation set: only a field with the additional required flag must be filled. In the shipped settings this applies to year but not to from or to.

The two Contract dates validFrom and validTo are the only date fields of the editor, and their control is a plain <input type="text"> showing d.m.Y behind the hint dd.mm.yyyy - the control the previous editor had. It is not an <input type="date">: the native control follows the locale of the browser rather than the one of the site, it cannot be styled with the rest of the editor, and its calendar is not the date picker this editor is meant to get. A date picker is deliberately not shipped yet. It is a feature of its own, it will not be the browser's, and until it exists the date is typed.

The endpoint accepts two formats for such a field. d.m.Y is what the control shows and submits. Y-m-d is what the endpoint answered and accepted while the control was a native one and stays accepted, so a client written against that shape keeps working. Both are read strictly - the parsed date is formatted back and has to match what came in, so 32.01.2026 is refused rather than read as the first of February - and anything that is neither format is refused with The value must be a valid date. The read view is unaffected: a stored date is rendered as the MEDIUMDATE of the site language, the way the public profile renders it.

How the document editor is rendered 

The editor of a document row is rendered in the browser, and cannot be rendered anywhere else: its fields, their labels, their select options and their display values all come from the documentForm response, which the permission-checked endpoint decides per section, per record and per mode. Partials/Profile/Documents/Editor.html therefore renders the shape of that editor, as a <template data-pe-proto="document-panel"> block, and the custom element <academic-persons-edit-document-editor> clones it and fills its slots. The element is created by TypeScript/frontend/profile/documents.ts inside the collapse target below the selected section heading for add actions, and inside the selected record row for view, edit and delete actions. The element is handed the response as properties and is removed again when its close transition reports back. This avoids pre-rendering one hidden form per section and record while keeping the static structure in Fluid and mapping validation errors directly back to the returned field names.

Exactly one document collapse is open at a time, while the complete profile view remains visible — and it counts as the open editor of the profile, so a pencil of another row, of another section, of a profile field or Edit all closes it first and asks about unsaved changes exactly as described for the profile fields above. Activating the same add or view trigger a second time closes its collapse with the same cleanup as Cancel. Saving an edit keeps the collapse open with what it stored, so the next change starts from the stored record; creating and deleting close it. The contact editor inside a contract behaves the same way: switching to another contact of the contract asks about a changed one, and a saved edit stays open. The element is created where it is shown and is never moved: moving it would disconnect it, and a disconnect destroys the CKEditor instances below it. The collapse target keeps the unique ID the controller assigns it when it is first opened, now for aria-controls alone.

A hidden ordinary DOM container supplies the Fluid-rendered row prototype used after creation (data-pe-document-item-template). It is a plain container rather than an HTML template element, and the successful create response is rendered by cloning the row inside it.

The icons of a browser-rendered editor cannot be resolved in the browser: core:icon asks the icon registry, which knows the set this extension registers and whatever a site overrode. Under the prototype design that needs no mechanism of its own: an icon is rendered by Fluid inside the prototype that draws it — the help button of a field, the five row controls of a contact and the add control of a section — so it is part of the markup an override reaches and no module ever looks one up.

contract is retained as a separate document kind. It uses the same editor for its configured fields and appends three contract-specific contact sections in the read view: physical addresses, email addresses and phone numbers. Their field schemas and validation flags come from contracts.contactSections.<section>.fields in the shared settings graph. The Contract form itself follows the order and metadata in contracts.fields. Those sections, their rows and the editor of one contact are rendered by a second custom element, <academic-persons-edit-contract-contacts>, which the document editor creates in its own template. The contact editor stands below the contact-section heading for add actions and directly inside the selected contact row for view, edit and delete actions — the two placements Documents/ContractContactEditor.html renders one prototype for.

Every contact row of a contract also carries a visibility toggle, the first control of its action group: it flips the record's own hidden column - the TCA enablecolumns.disabled field of the address, email and phone number tables, exactly the switch the previous editor offered for these three record kinds - through the toggleContractContactVisibility endpoint, which is sent the target state rather than a "flip" so that a double press stores what the visitor saw. The button's label names the press - Hide in frontend or Show in frontend - and its glyph (academic-persons-edit-visible respectively academic-persons-edit-hidden) shows the state; it carries no aria-pressed, because a label that changes with the state would announce the opposite of what a pressed button does. A hidden row is drawn in the secondary text colour, its controls at full contrast, and carries a Hidden tag in front of its action group. The toggle follows the edit entry of the contracts allow-list.

Contracts and profile information rows have the same toggle as the hide entry of their section's actions list - configurable like every other action, listed first in the shipped settings, and withdrawn by readonly with the others. It flips the record's own hidden column through the toggleDocumentVisibility endpoint, which is sent the target state and answers the serialised record; the row is written from that answer (data-item-hidden, the Hidden tag, the label and the glyph of the toggle) without being rebuilt, so the caret stays on the button. A hidden record stays in the editor - it is the one place it can be shown again - while the public views keep reading the relations, which respect the enable fields.

Every writable section heading has an Add action. Record controls are rendered in the exact order of the configured actions list. The first row's move-up action and the final row's move-down action are disabled. A list with both directions also has a drag handle; dropping a row submits the complete UID order and the server accepts it only when it contains every current section record exactly once. After a successful mutation JavaScript updates the row collection, alternating background, sort controls and empty placeholder without reloading the page. The drag handle is hidden below Bootstrap's md breakpoint; the explicit up/down controls remain available on mobile.

The action group is drawn in one line and is never pushed below the values of its row. From the md breakpoint up it stands at the right edge of the row (col-md-auto flex-shrink-0 ms-md-auto) and the value columns give way to it: the date columns are col-md-2, the title or position column takes what is left, and a long word breaks (text-break) instead of claiming the width of the buttons. Below md the group drops onto a full-width line of its own below the values and is centred there. The list header carries the same column widths, so it stays above the values it names.

The add, view, edit and delete workflows share one inline collapse. Its field schema and current values are loaded through documentFormAction(). Contract fields include the current organisational-unit, function-type and location options. Profile-information fields use the section's validation metadata. In every mode the view heading uses the non-empty title field of the current record. New records, contracts and records without a title fall back to the translated section heading; the mode label remains as its prefix. A field carrying the html flag is rendered as a full-width CKEditor 5 control; ordinary textareas are full width as well. Such a field is an <academic-persons-edit-rich-text> element of its own, which creates the editor when it is connected and destroys it when it is disconnected, so a re-render of the editor around it — which every validation error causes — never reaches into the subtree CKEditor owns. Rich-text values are sanitized before persistence and parsed through the frontend sanitizer before the row or the read-only view receives the HTML. When that field's editor.limit is a positive integer, the textarea carries the normalized limit and the view renders an accessible live character counter below CKEditor. The count uses normalized visible text rather than the stored HTML. CKEditor rejects additions past the limit while still allowing an older over-limit value to be shortened. The JSON endpoint and the Extbase form-data validator independently reject over-limit submissions, so client-side code is not the security boundary. Character limits do not alter backend TCA or the database schema; the shared required, readonly and field-type metadata does. The document pending state is released before CKEditor is initialized, because CKEditor deliberately skips disabled controls. Every JSON request increments one shared busy counter. While at least one request is active the document shows the wait cursor, and the final request restores the previous cursor in a finally path. aria-busy is set on the region that is actually waiting — the plugin root while a profile field is saved, the section while its rows are sorted, the open document or contact editor, the image editor, the synchronization form — and never on <body>, which would make a screen reader stop reporting the rest of the page as well. This keeps failures and concurrent requests from leaving a stale loading state.

Opening a different document replaces only the in-memory editor schema and values after the new form has loaded. It never calls the submit action: document forms have no blur, change, teardown or focus-loss save hook. Only their explicit save/delete submit control persists a mutation. Focus moves into the first writable control (or the read/delete heading), returns to the action that opened a closed editor and remains on native buttons throughout keyboard sorting. Drag sorting is an optional pointer enhancement; the up/down actions remain the complete keyboard path.

All generated controls keep an explicit label. Validation errors have stable IDs referenced through aria-describedby and update aria-invalid; dynamic editor headings and expanded controls expose their relationships via aria-controls and aria-expanded. The field editors are not modal dialogs and deliberately do not trap focus. When the view enters delete mode, its submit control is rendered with btn-danger. Every other mode renders it with btn-primary.

An open editor carries its controls in one action bar below the form: Cancel, and the submit control in every mode but the read view. The panel header carries the heading alone. A read view has no form and therefore offers no control of its own - the row action that opened it closes it again and reports which state it is in through aria-expanded.

That row action also looks like the way out while the panel stands: the eye is swapped for a crossed-out eye and its label changes from View to Close details. Both icons and both strings are rendered by Fluid into the button - the second icon carries the hidden attribute - so an override reaches them the way it reaches every other label of the editor. The browser only flips hidden and rewrites aria-label and title.

With a keyboard that action is reached by tabbing backwards out of the panel. The panel is rendered directly after the action group of the row it belongs to, so Shift + Tab passes the actions of that row and arrives at the control that opened it. No key closes a panel from the inside: the editor of a single profile field is the only place Escape is bound, and a document or contact panel is not a modal dialog, does not trap focus and leaves the page below it reachable.

An override of Documents/Editor.html or Documents/ContractContactEditor.html that still renders the removed header control keeps working. Its condition data-pe-when="showClose" is no longer filled, an unfilled condition counts as not met, and the control is removed with it - such an override renders the panel without the second Cancel rather than failing to render it at all.

createDocumentAction(), updateDocumentAction(), deleteDocumentAction() and sortDocumentAction() complete the document JSON API. Up, down and drag-and-drop intentionally share the sort endpoint. All endpoints resolve the profile from the authenticated frontend user and then resolve a record only inside the requested section. A UID from another profile, model kind or profile-information type is therefore rejected. They additionally enforce readonly and the configured action list. The shipped contracts section is writable and exposes the complete configured Contract field set.

The dedicated contractContactForm, createContractContact, updateContractContact, deleteContractContact and sortContractContact actions operate below a Contract resolved through the authenticated Profile. A contact UID from another Contract is rejected. New contacts are appended with a normalized sorting value; the up/down controls persist their order independently in each contact section. The first and last controls are disabled at their respective boundaries. Read-only Contract configuration blocks every contact mutation at the endpoint as well. Contract and contact helptexts use the same edit-only Bootstrap popovers as other inline fields. Physical-address countries are selected from localized TYPO3 CountryProvider options and persisted as ISO alpha-2 codes; submitted values outside that list are rejected by the endpoint.

The ProfileEditing plugin registers only ProfileController. All profile, contract, profile-information and contact mutations are handled by its normal or non-cacheable action map.

Section order is centralized and every section emits data-section-key and data-section-position together with the configured data-section-field-name, data-section-record-type, data-section-kind and data-section-readonly. Records additionally emit data-item-uid, data-item-sorting and data-item-position. data-section-sortable exposes whether both sorting directions are available. The explicit up/down controls and drag handle persist the same record order.

The presentation uses Bootstrap rows with one shared desktop column heading, compact flat records, separating borders and alternating tertiary backgrounds within each document section, which are a :nth-child(odd) rule of the extension's stylesheet rather than a class on the row. The year columns remain narrow while title and position columns consume the available width. On small viewports every record repeats its field labels instead of rendering the desktop heading. An empty section keeps its heading and add action, followed by one unobtrusive translated status line. During drag sorting the browser uses the complete record row as the drag image. Extension-specific state classes outline both the source row and active list, while a prominent line above or below the hovered row marks the exact insertion edge. Dropping into free list space shows the same line at the end of the list.

Profile-editing development boundary 

academicpersonsedit_profileediting is the only profile-editing content element offered in the backend CType selector and new-content-element wizard. All profile-editing behavior is implemented through the Profile template and partial tree, ProfileController AJAX actions and the inline JavaScript component. The former dedicated controllers and their Fluid trees are removed; they are not compatibility entry points.

The ProfileEditing TypoScript, AJAX page type, site set and page TSconfig live in their own Configuration/*/ProfileEditing components. ProfileEditing is the only component enabled through either its component configuration or the aggregate.

The ProfileEditing functional test setup reflects the same boundary. It uses a dedicated academicpersonsedit_profileediting fixture and the neutral AbstractFrontendProfilePluginTestCase base.

Synchronization checkbox 

The synchronization checkbox appears as the compact Private switch immediately left of the Edit all/Close all toggle in the page header and is persisted immediately through updateSkipSyncAction(). Its form is a sibling of the profile form, not a nested form. Its presence and writable metadata follow the special.skipSync configuration; the underlying data and endpoint semantics remain skipSync. It does not submit or mutate any other field. The endpoint accepts exactly one boolean property:

Synchronization update
{
  "profile": 123,
  "data": {
    "skipSync": true
  }
}
Copied!

Any additional property or a non-boolean value returns invalid_payload. On failure the JavaScript restores the last successfully persisted checkbox state.

Expanding profile image editor 

Clicking the compact edit button below the current profile image or its placeholder keeps the profile view active. Partials/Profile/Image/Editor.html is rendered by Fluid into a dedicated full-width target above the profile header, data-pe-image-editor-target, which is where it is shown: it is hidden until the editor is opened rather than inserted then, so the container contributes its final width and height from the first paint and the cropper can initialize with the complete available width. The header, profile fields and structured sections remain in the same profile flow below the cropper; no separate view or overlay is involved. While the editor is open, the complete image-preview column is hidden and the profile-fields column changes from col-lg-8 to col-lg-12. The editor itself uses a bordered, padded surface. Closing it scrolls the restored image-preview column into view and focuses its edit action without causing a second browser scroll. The focus is restored only after two animation frames have applied the collapsed layout. A 1.5rem scroll margin keeps the restored preview clear of the viewport edge.

<academic-persons-edit-image-editor> is the element that drives that partial. It renders nothing of its own — the <f:form> carries the __trustedProperties signature the property mapper validates the upload against, and only the server can produce that — so it is a controller over Fluid's markup: it binds the events and writes everything the shipped view derives from the editing state, including the two column widths of Templates/Profile/Index.html.

Opening and closing is animated with a short vertical move and fade, driven by the …-image-editor-enter-active / -enter-from / -leave-active / -leave-to classes of the extension's stylesheet. An explicit CSS grid row expands and collapses the editor height, padding, margin and border instead of removing the complete block in one layout step. The open scroll leaves 2rem above the editor. The return scroll starts together with the collapse and uses the preview's calculated final position; native scroll anchoring is disabled only during that phase, so it cannot introduce a competing correction. Environments requesting reduced motion skip the transition, and the close then completes in the same frame rather than waiting for an animation that never runs.

The full-width btn-sm edit action sits directly below the preview. Its visible label and image upload icon are complemented by localized title and aria-label attributes.

The image editor deliberately has no state-dependent Add or Replace action. Selecting a file immediately replaces only the inline crop preview with a local object URL. The page preview and persisted profile remain unchanged until Save succeeds. A successful upload replaces both previews, collapses the editor and shows the saved image directly. During the leave transition, the preview remains hidden and the profile fields retain their full width. The regular 4 / 8 grid is restored only after the editor has finished collapsing, preventing competing layout changes while the page returns to the image preview. Cancel discards the selected preview and restores the persisted image. Delete is shown only while an image is persisted.

If special.image.renderType is cropper and special.image.settings.ratio contains a positive ratio such as 1x1 or 16:9, the editor instantiates the CropperJS module the TYPO3 core maps as cropperjs - version 1.6.1 on both supported core versions. The selection remains constrained to that ratio and only the generated cropped file is added to the multipart request. No CDN or other runtime request is used. The ratio remains fixed while the selection can be dragged and resized to choose the visible image area, and the cropped file is written at the resolution of the selected file rather than at the size the editor displays it, up to 2400 pixels wide. The cropper is instantiated only after a new local file is selected. A persisted profile image and the placeholder remain normal previews and cannot themselves become crop input. The original upload behavior remains active for every other render type.

The save button remains disabled until a new local file is selected. This also prevents an already persisted image from being fetched, cropped and uploaded again. While an upload or deletion is pending, all image controls are disabled and the active action displays a Bootstrap spinner. This prevents duplicate requests and leaving the image editor during a running operation.

Upload 

The image form is intercepted by the frontend module and sends multipart/form-data to uploadImageAction() through fetch(). The FormData object is built before the file control is disabled for the pending state. Disabled controls are omitted by the browser and would otherwise produce an apparently valid request without an uploaded image. Extbase's file handling service validates the configured maximum file size and allowed MIME types, stores the file in the configured target folder and updates the FAL relation. Authorization is checked before Extbase maps or stores the uploaded file. A replaced physical file is removed only when it has no other references. After persistence, the ordered non-empty values title, firstName, middleName and lastName are joined with spaces. That composed profile name is written to alternative and title of the profile's file-reference overlay, which is the language-correct place for it: a translation carries its own name there, and the file may be shared between the languages of a profile.

The metadata record of the uploaded file itself is written once, by the upload, and only where it is empty — the record sys_file_metadata carries is the backend editor's from then on, and no later change of the profile name touches it. It is written at all because nothing else fills it: alternative and title are the fields an installation running typo3/cms-filemetadata or fgtclb/file-required-attributes reports as missing required attributes for a file uploaded in the frontend.

The JSON response returns the composed name so the in-page preview immediately uses the persisted metadata.

The image editor does not render f:form.validationResults. Upload validation failures are returned as JSON and displayed in an alert inside the active view. The controller additionally compares the submitted image with the persisted FAL reference. It returns image_upload_missing with status 422 instead of reporting success when no new file arrived.

The cropper preserves JPEG, PNG and WebP as output formats and falls back to PNG for unsupported source types. Every upload remains a separate FAL file. When a profile already has exactly one independent image relation, replacing the image keeps the sys_file_reference uid but assigns the newly uploaded file to it through DataHandler and persists image=custom in the profile's l10n_state. The old physical file is removed only after no active reference uses it anymore. Localized or duplicate legacy references are rebuilt as one independent relation, so changing one language cannot overwrite another language's custom image.

The upgrade wizard academicPersonsEdit_repairLocalizedProfileImages repairs legacy shared, localized or duplicate image relations and inconsistent relation counters. It rebuilds every relation through the DataHandler and deletes no file: a file may be reached through a soft reference that sys_file_reference knows nothing about, and an unattended bulk run is the wrong place to act on that question.

All JSON actions propagate non-successful responses out of TYPO3's Extbase USER content rendering with PropagateResponseException. A JsonResponse returned by the action alone would contribute its body to the surrounding PAGE object while the outer frontend response retained status 200. Propagation therefore preserves the documented non-200 status codes for the AJAX client.

The relevant TypoScript settings are:

plugin.tx_academicpersonsedit.settings.editForm.profileImage {
    targetFolder = 1:/profile-images
    validation {
        maxFileSize = 2M
        allowedMimeTypes = image/jpeg,image/png,image/webp
    }
}
Copied!

Delete 

The delete button calls deleteImageAction() exclusively through AJAX. The endpoint accepts a POST JSON request without profile field changes:

Image deletion
{
  "profile": 123,
  "data": {}
}
Copied!

The profile relation is cleared first. The physical file is deleted only if no other record references it. The response includes deleted and hasImage so clients can synchronize their local state.

Authentication and responses 

Every endpoint above requires an authenticated frontend user and accepts only the profile assigned to that user. The generic update, synchronization and delete endpoints propagate machine-readable JSON errors. Image upload validation is also converted to and propagated as JSON by the controller's error action.

Status Error identifier Meaning
200 The request was persisted successfully.
400 invalid_request, invalid_json or invalid_payload The X-Requested-With header is missing, or the JSON or the request structure is invalid.
401 authentication_required No frontend user is authenticated.
403 profile_not_editable The profile is not assigned to the frontend user.
405 method_not_allowed A JSON endpoint was called with a method other than POST.
415 unsupported_media_type A JSON endpoint was called without `Content-Type: application/json`.
422 invalid_profile_data, validation_failed or image_upload_missing A field value or uploaded file is invalid.
500 internal_server_error An unexpected error occurred. Details are logged but not exposed in the JSON response.

Customizing the view 

Override Resources/Private/Templates/Profile/Index.html and the partials below Resources/Private/Partials/Profile/ through the regular template and partial root paths. The index keeps URL/data setup, the responsive main grid, the prototypes and the composition. The Profile, Documents, Image and Field directories group the corresponding UI responsibilities; the status regions and client-side button templates remain shared at the root.

Every file in that tree is an override point, including the two regions a browser builds at runtime. The editor of one document or contract and the contacts of one contract cannot be rendered as finished markup - their fields, labels, options and display values come from the documentForm and contractContactForm responses - so Fluid renders their shapes instead, as <template data-pe-proto="…"> blocks, and the elements clone one and fill it.

File What it renders
Partials/Profile/Prototypes.html Every shape the browser-rendered editors draw, once per page: the five controls, the three field rows, an option, a help button and a display row. It renders the three partials below.
Partials/Profile/Documents/Editor.html The panel of one document or contract, as the document-panel prototype.
Partials/Profile/Documents/ContractContacts.html The section, the row and one summary cell of a contact list.
Partials/Profile/Documents/ContractContactEditor.html The editor of one contact, for both places it is shown.
Partials/Profile/Field/Control.html The only place a form control is spelled - inline for the permanent profile fields and, with its prototype flag, into the prototypes.
Partials/Profile/Image/Editor.html The image editor. Its <f:form> and hidden fields must stay - only the server can sign __trustedProperties - and so must its data-pe-* hooks; what the template used to derive is written by the element.

A prototype is filled through exactly four attributes, and an override keeps them and their keys:

Attribute Meaning
data-pe-slot="key" The text of the node becomes the value.
data-pe-attr="attribute:key …" Those attributes take the value; an absent or false value removes them.
data-pe-when="key" The node is removed when the value is falsy.
data-pe-list="key" Where repeated clones go.

What an override may change is every tag, every class and every label. What it may not change is the vocabulary - the data-pe-* hooks and the slot, condition and list keys - and what it cannot change is the order the elements insert things in and which slot carries which value; both are TypeScript. See Important: The profile editor is Fluid, driven by five custom elements in the changelog.

The custom elements and their events 

Five element names are part of the public contract of this extension from version 3.0 on. The prefix is the extension key with its underscores replaced, because a custom element name is global and has no scoping mechanism.

Element What it owns
<academic-persons-edit-profile-editing> One editor. It wraps the plugin root, reads its data-* contract once and starts everything below it. It renders nothing.
<academic-persons-edit-image-editor> The profile image editor, as a controller over the server-rendered upload form. It renders nothing.
<academic-persons-edit-document-editor> One open document or contract editor, in add, view, edit or delete mode. It clones the document-panel prototype and fills it from the documentForm response. It renders nothing.
<academic-persons-edit-contract-contacts> The contacts of one contract and the editor of one contact, from the section, row and editor prototypes. It renders nothing.
<academic-persons-edit-rich-text> One rich text field of a document editor and the CKEditor 5 instance on it. It wraps the textarea its prototype carries and renders nothing. The rich text fields of the profile itself stay Fluid-rendered textareas and are not wrapped in it.

Four events report what an open document editor did; the root element listens for a fifth, so that a descendant which does not hold the editing context can still have a status shown. All of them bubble, and none of them crosses a shadow boundary because there is none:

Event Meaning
pe:status Asks the root element to write one of the two live regions; detail { type, message? }. Listened for on the root, dispatched by nothing the extension ships.
pe:document-close The cancel button of an open editor was pressed.
pe:document-submit Its form was submitted; the browser's own submission is prevented.
pe:document-input A control changed; detail { name, value }.
pe:document-closed The close transition is over and the element may be removed.

Two vocabularies carry the rest, and both are unchanged in meaning by the move to custom elements. The plugin root of Templates/Profile/Index.html carries the configuration of this profile — fifteen endpoint URLs, the profile uid and the editor language, five image settings, twenty-six messages and six labels. It is read once, when the element above it starts the editor, and an attribute changed afterwards is not seen. Every control below the root carries a data-pe-* hook, including the controls an element clones out of a prototype: those carry the same hooks the removed partials did.

Keep the following contracts when reusing the shipped JavaScript. A hook that sits in a prototype is authored in Fluid like any other, so an override may retag, restyle and relabel it; what an override may not do is rename the hook, and what it cannot do is change the order the element inserts things in or which slot carries which value.

Selector or attribute Purpose
data-academic-persons-profile-editing Root component and scope for all queries. Everything below is read from it, and the <academic-persons-edit-profile-editing> element that wraps it is what starts the editor.
data-pe-missing-ajax-page-type The role="alert" message Templates/Profile/Index.html renders above the plugin root when the site delivers no PAGE object with typeNum = 1733735 , which is the one configuration error the editor cannot survive: every save would be answered with the site's error page instead of JSON. The same request logs the cause and names the site set to include. It is absent on a correctly configured site.
data-profile-uid and data-editor-language Positive profile identifier, and the language code CKEditor is initialized with.
data-update-url Generic field update endpoint.
data-skip-sync-url Synchronization endpoint.
data-delete-image-url Image deletion endpoint.
data-document-form-url, data-create-document-url, data-update-document-url, data-delete-document-url, data-sort-document-url and data-toggle-document-visibility-url The six document endpoints.
data-contract-contact-form-url, data-create-contract-contact-url, data-update-contract-contact-url, data-delete-contract-contact-url, data-sort-contract-contact-url and data-toggle-contract-contact-visibility-url The six contract-contact endpoints.
data-message-* and data-label-* The localized texts a browser-rendered control needs. They are on the root because a template cannot be reached from the browser.
data-pe-proto One <template> per shape a browser-rendered editor draws. The elements clone one and fill its data-pe-slot, data-pe-attr, data-pe-when and data-pe-list nodes.
data-pe-fields-form and academic-persons-profile-editing__field Generic field forms and controls. Separate forms preserve valid markup across the personal-data and about-section grid areas.
data-pe-rich-text and data-pe-editor-container Marks a textarea for lazy CKEditor initialization and its wrapper for show/hide handling.
data-pe-rich-text-preview and data-pe-rich-text-preview-content Direct formatted read preview and its safely replaceable content container.
data-pe-field-preview and data-pe-field-editor Plain read row and the inline control region for one field.
data-pe-profile-name and data-pe-profile-name-field-ids Main heading and the name controls used to refresh it after saving.
data-pe-sticky-image Sticky image container receiving the measured #page-header.navbar-fixed-top height plus a 10-pixel visual gap as its runtime top offset.
data-pe-document-sections and data-pe-document-section Editable structured-section list and stable boundary for its AJAX controls.
data-section-key and data-section-position Stable section identity and current zero-based presentation position.
data-section-field-name, data-section-record-type, data-section-kind and data-section-readonly Field, relation type, record kind and write state, taken from the shared settings graph for section-specific persistence.
data-pe-document-items and data-pe-document-item Mutable item collection and record boundaries inside a section.
data-pe-document-item-template Hidden Fluid-rendered row prototype retained in the mounted DOM and cloned after a successful create response.
data-pe-document-add-collapse-target and data-pe-document-item-collapse-target Where the <academic-persons-edit-document-editor> element is created: below a section heading for an addition, inside an individual record row for everything else. The document controller assigns a unique ID when a target is first opened, for aria-controls.
data-item-uid, data-item-sorting, data-item-position and data-item-hidden Persisted record identity, domain sorting value, current zero-based presentation position, and - present only on a hidden record - the styling anchor of the dimmed row.
data-pe-document-empty-state Localized placeholder rendered when a structured collection is empty.
data-pe-document-add, data-pe-document-view, data-pe-document-edit and data-pe-document-delete Section creation and in-place row actions.
data-pe-document-sort Up/down row action persisted through the shared sort endpoint.
data-pe-document-hide and data-pe-document-hidden-badge The visibility toggle of a row (its label names the press, its glyph shows the state) and the Hidden tag in front of its action group, both written from the answer of the visibility endpoint.
data-pe-document-view-container, data-pe-document-form, data-pe-document-heading, data-pe-document-fields and data-pe-document-field The collapse, the form, the heading, the field region used for add, view, edit and delete, and one control inside it. Fluid renders all of them: the first four in the document-panel prototype of Partials/Profile/Documents/Editor.html, the last through Partials/Profile/Field/Control.html, which carries the field name as its value. <academic-persons-edit-document-editor> clones the prototype and fills its slots.
data-pe-contract-contact-section, -item, -hidden, -heading, -form, -fields, -field, -editor, -actions, -add, -view, -edit, -delete, -sort, -hide, -cancel and -save The contact sections of a contract, their rows, the editor and its controls. Fluid renders them too: the section, the row and one summary cell in the contact-section, contact-row and contact-summary-cell prototypes of Partials/Profile/Documents/ContractContacts.html, the editor in the contact-editor-panel prototype of Partials/Profile/Documents/ContractContactEditor.html, and -field again through Partials/Profile/Field/Control.html. <academic-persons-edit-contract-contacts> clones and fills them; the presses are delegated on the plugin root like the document ones. -hidden marks a hidden contact row: it is the styling anchor of the dimmed row, and the hidden key that carries it also renders the Hidden badge. -hide is the visibility toggle of the row, see below.
data-pe-field-group, data-pe-field-ids and data-pe-display-field-ids Grouped preview/editor and the controls participating in it.
data-pe-group-edit, data-pe-group-dismiss, data-pe-group-cancel and data-pe-group-save Open, clear the draft, restore and persist a grouped field row.
data-pe-field-actions Content-sized Bootstrap group for the three per-field actions.
data-pe-autosave-on-change Saves configured checkbox controls immediately after a change.
data-pe-autosave-undo and data-pe-cancel Marks the undo action beside an editable checkbox. It restores the last successfully persisted value and closes the editor without sending another request. Select fields use the regular clear, undo and save action group.
data-academic-persons-profile-editing-edit-all-btn Enters and leaves full form editing: it discards the single field or group that is open, opens every editable field and grouped row at once, hides the per-field action groups and shows the form bars. Pressed again it discards the form, exactly as Discard does. It carries aria-pressed and names the field forms it controls in aria-controls.
data-pe-edit-all-label, data-pe-close-all-label and data-pe-edit-all-button-label Localized labels and replaceable label container for the edit-all toggle.
data-pe-dismiss Deletes the current draft value without closing or saving it.
data-pe-cancel Restores the last persisted value and closes one field without a request.
data-pe-save Persists one field through the generic JSON endpoint.
data-pe-sync-form and academic-persons-profile-editing__sync-checkbox Synchronization control.
academic-persons-profile-editing__image-form and data-pe-image-view-container AJAX-only multipart upload form and the editor panel around it. The form is server rendered and stays that way: it carries the __trustedProperties signature the property mapper validates the upload against, which only the server can produce.
data-pe-image-editor-target Profile-specific full-width container Fluid renders the image editor into, above the profile header.
data-pe-image-preview and data-pe-image-view-preview Image locations updated after upload or deletion.
data-image-render-type and data-image-cropper-ratio Render type and ratio consumed by the cropper of the image editor.
data-pe-status-toast="status" and data-pe-status-toast="alert" The two scoped status regions of the component: the polite one for saving, success and information, the assertive one for a failure. Both must exist — a region's politeness cannot be changed reliably once it is in the accessibility tree.

Not every hook in the table has a reader in the shipped JavaScript. Fifteen of them are override and styling anchors: they name a region so a stylesheet or a project's own script can address it, and no module of this extension looks them up. They are part of the contract for that reason and not by accident - an override may move them, and should keep them:

data-pe-autosave-undo          data-pe-document-kind
data-pe-compact                data-pe-document-section-header
data-pe-contract-contact-actions   data-pe-document-sections
data-pe-contract-contact-fields    data-pe-group-actions
data-pe-contract-contact-form      data-pe-helptext
data-pe-document-actions       data-pe-image-editor-heading
data-pe-document-fields        data-pe-profile-header
                               data-pe-rich-text-heading
Copied!

Of these, data-pe-compact is the only one a stylesheet of this extension uses; the rest exist for overrides.

One consequence of the prototype mechanism is worth stating: a clone keeps the data-pe-slot, data-pe-attr, data-pe-when and data-pe-list attributes of the block it came from. They are inert in the live DOM - the filler has already written the values they name - and they are not state. Do not read them, and do not style on them.

Every editable field needs one invalid-feedback element in its closest data-pe-field-wrapper, data-pe-group-control or .form-check wrapper. Inline collapse targets, views, status regions, icon templates and compatibility-template elements must remain inside the component root. All DOM lookups are scoped to that root, so multiple components remain independent.

State classes and selectors JavaScript uses 

No element and no prototype filler writes markup: every tag, every class attribute and every label of the editor is authored in Fluid. What the modules below them do is toggle state classes on markup that already exists, and select a few nodes by class. Both are part of the override contract - renaming one of them breaks the editor silently, because nothing throws when a classList.toggle() writes a class no stylesheet defines.

Class or selector Written or read for
d-none Everything that is shown and hidden by the field editing: a preview, an editor, a per-field action group, an empty section, the toast.
d-md-flex The header row of a structured document list, which is shown only while the list has rows.
is-invalid A control the server refused, on a profile field and on the synchronisation switch.
text-danger A rich text character counter that is over its limit.
text-body-secondary A preview that shows the empty label instead of a value.
active The Edit all toggle while it is pressed.
bg-danger, bg-success, bg-info, bg-warning The severity of the status toast; the four are exchanged, never combined.
.status-title and .status-message The two nodes of Partials/Profile/StatusToast.html a status is written into.
is-drag-active, is-dragging, is-drop-before, is-drop-after, is-drop-at-end The drag sorting of a structured document list and its insertion indicator.
is-image-closing The plugin root while the image editor collapses.
col-lg-4, col-lg-8, col-lg-12 and .academic-persons-profile-editing__profile-fields-column The two column widths the image editor exchanges while it is open.
.alert[role="alert"] and .spinner-border The message and the busy indicator of an open document or contact panel, written onto the panel that is already there rather than by rebuilding it.
.invalid-feedback, .form-check and .mb-3 The message element of a refused field, and the two wrappers it is looked up from when the field carries no data-pe-field-wrapper or data-pe-group-control.
.academic-persons-profile-editing__field Every editable control of the profile fields. This is the class the field editing enumerates by, so an override that drops it leaves the field unreachable.
.academic-persons-profile-editing__sync-checkbox The synchronisation switch of Partials/Profile/Header.html.
.ck CKEditor's own root, read to decide whether Escape belongs to the editor or to the form. It is the library's class, not this extension's, and is the one entry here an override cannot change.
…-enter-from, …-enter-active, …-leave-active, …-leave-to The collapse transitions, derived from the prefix of the editor that runs them. The declarations are in Resources/Private/Scss/frontend/profile-editing.scss.

Frontend build 

The package has no separate development toolchain below Resources/Public/. TypeScript sources, SCSS sources and the committed JavaScript and CSS output use the repository-wide frontend suites from the repository root:

Build/Scripts/runTests.sh -s buildJs
Build/Scripts/runTests.sh -s checkJsBuildClean
Build/Scripts/runTests.sh -s lintTypescript -n
Build/Scripts/runTests.sh -s typecheckJs
Build/Scripts/runTests.sh -s testJs
Copied!

Nothing is bundled: the build emits one module per source file and leaves every import as it was written. No JavaScript library is committed with the extension - both libraries the editor uses, CKEditor 5 and CropperJS, are resolved through the import map from rte_ckeditor and core.

Tests 

Controller unit tests cover method, payload and authentication errors for the JSON actions. Sanitizer unit tests cover the supported profile properties, allowed editor markup and rejection of scripts, event attributes, styles, unknown tags and unsafe link schemes. Validation-service unit tests verify that sanitization happens before a value is registered for persistence.

A JavaScript suite runs the shipped modules and elements against a DOM (Build/Scripts/runTests.sh -s testJs). It drives the rendered markup of the Fluid partials through the modules that read it, which is the only place the event handling, the optimistic list updates, the transitions and the focus management are executed at all.

Functional plugin tests render the inline-collapse targets, the prototypes and the image view, verify the decomposed Fluid contracts, AJAX-only controls, direct rich-text previews and the separate delete, cancel and save actions. They also assert the prototype inventory, every slot key of every prototype, and that a prototype control and the live control of the same type carry the same tag, the same classes and the same attributes. The section-provider unit test verifies that order, identifiers, field names, relation types and labels come from the shared settings graph while row fields, action capabilities, presentation modes and typed records are preserved. Functional fixtures cover contracts and every configured profile-information relation; the rendered page test derives the expected order and metadata from the same settings service, then checks placement below About me, alternating records, empty states, writable add controls and exactly the configured row actions. AJAX lifecycle tests cover schema loading, CKEditor inline fields, creation, partial updates, arrow and drag sorting, cross-section rejection, deletion and read-only endpoint denial. Contract tests additionally cover every configured field plus address, email-address and phone-number creation, display, update, independent sorting, deletion and cross-Contract rejection. They also guard the ProfileEditing plugin against accidentally exposing removed mutation controllers. Registration tests ensure that ProfileEditing is the only editing content element offered to editors. The architecture unit test scans the ProfileController, Fluid tree and JavaScript for forbidden legacy controller and template references.

The AJAX tests persist malicious rich-text input through the real update endpoint and assert that only the sanitized response is stored. The inline image tests verify that a missing file can never return success and that a real multipart upload returns hasImage: true and creates the FAL relation. They also exercise the dedicated image deletion endpoint through the generated action URL. Form submissions reuse the complete rendered action URL, including the JSON page type, so the tests exercise the same routing contract as the browser. Upload tests are assigned to the not-core-13 PHPUnit group because TYPO3 v13's CLI upload permission check requires a real HTTP upload.

The page type of the JSON endpoints 

Every writing endpoint of the editor is an Extbase action of the same plugin, reached through a dedicated PAGE object with typeNum = 1733735 . The object is delivered by the site set and by the static template of this extension, it is not cached, and it sets Content-Type: application/json on the page - TYPO3 v13 drops the headers a plugin response carries, so the page has to declare it.

Three things in a project's infrastructure have to know about that number:

  • The TypoScript of the site itself. The object exists where the TypoScript of this extension is included - through the site set fgtclb/academic-persons-edit-profile-editing or the static template Academic Persons Edit: Profile editing. There was no such page type before version 3.0, because the previous editor was a server-rendered form flow, so a site package that copied the extension's TypoScript into its own instead of including it has no academicPersonsProfileEditingAjax object. The editor then renders, and every save is answered with the HTML of the page instead of JSON - a failure that happens in the browser, with nothing in the TYPO3 log to look at. The editor therefore checks the TypoScript setup of the request for a PAGE object with that typeNum , and where there is none it renders a role="alert" message (data-pe-missing-ajax-page-type) instead of the silent failure, and logs the cause at error level. Include the delivered TypoScript, or copy the object into the site package:

    EXT:my_sitepackage/Configuration/TypoScript/setup.typoscript
    academicPersonsProfileEditingAjax = PAGE
    academicPersonsProfileEditingAjax {
      typeNum = 1733735
    
      10 < tt_content.academicpersonsedit_profileediting.20
    
      config {
        disableAllHeaderCode = 1
        admPanel = 0
        debug = 0
        disablePrefixComment = 1
        no_cache = 1
        additionalHeaders.10 {
          header = Content-Type: application/json
          replace = 1
        }
      }
    }
    Copied!
  • Route enhancers. A PageTypeDecorator that maps page types onto URL suffixes has to list 1733735, or the editor's request URLs are not resolvable. Without a decorator the type travels as &type=1733735 and nothing has to be configured.

    config/sites/<identifier>/config.yaml
    routeEnhancers:
      PageTypeSuffix:
        type: PageType
        default: ''
        index: ''
        map:
          profile-editing.json: 1733735
    Copied!
  • Web application firewalls and reverse proxies. The endpoints are POST requests carrying JSON, and one of them carries a multipart/form-data image upload. A rule set that blocks unknown JSON bodies, strips the X-Requested-With header or limits the upload size below the configured maxFileSize makes the editor fail with a generic error message and no server side log entry.

The request header the endpoints require 

Every writing endpoint - the fifteen JSON actions and the image upload - requires the request header X-Requested-With: XMLHttpRequest. A request without it is answered with 400 and the error code invalid_request.

The shipped JavaScript sends the header on every request, so this is invisible in normal use. It exists for the image upload, which is a multipart/form-data request and therefore one that any foreign page could submit with a plain form, carrying the visitor's session along. A custom header cannot be set on a cross-origin request without a CORS preflight, which the browser refuses for a cross-site page - so requiring one closes that door without relying on the SameSite attribute of the session cookie, which an installation may change.

A project that talks to the endpoints from its own code has to send the header.

Icon identifiers 

The action icons of the editor are registered in Configuration/Icons.php and rendered through core:icon with alternativeMarkupIdentifier="inline", so the SVG is inlined and follows the text colour of the button it sits in. The files are Bootstrap Icons (MIT, see Resources/Public/Icons/LICENSE-bootstrap-icons.txt) drawn in currentColor.

Identifier and file name name the action, not the glyph: a project that replaces the icon set changes the drawing, not the identifiers its template overrides address.

Identifier File Used for
academic-persons-edit-add add.svg Add a document, contract or contact row
academic-persons-edit-back back.svg Back to the profile overview
academic-persons-edit-clear clear.svg Clear the value of a field
academic-persons-edit-delete delete.svg Delete a row or the profile image
academic-persons-edit-edit edit.svg Open a field or a row for editing
academic-persons-edit-help help.svg Help text popover
academic-persons-edit-move-down move-down.svg Move a row down
academic-persons-edit-move-up move-up.svg Move a row up
academic-persons-edit-save save.svg Save a field or a row
academic-persons-edit-sort-handle sort-handle.svg Drag handle of a sortable list
academic-persons-edit-undo undo.svg Restore the last saved value
academic-persons-edit-upload-image upload-image.svg Open the profile image editor
academic-persons-edit-view view.svg Open a row read-only, or the public profile
academic-persons-edit-view-close view-close.svg Close the read view a row action opened

Overriding templates 

EXT:academic_persons_edit is using Fluid as template engine.

This documentation won't bring you all information about Fluid but only the most important things you need for using it. You can get more information in the section Fluid templates of the Sitepackage tutorial. A complete reference of Fluid ViewHelpers provided by TYPO3 can be found in the ViewHelper Reference

Change the templates using TypoScript constants 

As any Extbase based extension, you can find the templates in the directory Resources/Private/.

If you want to change a template, copy the desired files to the directory where you store the templates.

We suggest that you use a sitepackage extension. Learn how to Create a sitepackage extension.

# TypoScript constants
plugin.tx_academicpersonsedit {
    view {
        templateRootPath = EXT:mysitepackage/Resources/Private/Extensions/myextension/Templates/
        partialRootPath = EXT:mysitepackage/Resources/Private/Extensions/myextension/Partials/
    }
}
Copied!

Profile editing template 

Profile editing renders from Resources/Private/Templates/Profile/Index.html and thirty-one partials below Resources/Private/Partials/Profile/. Four of them sit directly in that directory - the header, the status toast, the button templates and the prototypes - and the rest in four subdirectories: Field/ for the controls and the two action bars, Documents/ for the structured sections and their two editors, Image/ for the image card and the image editor, and Profile/ for the field sections. Any of them can be overridden on its own.

Two of them carry more than markup. Profile/Field/Control.html is the one place a form control is spelled - every field, of every section and of both editors, is that partial - and Profile/Prototypes.html holds the <template data-pe-proto> blocks the custom elements clone for everything they draw in the browser. When overriding while keeping the shipped JavaScript, preserve the form data attributes, the field class and the slot names of those blocks. The complete contract is documented in Profile editing.

Replacing an icon 

The action icons of the profile editing frontend are addressed by identifier, not by file. They are registered in Configuration/Icons.php of EXT:academic_persons_edit as academic-persons-edit-add, -back, -clear, -delete, -edit, -help, -move-down, -move-up, -save, -sort-handle, -undo, -upload-image and -view.

To use different artwork, register the identifier again in the Configuration/Icons.php of the sitepackage with the own file - a later registration wins, and no template has to be overridden:

EXT:mysitepackage/Configuration/Icons.php
<?php

use FGTCLB\AcademicBase\Imaging\IconProvider\CurrentColorSvgIconProvider;

return [
    'academic-persons-edit-save' => [
        'provider' => CurrentColorSvgIconProvider::class,
        'source' => 'EXT:mysitepackage/Resources/Public/Icons/save.svg',
    ],
];
Copied!

That provider inlines the file rather than rendering an <img> , so the glyph takes the colour of the button it sits in. A file registered with it carries a viewBox, draws its shapes in currentColor and has no id attribute - the markup is part of the document, possibly more than once.

The shipped files are Bootstrap Icons; their MIT licence ships beside them in Resources/Public/Icons/LICENSE-bootstrap-icons.txt.

Known problems 

Please note that this extension is still in development. Changes to existing code may appear in upcoming versions.

If you run into a bug or a feature that would be helpful, please use the issue tracker.

ChangeLog v3 

Every change to the Academic Persons Edit extension is documented here.

Also available 

3.0 Changes 

Table of contents

Breaking Changes 

Features 

Deprecation 

Important 

Breaking: Contract fields and contacts are configured as a unit 

Description 

A contract and the address, email address and phone number records it owns are edited as one document in the new profile editing view, and they are configured that way too. The flat validations keys the editor used to read are gone (see the Breaking: Section-based AcademicPersons settings entry of EXT:academic_persons); what the editing frontend reads instead is:

  • contracts.fields, the ordered list of the contract's own fields, each with its render type, its option source and its help text;
  • contracts.contactSections.<section>.fields, the same per contact kind - physicalAddresses, emailAddresses and phoneNumbers;
  • documentSections.<section>, which declares the rows a compact list shows (rowFields), the actions it offers (actions) and whether it is readonly.

The order of the fields in the file is the order of the controls in the form, and a field that is not declared is not rendered and not written. That is the breaking half: a project that relied on the editor rendering every column of a record now has to declare the fields it wants.

readonly and the actions list are enforced on the server, not only in the user interface. A request that creates, updates, deletes or sorts a record of a read-only section is answered with 403 and the error code document_action_not_allowed or contract_contact_action_not_allowed, whether or not the button that would trigger it was rendered.

The rendered form itself changed with it: every control carries a label, its aria-describedby help text and its validation state; the compact lists put a column heading row above the rows on wide viewports and repeat each column's label beside its value on narrow ones; and the sortable lists offer keyboard controls next to the drag handle. The lists are Bootstrap grid rows rather than tables - a row is one record and its cells reflow into a block on a phone. Profile editing describes the result.

Impact 

  • A site package overriding the settings file has to move its contract and contact configuration into contracts.fields and contracts.contactSections. The runtime overlay described in the Feature: Legacy settings overlay and migration command entry of EXT:academic_persons reads the old shape for one more release and logs a warning per package and key; it does not read the new keys above, which have no old equivalent.
  • A field the project's settings file does not declare disappears from the editing form.
  • Code or tests posting to a read-only section now receive 403 instead of writing.

Affected Installations 

All installations of EXT:academic_persons_edit that ship their own Configuration/AcademicPersons/Settings.yaml, and all installations that relied on the previous, non configurable contract form.

Migration 

Declare the contract fields and the contact sections in the settings file of the site package, in the order the form should render them. The shipped EXT:academic_persons/Configuration/AcademicPersons/Settings.yaml is the reference; Profile editor and validation documents every key.

Breaking: Extbase plugins require CType on TYPO3 v14 

Description 

TYPO3 v14 removed the tt_content sub-type feature (the list_type column) and changed ExtensionManagementUtility::addPlugin() accordingly. The academic plugins have been registered as first-class content elements (CType) since the 2.1 version line (see the 2.1 breaking note about migrating from list_type to CType); for TYPO3 v14 support the internal registration was adapted to the new addPlugin() signature and the vestigial list_type handling was dropped.

Impact 

On TYPO3 v14 the tt_content.list_type column no longer exists. Any content records still stored as CType=list with a list_type of one of the plugins below will no longer resolve, and custom TypoScript, TSconfig, page TSconfig or SQL that references list_type for these plugins stops working.

The change relates to the following plugins:

  • academicpersonsedit_profileediting
  • academicpersonsedit_profileswitcher

Affected Installations 

Installations that upgrade to TYPO3 v14 and still hold content elements stored as CType=list + list_type=<plugin>, or that reference list_type for these plugins in their own configuration.

Migration 

Run the provided upgrade wizard academicPersonsEdit_pluginContent before upgrading to TYPO3 v14 (it requires the list_type column, which v14 removes) to migrate the tt_content records to the dedicated CType values. Update any custom configuration referencing list_type to match on CType instead.

Breaking: Profile image upload validation always applies 

Description 

plugin.tx_academicpersonsedit.settings.editForm.profileImage.validation.allowedMimeTypes and ...validation.maxFileSize no longer switch their validator off when they are set to an empty value.

Until 2.x an empty allowedMimeTypes meant "no mime type restriction": the setting was passed through as configured, and an empty list added no validator. The same held for an empty maxFileSize .

From 3.0.0 both settings fall back to the shipped default when they are missing, blank or not a string, and both validators are always added:

Setting Default used for a blank value
validation.allowedMimeTypes image/jpeg,image/png,image/webp
validation.maxFileSize 2M

The defaults are the same list the accept attribute of the file input advertises, so what the browser offers and what the server accepts cannot drift apart.

Impact 

An installation that deliberately blanked one of the two settings to accept any image, or any size, now has uploads rejected that were accepted before: a TIFF, an SVG or a file above 2 MB reaches the editor as a validation error instead of being stored.

Silently accepting whatever a browser sends is not a defensible default for an upload a frontend user performs, which is why the semantics were changed rather than kept.

Affected Installations 

Installations using the profile editing plugin (academicpersonsedit_profileediting) that set either setting to an empty value. An installation that never touched the two settings is unaffected - the previous shipped values were the same defaults.

Migration 

Set the mime types you want to accept explicitly, for example:

plugin.tx_academicpersonsedit.settings.editForm.profileImage.validation {
    allowedMimeTypes = image/jpeg,image/png,image/webp,image/gif
    maxFileSize = 8M
}
Copied!

There is no way to switch either validator off. A list wide enough for the formats a site accepts is the replacement for the blank value.

Breaking: Removed TYPO3 v12 support 

Description 

Support for TYPO3 v12 has been removed for the 3.x version line, based on the dual TYPO3 core version support per major version of the academic extensions support matrix.

This includes removing build, test and configuration parts only required for TYPO3 v12. Version specific code paths are dropped in a dedicated step.

Impact 

TYPO3 v12 or older instances can no longer install or update to the 3.x version of the academic extensions and are required to upgrade TYPO3 first.

The extension cannot be installed on TYPO3 v12 anymore but does not break otherwise.

Affected installations 

All installations using an academic extension on TYPO3 v12 that want to upgrade to the 3.x version line.

Migration 

Upgrade the TYPO3 installation to a supported version - TYPO3 v13 or v14 - beforehand or within the same upgrade step.

Breaking: Replaced the profile editing view 

Description 

The Profile editing content element used to be a set of Extbase forms: one page per record, a form submission per change, a redirect after every save. It is replaced by a single view that edits the profile in place and writes through JSON endpoints of the same plugin - see Profile editing for what it does and Important: Profile editing is replaced in place for why nothing has to be migrated in the page tree.

The plugin identity is unchanged: the content type stays academicpersonsedit_profileediting, the plugin stays AcademicPersonsEdit / ProfileEditing, the request namespace stays tx_academicpersonsedit_profileediting, the site sets keep their names and the default action stays list. Everything below the plugin is new.

Removed controllers and actions 

Five of the six controllers are removed. ProfileController is the only one the plugin registers, and its action list is new:

Removed Replacement
ContractController , ProfileInformationController , PhysicalAddressController , EmailAddressController , PhoneNumberController , each with list, show, new, create, edit, update, confirmDelete, delete, sort and partly toggleVisibility The documentForm/createDocument/updateDocument/ deleteDocument/sortDocument/toggleDocumentVisibility and contractContactForm/createContractContact/ updateContractContact/deleteContractContact/ sortContractContact/toggleContractContactVisibility JSON actions of ProfileController
ProfileController::showAction() , editAction() , editImageAction() , addImageAction() , removeImageAction() , toggleSkipSyncAction() indexAction() for the view, and the update, uploadImage, deleteImage and updateSkipSync actions
AbstractActionController , \Property\TypeConverter\AbstractFormDataConverter , \Service\UserSessionService , \Exception\AccessDeniedException \Service\ProfileUpdateRequestService , \Domain\Parser\ProfileUpdatePayloadParser and \Service\ProfileUpdateValidationService
\Domain\Validator\AddressFormDataValidator , ContractFormDataValidator , EmailFormDataValidator , PhoneNumberFormDataValidator , ProfileInformationFormDataValidator The validation set of the section a record belongs to, resolved from the settings graph - see Profile editor and validation
Domain\Model\Dto\ProfileFormData::createFromProfile() , deprecated since 2.3 and announced for removal in 3.0 Domain\Factory\ProfileFormDataFactory::createFromProfile()

An installation that links to one of the removed actions - a f:link.action in an override, a hand written URL, a bookmark - gets a 500, not a fallback view. Extbase throws InvalidControllerNameException (1313855173) for a removed controller and InvalidActionNameException (1313855175) for a removed action; the fallback to the default action only happens where config.tx_extbase.mvc.callDefaultActionIfActionCantBeResolved is set, and this extension does not set it.

Removed templates, layouts and partials 

Every Fluid file of the form flow is removed. An override of one of them is dead: the file is no longer rendered, and the project keeps a copy of a view that no longer exists.

  • Resources/Private/Layouts/ProfileEdit.html
  • Resources/Private/Templates/Profile/Edit.html, Profile/EditImage.html, Profile/Show.html
  • Resources/Private/Templates/Contract/{Edit,New,Show}.html and the same three files for EmailAddress/, PhoneNumber/, PhysicalAddress/ and ProfileInformation/
  • Resources/Private/Partials/Profile/Buttons/{DeleteCancel,SaveExitCancel}.html
  • Resources/Private/Partials/Profile/Forms/{Checkbox,DateTime,Errors,FieldWrapper,Select,Textarea,Textfield}.html
  • Resources/Private/Partials/Profile/List/{Contracts,EmailAddresses,PhoneNumbers,PhysicalAddresses,ProfileInformation}.html
  • Resources/Private/Partials/Profile/Properties/{Contract,EmailAddress,PhoneNumber,PhysicalAddress,Profile,ProfileInformation}.html
  • Resources/Private/Partials/Profile/Show/{Image,Personal}.html

The plugin renders without a Fluid layout since, so the TypoScript constant plugin.tx_academicpersonsedit.view.layoutRootPath and the view.layoutRootPaths block it filled are removed as well. A site package that still sets the constant sets something nothing reads.

Resources/Private/Templates/Profile/List.html is kept and rewritten, and Resources/Private/Templates/Profile/Index.html with the thirty-one partials below Partials/Profile/ is the new tree: four of them - Header.html, StatusToast.html, ButtonTemplates.html and Prototypes.html - directly in that directory and the rest in {Documents,Field,Image,Profile}/. Overriding templates describes what may be overridden.

The JavaScript module of the removed form flow goes with it: Resources/Public/JavaScript/frontend/rich-text.js, addressed by the bare specifier @fgtclb/academic-persons-edit/frontend/rich-text.js, configured CKEditor 4 for Templates/Profile/Edit.html and is deleted. A site package that still loads that specifier loads nothing. Rich text is CKEditor 5 from EXT:rte_ckeditor now, created by the editing view itself - see Profile editing.

Removed icons 

The ten icon files 2.4 shipped for the form flow are deleted:

add-image-icon.svg, add-item-icon.svg, back-icon.svg, cancel-icon.svg, delete-icon.svg, edit-icon.svg, save-icon.svg, sort-icon.svg, sort-vertical-icon.svg, view-icon.svg.

Five of the ten identifiers they were registered under go with them: academic-persons-edit-add-image, -add-item, -cancel, -sort and -to-top. The other five - academic-persons-edit-edit, -view, -delete, -save and -back - stay and now resolve to the new artwork.

They also resolve through a different icon provider: \FGTCLB\AcademicBase\Imaging\IconProvider\CurrentColorSvgIconProvider inlines the <svg> where TYPO3's own SvgIconProvider emitted an <img>. A site package styling the editor's icons through a rule such as .t3js-icon img therefore stops matching, and styles the <svg> instead; the Feature: Icon provider for icons that follow the text colour entry of EXT:academic_base describes the provider.

Thirteen action icons are registered, under the identifiers listed in Icon identifiers; the extension icon persons_edit_icon is the fourteenth entry of Configuration/Icons.php and is unchanged. They are Bootstrap Icons (MIT) drawn in currentColor and rendered inline, so they take the colour of the control they sit in - see Feature: The profile editing icon set.

Removed labels 

Resources/Private/Language/locallang.xlf goes from 208 to 145 trans-units: 144 are removed and 81 are new. 64 survive, all of them byte-identical. The German de.locallang.xlf follows one to one. That is not a list worth printing - the authoritative one is the diff of the file for this release - but the shape of it is:

  • Everything the removed Extbase form flow needed goes with it: every *.create.success / *.update.success / *.delete.success / *.sort.success message, every *.placeholder, every list.no*Found entry except list.noProfilesFound, which the profile overview still renders, the actions.hide / show / translate / saveAndExit / setToTop / setToBottom / replace actions, back, list.hidden.badge, list.contract.position, the profile.* section headings and every *FormData.*.error.* unit.
  • contract.published.label was a stale duplicate of contract.publish.label, which stays, and is removed; emailAddress.emailAddress.label is replaced by the shorter emailAddress.email.label, which is new in this release.
  • The year fields of a timeline entry keep their labels profileInformation.year.label, profileInformation.yearStart.label and profileInformation.yearEnd.label; their *.placeholder companions are removed with the form flow that rendered them.
  • The new view brings its own vocabulary under the profileEditing.* prefix - status messages, empty states, the image editor, the document section labels and the controls of full form editing (Feature: Full form editing applies as one form).

All of them are overridable through locallangXMLOverride , so an installation that translated or reworded one of the 149 removed units loses that override silently: the key is simply not read any more. Compare the overrides against the shipped file after the update.

Removed extension configuration 

profile.autoCreateProfiles and profile.createProfileForUserGroups are removed from ext_conf_template.txt. Neither is read by any code path of this extension. Their stored values are not lost by the removal - the extension configuration merges the current values over the template and prunes nothing - and they are still the source the academicPersons_MigrateProfileAutoCreateExtensionsConfiguration upgrade wizard of EXT:academic_persons reads to carry both settings over. profile.allowedLanguages is unchanged.

New page type 

The JSON endpoints are reached through a PAGE object named academicPersonsProfileEditingAjax with typeNum = 1733735 , delivered by the site set fgtclb/academic-persons-edit-profile-editing and by the static template. A project with a PageType route enhancer, a web application firewall or a reverse proxy in front of TYPO3 has to let that page type and the X-Requested-With header through - see The page type of the JSON endpoints.

There was no such page type in 2.4, where the editor was an Extbase form flow, so a site package that copied the TypoScript of this extension into its own instead of including the site set or the static template has the editor and not the page type. Every save is then answered with the site's error page where the browser expects JSON, and the editor can do nothing about it. The editor therefore looks the page type up while it renders: when no PAGE object of this site carries typeNum = 1733735 , it writes an error naming the cause to the TYPO3 log and tells the visitor that the profile cannot be saved, instead of letting them find out on their first change. The object is matched by its typeNum , not by its name, so a project that declares it under a name of its own is recognised.

Impact 

  • Overrides of any removed Fluid file stop having an effect. The editing view renders from the shipped templates.
  • Links to the removed actions are a 500 rather than a fallback view.
  • Templates and PHP code referring to the five removed icon identifiers, labels or controller classes fail: an unknown icon identifier renders TYPO3's default-not-found placeholder, an unknown label renders its own key, and an unknown class is a fatal error.
  • A form posting profileInformationFormData[year], contractFormData[...], addressFormData[...], emailFormData[...] or phoneNumberFormData[...] to one of the removed actions is not processed. The editor posts JSON, under the property names year, yearStart and yearEnd.
  • The two removed extension configuration options disappear from the Settings module. A stored value is ignored.

Affected Installations 

All installations using the Profile editing content element of EXT:academic_persons_edit, and in particular every installation that overrides one of its Fluid files or links to one of its actions.

Migration 

  1. Remove the overrides of the deleted Fluid files, and re-apply the project specific changes on top of the new template tree where they are still wanted. Overriding templates names the files that carry the hooks the JavaScript binds to; changing those breaks the editor rather than the layout.
  2. Replace links to the removed actions with a link to the index action and the profileUid argument, or with the profile overview.
  3. Replace the five removed icon identifiers with the ones of Icon identifiers, and re-point CSS that selected the icons as an <img>.
  4. Confirm that the site actually delivers page type 1733735. Include the site set fgtclb/academic-persons-edit-profile-editing or the static template of this extension; a site package that maintains a copy of the extension's TypoScript adds the academicPersonsProfileEditingAjax PAGE object to that copy. The editor reports a missing page type in the TYPO3 log and on the page itself, but it reports it, it cannot repair it.
  5. Let page type 1733735 and the X-Requested-With header pass through route enhancers and firewalls.
  6. Drop profile.autoCreateProfiles and profile.createProfileForUserGroups from the deployment configuration - but only after the academicPersons_MigrateProfileAutoCreateExtensionsConfiguration upgrade wizard has run, because that wizard reads them.

Feature: Frontend profile edits synchronize translations 

Description 

The frontend editing flow of EXT:academic_persons_edit dispatches \FGTCLB\AcademicPersons\Event\AfterProfileUpdateEvent again. The dispatch had been lost in the 2.x profile editing restructuring: since then the event fired only when a profile was auto-created on frontend user login, so a profile edited through the frontend plugins was never synchronised into its translations, and the profile slug was not regenerated either.

These actions dispatch the event once after the change is persisted: the generic field update of the profile form, the skip_sync toggle, the profile image upload — which is also a replacement — the removal of the profile image, and the create, update, delete and sort actions of a document section and of the contacts of a contract. The event always carries the persisted default language profile; child records resolve their owning profile through the contract, and an edit of a profile fetched as translation overlay does not dispatch (synchronisation runs from the default language record only). A removal that found no image to remove changed nothing and announces nothing.

Impact 

With profile.allowedLanguages configured, a frontend edit keeps the translated profile records in sync again: missing translations are created and existing ones updated through the record synchronisation of EXT:academic_persons, which routes every write through the TYPO3 DataHandler . The profile slug is regenerated on every frontend edit as well, since the slug listener reacts to the same event.

Installations that left profile.allowedLanguages empty only regain the slug regeneration — the synchronisation itself stays a no-op for them.

Affected Installations 

Every installation using the frontend editing plugins of EXT:academic_persons_edit. Behaviour beyond the editing flow is unchanged: profile auto-creation dispatched the event before and still does, and project side dispatches from own DataHandler hooks are not affected.

Feature: Full form editing applies as one form 

Description 

Edit all opens every editable profile field at once. It now also gives the form controls of its own: Apply, Undo and Discard, rendered once at the end of every field form by Partials/Profile/Field/FormActions.html. The per-field clear, undo and save groups are hidden for as long as the form is open, and the undo beside an autosaving checkbox with them.

  • Apply sends every changed field of the profile in one request. updateAction() validates the whole field map before it writes anything, so either all of it is stored or none of it is - a refused property leaves the other submitted properties unwritten as well. On success the stored values are written back into the fields, the previews and the name heading, the form closes and the focus returns to Edit all.
  • Undo restores every field to the value that is stored and keeps the form open.
  • Discard restores every field and closes the form. Closing the form with Edit all does the same thing.

Entering the form discards the single field or group that was open. Its control goes back to the stored value and any message beside it is cleared, exactly as its own Undo would leave it, so the form starts from the stored profile rather than from a value the visitor typed in a state they have just left.

While an apply is on its way to the server none of the three, and neither Edit all nor Escape, does anything: the request cannot be taken back, and reverting under it would leave the stored profile and the editor's baseline disagreeing silently. Because entering the form discards, the same applies while a single field is being saved: Edit all and every pencil do nothing until the answer has been written back, and say so in the polite live region rather than merely not reacting - see Important: Opening an editor asks about the unsaved one.

A refusal reverts nothing. Every entered value stays where it was entered, the refused fields are marked and described by their own message, the caret goes to the first of them and the refusal is announced once rather than once per field. Reverting thirty typed fields because one of them was rejected is data loss; the per-field paths revert only because a checkbox has one bit and no other way to report a failure.

A checkbox that saves on change stops doing so while the form is open and is applied with everything else instead. Without that, the value would reach the database while the visitor is still deciding, and Discard could not take it back. The synchronisation switch of the page header sits outside the field forms and keeps saving immediately.

Keyboard and screen reader 

  • Opening the form puts the caret in the first editable field.
  • The controls stand after the fields in document order and are tabbed through as Apply, Undo, Discard.
  • Escape discards the form, unless the caret is inside a rich text editor - that key belongs to the editor's own balloons first.
  • Ctrl + Enter (Cmd + Enter) applies it, and so does Enter in a text field, which submits the form. Both keys belong to the field form, so a document, contract or image editor open at the same time keeps its own handling of them.
  • The bar is a role="group" with its own accessible name, the Edit all button carries aria-pressed and names the forms it controls through aria-controls, and the two live regions of the editor announce the result: the polite one carries the restored notice and the success, the assertive one a failure and a refusal of the form. A refusal beside a single field stays polite, as it always was.

Impact 

This is a feature rather than a breaking change: the whole profile editing view is new in 3.0.0 (Breaking: Replaced the profile editing view) and no release ever shipped Edit all with per-field controls.

Single-field editing keeps its controls - the pencil, the three buttons beside a field and the field groups act exactly as they did, and the editors of every document and contract panel are not touched at all. What changed is that only one field or group is open at a time: opening another field or group, with a pencil or with Edit all, discards the editor that was open before it. Pressing the pencil of the field that is already open is not that - its own editor is left alone and what has been typed into it survives. An unsaved value left behind in another row is lost, rather than being posted later from a control the visitor can no longer see. A pencil pressed while the form is open does nothing at all - every field is already open, and the bar is the way out.

An override of Partials/Profile/Profile/Fields.html has to render Profile/Field/FormActions at its end, or the profile offers no way to apply the form. An override of the bar itself keeps the data-pe-form-actions element with its data-pe-form-reverted-message, and the three data-pe-form-apply, data-pe-form-undo and data-pe-form-discard buttons; every label of it is Fluid, none is spelled in JavaScript. See Two editing modes.

Feature: Contracts and profile information rows can be hidden 

Description 

Every row of a document section of the Profile editing view - a contract, a publication, a lecture, a cooperation and the other timeline entries - carries a visibility switch as the hide entry of its section's actions list. Pressing it hides the record in the public profile, pressing it again shows it. While the record is hidden the row is drawn in the secondary text colour, its controls at full contrast, and carries a Hidden tag in front of its action group; the label of the switch names the press (Hide in frontend respectively Show in frontend) and its glyph shows the state.

The switch is what the contact rows below a contract already have, extended to the records they belong to. It flips the record's own hidden column, the TCA enablecolumns.disabled field, through the new toggleDocumentVisibility JSON action of ProfileController , which is sent the target state rather than a "flip" so that a double press stores what the visitor saw, answers the serialised record, and follows the hide entry of the section's allow-list like every other action follows its own.

A hidden record stays in the editor: it is the one place it can be shown again. The editor therefore lists the records of a section through the repositories' IncludingHidden queries, while the public views keep reading the relations of the profile, which respect the enable fields.

Impact 

The switch is listed first in the shipped actions of every section. A site that overrides a section's actions keeps its list and adds hide where the switch is wanted; a section marked readonly never offers it. A site overriding Templates/Profile/Index.html has to carry data-toggle-document-visibility-url, data-message-document-hidden and data-message-document-shown; one overriding Partials/Profile/Documents/Actions.html renders the hide case and the tag itself.

Affected Installations 

Installations of the Profile editing content element of EXT:academic_persons_edit.

Feature: The profile editing view rewritten 

Description 

The Profile editing content element renders the whole profile on one page and saves each change where it is made. The previous form flow - one page per record, a submission per change, a redirect after every save - is gone (Breaking: Replaced the profile editing view). Profile editing is the reference; this entry is the overview.

Overview 

list renders the profiles assigned to the authenticated frontend user, with the language each of them belongs to and a link to the public detail page. index opens one of them for editing. Every other action of the plugin answers JSON.

Editing in place 

Each field carries an edit control. Activating it turns the rendered value into an input, saving posts only what changed, and the answer is written back into the page - no reload, no lost scroll position. A group of fields that belong together (the name parts, a link and its title) is edited and saved as one.

An unsaved field can be restored to the value that is stored, and a checkbox saves on change and reverts itself when the request fails, so what is on screen is what is in the database. Only one field or group is open at a time: opening another one discards the one that is open, so no value is ever left behind in a control the visitor cannot see.

Edit all opens every field of the page at once and gives the form its own controls; see Feature: Full form editing applies as one form.

Structured sections 

Contracts and the seven kinds of timeline entry - cooperation, lectures, memberships, press and media, publications, scientific research and vita - are compact lists with an editor that folds out below the row it belongs to. Creating, viewing, editing, deleting and reordering happen without leaving the page; deleting asks first. Which columns a list shows, which actions it offers and whether it is read-only is configured per section (Breaking: Contract fields and contacts are configured as a unit), and the server enforces it.

Reordering works with the up and down buttons, with the keyboard, and by dragging a row onto its new place. A failed request puts the previous order back.

Contract contacts 

The addresses, email addresses and phone numbers of a contract are edited inside the contract's own editor, each kind as its own list with its own configured fields.

Rich text and character limits 

A field configured as ckeditor opens a CKEditor 5 instance with bold, italic, lists and links, in the language of the site. A configured character limit is shown while typing, enforced in the editor and validated again on the server. Every stored value is sanitized against an allow list - paragraphs, line breaks, bold, italic, lists and links with an http, https, mailto or tel target - so what an editor pastes cannot reach the public profile as markup the template did not expect.

Profile image 

The image is edited in a panel that folds out over the profile. A newly selected file is cropped to the configured aspect ratio in the browser before it is uploaded, so the stored file is the one that is shown. Uploading replaces the existing relation rather than adding a second one, and deleting asks for a confirmation first. The upload is validated on the server against the configured maximum file size and MIME type list - a blanked setting falls back to the same defaults the file input advertises, it does not disable the check.

Years 

The three year fields of a timeline entry are <input type="number"> controls carrying min="0", max="9999" and step="1" - the bounds the TCA of fgtclb/academic-persons declares for the same columns, which the endpoint enforces again on every submission. Nothing about a year is formatted, so nothing about it follows a locale.

Contract dates 

The two contract dates validFrom and validTo are typed into a plain text control showing d.m.Y behind the hint dd.mm.yyyy, exactly as the previous editor showed them. They are deliberately not an <input type="date">: the native control follows the locale of the browser rather than the one of the site, it cannot be styled with the rest of the editor, and its calendar is not the date picker this editor is meant to get. A date picker is deliberately not shipped in 3.0. It is a feature of its own and it will not be the browser's.

The endpoint reads d.m.Y and Y-m-d, both strictly, and refuses anything else. A stored date is displayed as the MEDIUMDATE of the site language, the way the public profile renders it.

Synchronisation switch 

The switch that excludes a profile from the translation synchronisation is saved through its own endpoint, and reverts itself when the request fails.

Accessibility 

Every control has an accessible name, the fold-out regions carry aria-controls/aria-expanded, validation errors are announced through aria-describedby/aria-invalid, a failed request is announced through an assertive live region and a successful one through a polite one, and closing an editor returns the focus to the control that opened it.

That last one has one deliberate exception. An editor that is closed because another one is being opened does not hand the focus back to its own pencil - the caret belongs in the editor the visitor has just opened, and moving it twice would take it out of there again. The rule is therefore that a close the visitor asked for returns the focus, and a close that happens on the way to somewhere else leaves it where it is going.

Bundled libraries 

One third-party set is shipped with the extension, with its licence file next to it: Bootstrap Icons (MIT), the thirteen control icons of this view, as SVG files under Resources/Public/Icons/ with LICENSE-bootstrap-icons.txt beside them.

No JavaScript library is shipped. The view is Fluid, driven by five plain custom elements that depend on no framework; CKEditor 5 is loaded from the system extension rte_ckeditor - see Important: New dependencies cms-rte-ckeditor and html-sanitizer and Important: The profile editor is Fluid, driven by five custom elements - and the image cropper is the Cropper.js the TYPO3 core itself maps as cropperjs, version 1.6.1 on both supported core versions. No CDN or other runtime request is involved either way.

Impact 

The editing view is replaced for every installation of the content element. What has to be looked at when updating is listed in Breaking: Replaced the profile editing view.

Feature: The profile editing icon set 

Description 

The profile editing frontend addresses its action icons through fourteen identifiers registered in Configuration/Icons.php:

Identifier Action
academic-persons-edit-add Add an entry to a list
academic-persons-edit-back Leave the editor
academic-persons-edit-clear Clear the value of a field
academic-persons-edit-delete Delete an entry
academic-persons-edit-edit Start editing a field or an entry
academic-persons-edit-help Show the help text of a field
academic-persons-edit-move-down Move an entry down
academic-persons-edit-move-up Move an entry up
academic-persons-edit-save Apply an edit
academic-persons-edit-sort-handle Drag handle of a sortable entry
academic-persons-edit-undo Undo an edit
academic-persons-edit-upload-image Upload or replace the profile image
academic-persons-edit-view Open the public view of a record
academic-persons-edit-view-close Close the read view a row action opened
academic-persons-edit-visible The visibility toggle of a contact that is shown in the frontend
academic-persons-edit-hidden The visibility toggle of a contact that is hidden in the frontend

Identifier and file name are the action, never the drawing: a later icon set changes the glyph, not the API the templates address.

The files are Bootstrap Icons and carry their MIT licence in Resources/Public/Icons/LICENSE-bootstrap-icons.txt. They are drawn in currentColor and registered with \FGTCLB\AcademicBase\Imaging\IconProvider\CurrentColorSvgIconProvider , which inlines the file instead of rendering an <img> - so a glyph takes the colour of the button it sits in, in the frontend as much as in a dark backend colour scheme.

Impact 

The identifiers are public API. A site package that wants different artwork re-registers one of them in its own Configuration/Icons.php with its own file and needs no template override; a file registered that way is drawn in currentColor as well, or it will not follow the surrounding text.

Five of the identifiers - academic-persons-edit-edit, academic-persons-edit-view, academic-persons-edit-delete, academic-persons-edit-save and academic-persons-edit-back - existed before and now resolve to the new artwork and the new provider. An installation that renders them through the shipped templates sees a different glyph that follows the text colour instead of a fixed dark grey.

Affected Installations 

All installations using the EXT:academic_persons_edit extension starting with version 3.0.

Migration 

No migration is required. An installation that re-registered one of the five existing identifiers in a site package keeps its own artwork, since a later registration wins.

Important: Profile image upload uses native Extbase upload handling 

Description 

The profile image upload of the academicpersonsedit_profileediting plugin was handled by the custom type converter \FGTCLB\AcademicBase\Extbase\Property\TypeConverter\FileUploadConverter (EXT:academic_base). It has been replaced with the native Extbase file upload handling introduced in TYPO3 v13.3 ( FileUploadConfiguration , see TYPO3 feature forge#103511), and the replacement survives the editor rewrite of the same release: the new editing view uploads the image through the same configuration.

The TypoScript setting names are unchanged. settings.editForm.profileImage.targetFolder , settings.editForm.profileImage.validation.maxFileSize and settings.editForm.profileImage.validation.allowedMimeTypes keep their names and are mapped onto the core FileSizeValidator and MimeTypeValidator . What a blank value means changed - see Breaking: Profile image upload validation always applies.

Impact 

The upload behaves differently in four ways:

  • Stored file names change. The custom converter built the file name from the profile data as <firstname>-<lastname>-<uid>.<extension> and replaced an existing file of that name. The native handling keeps the name supplied by the client, appends a random suffix and renames on conflict instead. Stored names therefore no longer contain the name of the person, which is an improvement for a folder that is usually reachable over the web.
  • The previous image is deleted on re-upload. Because the generated name changed on every upload, replacing an image would leave the previous file behind. Uploading a new profile image now deletes the file the profile referenced before, unless it is still referenced by another record.
  • The mime type is detected from the file content. The custom converter trusted the media type sent by the browser, which can be spoofed. The core MimeTypeValidator inspects the uploaded file itself and additionally cross-checks the file extension. An upload whose real content does not match an allowed mime type is now rejected, even if the browser announced an allowed one. Uploads that only passed because of a faked header stop working - this is intended.
  • The file is only stored once the upload validates. Previously the file was imported into FAL while mapping the request, so an upload that failed validation afterwards left an unreferenced file behind in the upload folder. The file is imported after successful validation now, which avoids those orphaned files.

Affected Installations 

Installations using the profile editing plugin (academicpersonsedit_profileediting) with profile image uploads. Installations that rely on the stored file name - for example when referencing those files by a fixed path outside of FAL, or when addressing them by the person's name - need to review that assumption.

Migration 

No configuration change is required for the upload itself; check the blank-value semantics of the two validation settings in the breaking entry named above. Files uploaded before this change keep their existing names and references, and are deleted as soon as the corresponding profile image is replaced.

Important: Repair wizard for profile image relations 

Description 

With academic_persons 3.0 a translated profile can carry an image of its own (changelog entry Breaking: The profile image translates of that extension). The relation rows this extension's pre-3.0 image upload and translation synchronisation wrote by hand do not always fit that model, and the new upgrade wizard academicPersonsEdit_repairLocalizedProfileImages repairs the three shapes that do not:

  • Duplicate references on one profile. The profile keeps the file of the reference the frontend renders — the first by sorting_foreign , then by uid . The relation itself is rewritten: one new reference for that file, every old row of the column deleted. A crop or a caption that sat on one of those rows is not carried over, because the new row is what carries the localization state the column now needs. No file is deleted.
  • A relation counter in tx_academicpersons_domain_model_profile.image that disagrees with the number of references — on a default-language profile, on a translation with an image of its own, or on a translation that follows the default-language image. The last case is the shape the 2.x synchronisation left behind, a copied counter without the reference; it is repaired by letting the core localize the default-language reference into the translation, which is what every later synchronisation does.
  • A translation carrying a reference of its own — one that is not a localization of the default-language reference — without the custom localization state. Without the state the next synchronisation would replace that reference with a localization of the default one. The state is set; the reference and its file stay.

A translation whose image follows the default language, with a localized reference or none at all, is the regular shape of the translatable column and is not touched. A translation whose l10n_parent is missing, deleted or itself a translation cannot be propagated into; it is repaired as the independent record it de facto is, and the orphan is written to the log.

Every write of the wizard goes through the TYPO3 DataHandler, so the reference index, the record history and the localization state are maintained by the core. The wizard is idempotent and reports nothing to do once the relations are consistent. It is registered as repeatable, so it is never marked as done and stays available in the upgrade module and on the command line, however often it ran and whatever it found.

The wizard reads and writes live records only. Workspace versions and records that exist only inside a workspace are skipped, and the run itself acts as a backend user in the live workspace. It therefore repairs the same records whether it is started from the command line or from the upgrade module, and it does so also when the person who starts it has a workspace selected in the backend. Repairing a workspace version is not possible and is not attempted: publish the workspace and run the wizard again.

No file is deleted, and none is copied. The wizard rewrites relations only. Whether a file is still used cannot be answered from sys_file_reference , which records FAL relations and knows nothing about an RTE t3://file link, a typolink or a file collection — and an unattended bulk delete on that basis is unrecoverable. A file left without a relation stays in its storage; the "unused files" report of the install tool lists such files for a human to judge.

Impact 

Installations upgrading from 2.x see the wizard in the upgrade module after the database has been updated. Until it ran, a profile with duplicate references renders whichever reference sorts first — as before — and a translation with a stale counter renders no image — as before. Running it leaves no file behind that was not there before, so it is safe to run on an installation whose files are also linked from content elements.

Run it with vendor/bin/typo3 upgrade:run academicPersonsEdit_repairLocalizedProfileImages or from Admin Tools > Upgrade > Upgrade Wizard. Because the wizard is repeatable, it can be started before the database update as well: it reports that there is nothing to do and stays available for the run that follows the schema update.

Affected Installations 

Installations that uploaded profile images or synchronised translated profiles with a 2.x version of this extension.

Important: New dependencies cms-rte-ckeditor and html-sanitizer 

Description 

EXT:academic_persons_edit gained two hard requirements with the replaced profile editing view:

Package Constraint What needs it
typo3/cms-rte-ckeditor ~13.4.0@dev || ~14.3.6@dev The rich text fields of the editing view load six CKEditor 5 bundles shipped by that system extension. Only its JavaScript is used - none of its backend rich text configuration.
typo3/html-sanitizer ^2.3 Every rich text value is sanitized against an allow list on the server before it is stored. The extension registers its own builder on that package.

ext_emconf.php declares rte_ckeditor accordingly. typo3/html-sanitizer is a Composer library, not an extension, so it has no ext_emconf.php counterpart.

Impact 

Composer-managed installations resolve both automatically on update; nothing has to be done. typo3/html-sanitizer is a dependency of the TYPO3 core already, so only typo3/cms-rte-ckeditor is actually added to most installations.

Classic, non-composer installations have to activate the system extension rte_ckeditor in the Extension Manager. Without it the rich text fields of the editing view stay plain textareas and their JavaScript fails to load.

Affected Installations 

Every installation of EXT:academic_persons_edit 3.0.0. Classic installations that had rte_ckeditor deactivated need to activate it.

Migration 

Nothing beyond installing the extension - see Installation.

Important: Plugins assign a record view variable 

Description 

TYPO3 v14 rewrote the header partial of EXT:fluid_styled_content. Where v13 Header/All.html reads {data.header}, the v14 Header/All.fluid.html renders header and subheader with {record -> f:render.text(...)}, and that ViewHelper requires a record object.

Content elements based on lib.contentElement receive that record from the record-transformation data processor, but an Extbase plugin view assigns only the data array. Templates of this extension render the shared header partial, so on TYPO3 v14 they aborted with

The record argument must be an instance of ... Given: null
Copied!

The plugin controllers now assign an additional record view variable, built from the tt_content row of the current content element. TYPO3 v13 ignores it, its header partial keeps reading data, so one implementation serves both core versions.

Impact 

The affected plugins render again on TYPO3 v14. Nothing was removed or renamed, so no configuration or template override needs to be adapted.

Custom templates and template overrides may use the new {record} variable, for example with <f:render.text record="{record}" field="header" />.

Affected Installations 

Installations running the plugins of this extension on TYPO3 v14. TYPO3 v13 installations are unaffected.

Migration 

None required.

Important: Opening an editor asks about the unsaved one 

Description 

Only one editor of the Profile editing view is open at a time — one field or group, the whole form, one document row or one contact of a contract. Opening another one, and pressing Edit all, closes the editor that is open first. One that still holds the values it was opened with closes silently, exactly as its own Undo or Cancel would close it. One with changes is not thrown away: the view asks, in a dialog of its own, whether to Save and continue, to Discard changes or to Keep editing. Saving stores what the open editor's own save would store and then opens the other one; a save the server refuses keeps the visitor in the refused editor, with its messages, and opens nothing. Keeping — and Escape — opens nothing at all.

The dialog is a <dialog> element the view clones from the unsaved-changes template of Partials/Profile/UnsavedChanges.html, never a browser prompt: its question and its three answers are labels of the extension, translated in both languages, and overridable with the partial. A site that drops the template leaves the visitor in the editor that is open.

The view also says what it did. A discard the visitor chose is announced in the polite live region — Unsaved changes were discarded. — because the row collapsing is the only other sign of it, and that is no sign at all for a visitor who is not looking at it. An editor closed with the value it was opened with announces nothing, and the Undo a visitor pressed themselves keeps its silence.

Two related rules. While a save is on its way to the server, the pencil, Edit all and the Delete content and Undo beside a field or a group do nothing: the answer is about to write the stored value back into the field it saved, so a discard under it would be taken back a moment later with nothing saying why. They say Please wait until the change has been saved. rather than simply not reacting. The requests carry no timeout, so a save the server accepts and never answers leaves those controls refusing until the page is reloaded. And a pencil pressed while Edit all is open does nothing at all, silently: every editable field is already open, and the form's own bar is the way out of that mode.

A saved edit of a document row or of a contact stays open with what it stored, so the next change starts from the record; creating and deleting close the editor as before.

Impact 

The two status messages travel from Templates/Profile/Index.html to the frontend as data-message-discarded and data-message-save-in-progress, translated from profileEditing.status.discarded and profileEditing.status.saveInProgress. A site that overrides that template has to carry the two attributes, and has to render Partials/Profile/UnsavedChanges.html below the plugin root; without the attributes the view announces its generic information text in their place, without the partial it never asks and never opens a second editor over a changed one.

Affected Installations 

Installations of the Profile editing content element of EXT:academic_persons_edit, and in particular those overriding Templates/Profile/Index.html or the language files of the extension.

Important: The profile editor is Fluid, driven by five custom elements 

Description 

The Profile editing view of EXT:academic_persons_edit is rendered by Fluid and driven by five custom elements. The elements control that markup; none of them produces any. An integrator therefore changes what the editor looks like by overriding a Fluid partial, never by replacing a JavaScript module, and that holds for the two regions whose content only exists at runtime as well.

Those two regions are the editor of one document or contract and the contacts of one contract: their fields, labels, options and display values come from the documentForm and contractContactForm responses, which answer field descriptors and never HTML. Fluid renders their shapes as <template data-pe-proto="…"> blocks - eleven of them in Resources/Private/Partials/Profile/Prototypes.html and five more in the three Documents/ partials it renders - and the elements clone one and fill it.

The four verbs 

A prototype is filled through exactly four attributes, and there is no fifth:

Attribute Meaning
data-pe-slot="key" The text of the node becomes the value. Never markup: a value that contains <script> is shown, not run.
data-pe-attr="attribute:key …" Those attributes take the value. A value that is absent or false takes the attribute off the clone.
data-pe-when="key" The node is removed when the value is falsy.
data-pe-list="key" Where repeated clones go: into the marked element, or in place of it when the marker is a <template>.

An override may change every tag, every class and every label of a prototype. What it may not change is the vocabulary: the data-pe-* hooks with their profile-editing-{uid}-{field} id shape, and the slot, condition and list keys. What it cannot change is the order the elements insert things in and which slot carries which value - both are TypeScript.

One place spells a control 

Resources/Private/Partials/Profile/Field/Control.html is the only place a field control of this editor is spelled - a text input, a textarea, a rich text field, a select and a checkbox. It is rendered inline for the permanent profile fields, once per type into the prototypes, and through those for every field of a document or contact editor. Overriding that one file changes every field control of the editor at once. Two controls stand outside it because they are not profile fields: the synchronisation switch of Header.html and the f:form.upload of Image/Editor.html, which belongs to the Extbase form that carries the upload signature.

The five elements 

Their names are public API from this release on. The prefix is the extension key with its underscores replaced, because a custom element name is global.

Element What it owns
<academic-persons-edit-profile-editing> One editor. It wraps the plugin root, reads its data-* contract once and starts everything below it.
<academic-persons-edit-image-editor> The image editor over the server rendered upload form. That form stays server rendered because only the server can sign its __trustedProperties.
<academic-persons-edit-document-editor> One open document or contract editor, in add, view, edit or delete mode.
<academic-persons-edit-contract-contacts> The contacts of one contract and the editor of one contact.
<academic-persons-edit-rich-text> One rich text field and the CKEditor 5 instance on it.

Four events report what an open editor did, and one is dispatched upwards by any descendant that wants a status shown: pe:document-close, pe:document-submit, pe:document-input with { name, value }, pe:document-closed and pe:status with { type, message? }.

No element opens a shadow root. A project's stylesheet, the theme's Bootstrap classes and any CSS written against the rendered markup reach every control - there is no style encapsulation to work around and no ::part() to declare.

Impact 

  • Every part of the editor is overridable in Fluid, including the document editor and the contact list of a contract.
  • An override of a partial that carries a <template data-pe-proto> block has to keep the block, its name and its slot keys. An element that cannot find its prototype fails loudly: the editor that was being opened is taken back out, the error message of the plugin is announced, and the missing name is in the browser console. A slot that is not emitted leaves the value unwritten.
  • The class names and the data-pe-* hooks of the rendered controls are as much part of the contract as the endpoints are.
  • The element names must not be defined a second time by a project.

Affected Installations 

Installations of the Profile editing content element of EXT:academic_persons_edit that override one of its Fluid files, or that style or script the editor against its markup.

Important: The profile editor draws its own focus ring 

Description 

The Profile editing view now styles the focus of the controls and the buttons it renders, and no longer leaves that to the surrounding theme.

Bootstrap draws a focused control's ring with box-shadow, and a theme is free to layer more rings behind Bootstrap's. bk2k/bootstrap-package does: it adds an opaque white ring and an opaque black one behind the translucent accent ring, on :focus-visible of .form-control, .form-select, .form-check-input and .btn. Shadows paint in the order they are written, so the black ring covers the full width and the white one only its inner half, and a translucent accent ring over an opaque black one is dark: the outer half of the ring reads as a hard black rectangle tight around the control. Rendered and read back pixel by pixel, that band is #161e18 on the theme's light body background.

Two more things were wrong with drawing a focus ring as a shadow, and the view now avoids all three at once by drawing a real outline instead.

A shadow is painted outside the border box, so an ancestor with overflow: hidden cuts it away. The document editor's collapse panel and the image editor are exactly that, the grid row inside them pulls itself out to the clipping edge, and every field and every button of those panels therefore sat with its left and its right border edge on that edge. Measured before this change, a focused field of a document panel and the file input of the image editor - which the image editor focuses on every open - showed no ring pixel at all on either side, only Bootstrap's #abbbb0 border at about 2:1 against white.

And forced-colours mode drops box-shadow altogether, while the rules that draw a ring that way also set outline: 0. What a user of a high contrast theme then saw was not the site's focus indicator but whatever the browser put in its place, which differs between browsers. An outline is kept and recoloured in every one of them.

The ring is therefore drawn as an outline on :focus-visible, inside the border box so that no ancestor can clip it, and box-shadow: none in the same rule takes the layered rings and the theme's inset shadow off with it. :focus-visible rather than :focus is what the theme's own rules use and what Bootstrap uses for buttons, so it is exactly where the defect is.

:focus-visible is not the same thing as keyboard focus, and the difference decides what a visitor using a mouse sees. Measured in Chrome, a pointer click on a checkbox or on the synchronisation switch does not match it, so those two keep the soft glow Bootstrap has always drawn for them. A pointer click on a text input or a select does match it, so those lose the glow and take the inset ring instead. Four of the five controls of Partials/Profile/Field/Control.html - the text field, the two textareas and the select - therefore look different on a plain mouse click than they did before, and so do the file input of the image editor and the controls of the document and contract editors. The majority of the editor's controls change their appearance on a mouse click, not only under the keyboard. Buttons are the exception in the other direction: the theme's rule for .btn was already :focus-visible, so what changes for a button is the shape of the ring and not when it appears.

Its colour is currentcolor, the colour the control draws its own text in. That contrasts with the control by construction and in every colour mode, which no fixed colour and no Bootstrap custom property does - --bs-primary-text-emphasis is dark by design and measures 1.6:1 against the green of a focused .btn-success.

.form-check-input is the one control that reasoning does not cover, and it is worth stating rather than glossing over. Bootstrap draws the tick of a checkbox and the knob of a switch as a background image with a hardcoded #ffffff fill, not in currentcolor, so the ring on those two takes the inherited body colour instead of the colour of the mark. With the shipped theme that is #212121 on the #577760 of a checked control, which measures 3.23:1 - above the 3:1 WCAG 2.1 SC 1.4.11 asks of a focus indicator, but with nothing to spare. A site whose $primary is darker than the theme's falls below it, and should either lighten the control or override the outline-color of .form-check-input in the view.

The rule reaches every input, select, textarea and button of the view: the five controls of Partials/Profile/Field/Control.html, the image upload of Partials/Profile/Image/Editor.html, the synchronisation switch of Partials/Profile/Header.html and the buttons of every action group. Plain links are deliberately not covered - no theme rule takes their focus ring away. CKEditor 5 is covered by halves: its editable region is a div and keeps the library's own focus styling, while the buttons of its toolbar are button elements below the plugin root and take this ring instead.

Impact 

A site that did nothing about the focus appearance gets one clearly visible ring on every control and every button of the view, in both colour modes, and gets it in the panels where the previous appearance was cut away.

Check it with the mouse, not only with the keyboard. Most of the controls the view renders match :focus-visible on a plain pointer click, so the new ring replaces Bootstrap's glow for a visitor who never touches the tab key; only the checkbox and the synchronisation switch keep the appearance they had.

A site that styled the focus of the editor's controls itself has to check that its rule still wins. The shipped rule carries the plugin root three times against the theme's compat layer, which puts it at four class selectors and a tag name; a project rule that matched two class selectors now loses to it. Adding the plugin root to the project's own selector, or an !important, restores the project's appearance.

Affected Installations 

Installations of the Profile editing content element of EXT:academic_persons_edit whose site styles the focus of its form controls or buttons, or whose theme relied on styling them.

Important: Profile editing is replaced in place 

Description 

The profile editing frontend is rewritten from the ground up (Breaking: Replaced the profile editing view), but it keeps the identity of the content element it replaces. Nothing in the page tree, in a site configuration or in a deployment has to be renamed:

What Value
Content type (tt_content.CType) academicpersonsedit_profileediting
Extbase plugin AcademicPersonsEdit / ProfileEditing
Request namespace tx_academicpersonsedit_profileediting
Site sets fgtclb/academic-persons-edit and fgtclb/academic-persons-edit-profile-editing
Static template / page TSconfig EXT:academic_persons_edit/Configuration/TypoScript/Full and EXT:academic_persons_edit/Configuration/TSconfig/ProfileEditing/page.tsconfig
TypoScript object plugin.tx_academicpersonsedit
Default action list

Existing content records keep working after the update. There is no upgrade wizard for the content element and none is needed.

Impact 

An update replaces the rendered view and the markup below the plugin, not the record that renders it. What does change is described in Breaking: Replaced the profile editing view; what an integrator has to look at is the list of removed Fluid files, the removed action links and the new page type of the JSON endpoints.

Affected Installations 

All installations using the Profile editing content element of EXT:academic_persons_edit.

Important: An uploaded profile image carries its metadata 

Description 

A profile image uploaded in the frontend editor now arrives with its metadata filled instead of empty. Two records are involved, and they are written for different reasons:

  • The sys_file_reference row of the profile carries the composed name of the profile record it belongs to in title and alternative , and follows every later change of that name. This is the language-correct place for the text — a translation carries its own name, and the file may be shared between the languages of a profile. It is written by fgtclb/academic-persons , for a backend save as well as for a frontend one (changelog entry Breaking: The profile image translates of that extension).
  • The sys_file_metadata record of the uploaded file is filled once, by the upload that created the file, and only where it is empty: title , alternative and — where typo3/cms-filemetadata adds the column — copyright . These are what an installation running that extension or fgtclb/file-required-attributes reports as missing required attributes, and nothing else fills them for a file that was never touched in the backend. A value a backend editor maintained on the record is never overwritten, and a later change of the profile name does not reach it.

The composed name is the ordered non-empty values of title , first_name , middle_name and last_name , joined with single spaces.

Impact 

An image uploaded through the profile editor no longer shows up in File > Filelist as a file with missing required attributes, and the name of the person is rendered as alt and title text wherever the file is used — not only where the profile's own reference is rendered.

copyright is a column of sys_file_metadata only. The relation row sys_file_reference has no such column, in the core or through any system extension, so it carries the composed name in title and alternative and nothing else.

Nothing has to be configured for it. An installation that maintains file metadata editorially keeps what it maintained: the upload fills empty fields only.

What is written can be changed, and columns an installation adds itself can be filled, through the event ModifyProfileImageMetadataEvent of fgtclb/academic-persons — changelog entry Feature: Decide what a profile image's metadata will be of that extension.

Affected Installations 

Installations that let people upload a profile image in the frontend, in particular those requiring file metadata through typo3/cms-filemetadata or fgtclb/file-required-attributes .

Important: Validator exceptions moved to EXT:academic_base 

Description 

The two exceptions the form data validators of this extension raise moved to EXT:academic_base, next to the validation settings classes they belong to:

Before Now
\FGTCLB\AcademicPersonsEdit\Exception\UnknownValidatorException \FGTCLB\AcademicBase\Settings\Exception\UnknownValidatorException
\FGTCLB\AcademicPersonsEdit\Exception\UnsuitableValidatorException \FGTCLB\AcademicBase\Settings\Exception\UnsuitableValidatorException

No class aliases are registered for the old names. Both are raised by @internal classes only, and nothing in this set caught them from outside EXT:academic_persons_edit.

The validationEnsure ViewHelper moved to EXT:academic_base in the same step. No Fluid file of this extension declares the p namespace any more - the form partials that used it were removed with the editing rewrite (Breaking: Replaced the profile editing view), and the new editing view resolves the validation of a field from the settings graph in the controller instead of in the template.

Impact 

Code catching one of the two exceptions by its old name no longer matches and has to import the EXT:academic_base name. A Fluid override still declaring xmlns:p="http://typo3.org/ns/FGTCLB/AcademicPersons/ViewHelpers" fails to render, because Fluid resolves the ViewHelper class under the declared namespace and the old class no longer exists. In this extension that can only be an override of a file that no longer exists either - see Breaking: Replaced the profile editing view.

3.x Changes by type 

This lists all changes to the Academic Persons Edit extension of minor versions grouped by their type.

Table of contents

Breaking Changes 

Features 

Deprecations 

Important notes 

ChangeLog v2 

Every change to the Academic Persons Edit extension is documented here.

Also available 

2.4 Changes 

Table of contents

Breaking Changes 

Features 

Deprecation 

Important 

Breaking: Adapted frontend editing templates for contact record visibility 

Description 

To support showing and hiding contact records in the frontend (see Feature: Show and hide contact records in the frontend editing), the frontend editing templates and partials rendering the contact records of a contract were changed.

The following Fluid files were modified:

  • Resources/Private/Templates/Contract/Show.html
  • Resources/Private/Partials/Profile/List/PhysicalAddresses.html
  • Resources/Private/Partials/Profile/List/EmailAddresses.html
  • Resources/Private/Partials/Profile/List/PhoneNumbers.html

The relevant changes are:

  • The list partials no longer iterate the contract relation ({contract.physicalAddresses}, {contract.emailAddresses}, {contract.phoneNumbers}). They now iterate dedicated variables ({physicalAddresses}, {emailAddresses}, {phoneNumbers}) assigned by the controller, which also contain hidden records.
  • Contract/Show.html passes these new variables to the partials.
  • Each row gained a show/hide toggle and a "Hidden" badge, and the remaining row actions are only rendered for visible records.

Impact 

Installations that override or extend any of these templates or partials for project specific styling will not show the new show/hide functionality and may not list hidden records, until the overrides are adapted.

Affected Installations 

All installations using the EXT:academic_persons_edit extension that override the contract show template or the contact record list partials.

Migration 

Re-apply the project specific overrides on top of the updated templates and partials: switch the list iterations to the new {physicalAddresses}, {emailAddresses} and {phoneNumbers} variables and integrate the new show/hide toggle column.

Breaking: Removed the profile switcher plugin 

Description 

The content element academicpersonsedit_profileswitcher is no longer registered. It cannot be selected any more, and the content elements that still use it are removed by an upgrade wizard.

This is a cleanup that was missed in 2.1, not a change of behaviour.

Background 

The profile switcher used to be a real Extbase plugin. It let a frontend user pick one of their profiles, stored that choice in the session, and the profile editing plugin then worked on the chosen profile.

Version 2.1 restructured the frontend editing and replaced that concept: the profile editing plugin now lists every profile of the logged in user and links each one directly, so there is nothing left to switch between. The restructure removed the plugin registration, both controller actions (showProfileSwitch and executeProfileSwitch), the templates and the whole "active profile" infrastructure around it — the request middleware, the context aspect and the session handling.

What it did not remove was the content type registration. So since 2.1 the element has been offered to editors while nothing rendered it: there is no plugin configuration, no controller action and no TypoScript behind it. Selecting it produces an empty content element.

The same release also migrated the plugins from list_type to CType (see Breaking: Migrated extbase plugins from list_type to CType) and carried the dead type along, which is why leftover records exist in two different shapes today.

Upgrade wizard 

The upgrade wizard "Remove content elements of the removed academic_persons_edit profile switcher plugin" (academicPersonsEdit_removeProfileSwitcherContent) sets the affected content elements to deleted. It covers both shapes:

  • CType = academicpersonsedit_profileswitcher — records of an installation that ran the list_type to CType migration while it still carried this type along.
  • CType = list with list_type = academicpersonsedit_profileswitcher — records of an installation that never ran that migration. TYPO3 v14 removed the list_type column, so this shape is only looked for when the column exists.

The records are set to deleted, not removed from the table, so they stay visible in the recycler and can be restored if an installation wants to look at them before they are finally discarded.

The list_type to CType migration no longer migrates this plugin. A record that has not been migrated yet therefore stays in its legacy shape until the wizard above deletes it, instead of being moved onto a content type that does not exist.

Impact 

The content element cannot be selected any more, and existing elements of it are deleted by the upgrade wizard.

No rendered output changes. These elements have rendered nothing since 2.1, on every page they sit on, so removing them cannot change what a visitor sees. What changes is the backend: the type is gone from the content element type list, and any element the wizard has not deleted yet is shown with an "INVALID VALUE" badge in the page module instead of looking like a working plugin.

Affected Installations 

All installations using the EXT:academic_persons_edit extension that still have content elements of the profile switcher plugin — either as CType or as a legacy list_type record.

Installations that never used the plugin are not affected. The upgrade wizard reports nothing to do for them.

Migration 

Run the upgrade wizard in the Install Tool, or on the command line:

vendor/bin/typo3 upgrade:run academicPersonsEdit_removeProfileSwitcherContent
Copied!

To see which records are affected before running it:

SELECT uid, pid, header, CType FROM tt_content
 WHERE deleted = 0 AND CType = 'academicpersonsedit_profileswitcher';

-- TYPO3 v13 and below, for records that were never migrated:
SELECT uid, pid, header, CType, list_type FROM tt_content
 WHERE deleted = 0 AND CType = 'list'
   AND list_type = 'academicpersonsedit_profileswitcher';
Copied!

There is no replacement to migrate to. The profile editing plugin lists all profiles of the logged in user by itself, which is what the switcher was there for.

Breaking: Site sets and static templates have been restructured 

Description 

The TypoScript of this extension was shipped twice: the static template read Configuration/TypoScript/, and the site set fgtclb/academic-persons-edit shipped its own constants.typoscript and setup.typoscript, each of them a single @import of that folder. The page TSconfig existed only as Configuration/TSconfig/page.tsconfig, reachable through the auto-included Configuration/page.tsconfig and through nothing else — it was not selectable on a page at all.

Both mechanisms now read one physical copy of every file, and both of them deliver the extension per component instead of as one block:

  • Configuration/TypoScript/ProfileEditing/ holds the TypoScript of the Profile editing content element and is what the static template registers and what the set points its typoscript key at.
  • Configuration/TSconfig/ProfileEditing/page.tsconfig holds its page TSconfig and is what the page field Page TSconfig offers and what the set points its pagets key at.
  • Configuration/TypoScript/Full/ and Configuration/TSconfig/Full/page.tsconfig are the aggregates for installations that do not use site sets.

The content element is now hidden by default. The always-included Configuration/page.tsconfig removes academicpersonsedit_profileediting from the selectable content element types, and the page TSconfig of the component adds it back — so the element is offered where it is wanted instead of on every page of every installation. The TCA registration itself did not move, so the frontend renders existing records exactly as before. Editing such a record in the backend is a different matter — read the warning below before upgrading.

Impact 

A sys_template record that selected the old static template keeps its stored value, and that value now points at a folder holding no constants.typoscript and no setup.typoscript. It is not an error — the frontend simply loses the plugin configuration, and the plugin renders with no template paths.

A site package that imported one of the shipped files by path fails to resolve it. @import of a missing file is silent, so this also shows up as missing configuration rather than as an error message.

The Profile editing content element is no longer offered in the backend until the page TSconfig of the component is included, through the site set or through the page field Page TSconfig.

The set fgtclb/academic-persons-edit keeps its name and keeps delivering everything, so a site configuration that depends on it needs no change.

Affected Installations 

Installations that select the static template of this extension in a sys_template record, that import one of the shipped files from an own site package, or that use the content element without including the page TSconfig of this extension.

Migration 

Replace the static template entry in the sys_template record:

Old entry New entry
Academic Persons Edit Settings (academic_persons_edit), stored as EXT:academic_persons_edit/Configuration/TypoScript Academic Persons Edit: All components (academic_persons_edit), stored as EXT:academic_persons_edit/Configuration/TypoScript/Full — or Academic Persons Edit: Profile editing (academic_persons_edit), stored as EXT:academic_persons_edit/Configuration/TypoScript/ProfileEditing

Add the page TSconfig entry, which did not exist before, in the page record of the site root, tab Resources, field Page TSconfig: Academic Persons Edit: All components (academic_persons_edit), stored as EXT:academic_persons_edit/Configuration/TSconfig/Full/page.tsconfig. Without it the content element is not selectable any more, and existing records of it lose their CType when they are saved from the backend form.

Sites that use the site set instead need no migration — but they must not use both mechanisms at once, see the Configuration chapter.

Adjust every @import in an own site package:

Old path New path
EXT:academic_persons_edit/Configuration/TypoScript/constants.typoscript EXT:academic_persons_edit/Configuration/TypoScript/ProfileEditing/constants.typoscript
EXT:academic_persons_edit/Configuration/TypoScript/setup.typoscript EXT:academic_persons_edit/Configuration/TypoScript/ProfileEditing/setup.typoscript
EXT:academic_persons_edit/Configuration/TSconfig/page.tsconfig EXT:academic_persons_edit/Configuration/TSconfig/ProfileEditing/page.tsconfig

A site configuration may name the new component set instead of the aggregate:

Set Delivers
fgtclb/academic-persons-edit Unchanged in name, now delivers through the component set below.
fgtclb/academic-persons-edit-profile-editing The Profile editing content element only.

Feature: Show and hide contact records in the frontend editing 

Description 

Profile owners can now control which of their contact records are displayed on the public profile directly from the frontend editing plugin of EXT:academic_persons_edit.

For each physical address, email address and phone number a new "show"/"hide" toggle is available in the contract editing view. Hiding a record sets the standard TYPO3 hidden enable field on the record, which removes it from the public profile display while keeping the data intact.

The frontend editing list always shows hidden records (marked with a "Hidden" badge) so that they can be made visible again at any time. The other actions (view, edit, sort, delete) are only offered for visible records — a hidden record has to be shown again before it can be edited.

Records created and updated by the profile synchronization (EXT:academic_persons create/update profile commands) can be hidden as well: the synchronization keeps updating their data, but no longer changes their visibility once a profile owner has decided to hide them.

Impact 

Profile owners using the EXT:academic_persons_edit frontend editing gain fine-grained control over the visibility of their:

  • physical addresses
  • email addresses
  • phone numbers

without having to delete records they only want to hide temporarily.

Affected Installations 

All installations using the EXT:academic_persons_edit extension starting with version 2.4.

Migration 

No migration is required. Existing records keep their current visibility (they are visible unless they were already hidden in the backend). The new toggle can be used immediately.

Important: Adapted profile image partial 

Description 

Resources/Private/Partials/Profile/Show/Image.html read the image metadata from a view variable image that is never assigned. The profile detail view of the academicpersonsedit_profileediting plugin therefore never rendered the caption, and it emitted an empty alt and title attribute even when the file carried that metadata.

The profile carries a TYPO3CMSExtbaseDomainModelFileReference, and that class exposes nothing but getOriginalResource(). Every other property path on it resolves to null — which is why {profile.image.alternative} was as empty as {image.description}. All values are now read through originalResource:

<!-- before -->
<img alt="{profile.image.alternative}" title="{profile.image.title}">
<f:if condition="{image.description}">
    <figcaption class="visually-hidden">{image.description}</figcaption>
</f:if>

<!-- after -->
<img alt="{profile.image.originalResource.alternative}"
     title="{profile.image.originalResource.title}">
<f:if condition="{profile.image.originalResource.description}">
    <figcaption class="visually-hidden">{profile.image.originalResource.description}</figcaption>
</f:if>
Copied!

The same partial rendered a copyright, which never worked either and was removed instead of repaired. The partial itself is gone from version 3.0 on - see Breaking: Replaced the profile editing view.

Impact 

The profile detail view of the frontend editing plugin now renders

  • the alt and title attribute of the profile image from the file metadata instead of always empty,
  • a figcaption when the image has a description.

Note that nothing in this extension writes image metadata — an image uploaded through the plugin has none until it is maintained in the backend. Installations that never maintained the metadata of their profile images therefore see no change in the rendered output.

Affected Installations 

Installations using the academicpersonsedit_profileediting plugin, and any installation overriding Partials/Profile/Show/Image.html.

Migration 

No configuration change is required.

Installations that override the partial keep their own copy and stay unaffected — including its defect. Adopt the property paths above to render the metadata.

Important: Form data transformation only maps submitted fields 

Description 

The frontend editing of EXT:academic_persons_edit maps submitted form data transfer objects ( \FGTCLB\AcademicPersonsEdit\Domain\Model\Dto\AbstractFormData descendants) onto the domain models through the per-property factory classes in \FGTCLB\AcademicPersonsEdit\Domain\Factory\* .

Until now these factories wrote every property on each request, so a field that was not part of the submitted form was silently overwritten with the empty default of the form data object, wiping already persisted data. This is corrected: a property is now only applied when it was actually sent within the current request.

To make this possible the following additions were made (all of them on @internal classes that are not part of the public API):

  • \FGTCLB\AcademicPersonsEdit\Domain\Model\Dto\AbstractFormData carries the current request and the mapped argument name and exposes wasPropertySentInRequest(string $propertyName): bool to detect which properties were part of the submission. The request and argument name are provided by the new \FGTCLB\AcademicPersonsEdit\Property\TypeConverter\AbstractFormDataConverter , which is registered for all AbstractFormData based arguments in \FGTCLB\AcademicPersonsEdit\Controller\AbstractActionController::initializeAction() .
  • All factory classes (Profile, ProfileInformation, Contract, Address, Email, PhoneNumber) skip properties that were neither sent within the request nor registered as override. The existing readOnly / disabled validation configuration keeps precedence and continues to protect persisted data.
  • For the case where a property is not part of the request but still has to be written - for example when a PSR-14 event fills up data from another source before the transformation runs - AbstractFormData gained a per-property override store via setPropertyOverride(string $propertyName, mixed $value) , hasPropertyOverride(string $propertyName) and getPropertyOverride(string $propertyName) . Registered overrides are applied even when the property was not submitted.

Additionally \FGTCLB\AcademicPersonsEdit\Domain\Factory\ContractFactory::setValidTo() was fixed to evaluate the validation configuration of validTo instead of validFrom.

Impact 

The runtime behaviour of the frontend edit forms changes: submitting a form no longer resets fields that are not contained in that form. Fields are only written when they were part of the request or were explicitly registered as override on the form data object. No public method signature changed in an incompatible way.

Affected Installations 

Only installations that extend or replace the internal form data factory classes or AbstractFormData , or that relied on the previous "always overwrite" behaviour of the transformation, need to take the changed behaviour into account. All other installations benefit from the fix without any action required.

Migration 

No explicit migration is required. Custom code that populates a form data object outside of the request (e.g. within a PSR-14 event) and expects the value to be persisted must register it via AbstractFormData::setPropertyOverride() so the transformation applies it despite the property not being part of the request.

Feature: Added options to move items to top or bottom in lists 

Description 

Profile related lists now provide additional actions to quickly move entries directly to the first or last position without having to step through them item by item. This applies to contracts and profile information on a profile as well as email addresses, phone numbers and physical addresses on a contract.

Instead of only being able to move an item one step up or down, editors can now use dedicated "move to top" and "move to bottom" actions to rearrange the order of items more efficiently. The sorting values of affected records are normalized while reordering to keep a clean, strictly increasing sorting sequence.

Impact 

Editors working with the EXT:academic_persons_edit extension gain more convenient tools for managing the order of:

  • profile contracts
  • profile information
  • contract email addresses
  • contract phone numbers
  • contract physical addresses

The new options reduce the number of clicks required to bring a record to the top or bottom of a list and make it easier to maintain a stable and predictable display order.

Affected Installations 

All installations using the EXT:academic_persons_edit extension starting with version 2.3.

Migration 

No explicit migration is required. Existing records keep their current order and sorting values. The new actions can be used immediately to rearrange items as needed and will normalize sorting values on change.

Important: Added options to move items to top or bottom in lists 

Description 

Profile related lists now provide additional actions to quickly move entries directly to the first or last position without having to step through them item by item. This applies to contracts and profile information on a profile as well as email addresses, phone numbers and physical addresses on a contract.

Instead of only being able to move an item one step up or down, editors can now use dedicated "move to top" and "move to bottom" actions to rearrange the order of items more efficiently. The sorting values of affected records are normalized while reordering to keep a clean, strictly increasing sorting sequence.

Impact 

Editors working with the EXT:academic_persons_edit extension gain more convenient tools for managing the order of:

  • profile contracts
  • profile information
  • contract email addresses
  • contract phone numbers
  • contract physical addresses

The new options reduce the number of clicks required to bring a record to the top or bottom of a list and make it easier to maintain a stable and predictable display order.

Affected Installations 

All installations using the EXT:academic_persons_edit extension starting with version 2.3.

Migration 

No explicit migration is required. Existing records keep their current order and sorting values. The new actions can be used immediately to rearrange items as needed and will normalize sorting values on change.

Breaking: Migrated extbase plugins from list_type to CType 

Description 

TYPO3 v13 deprecated the tt_content sub-type feature, only used for CType=list sub-typing also known as list_type and mostly used based on old times for extbase based plugins. It has been possible since the very beginning to register Extbase Plugins directly as CType instead of CType=list sub-type, which has now done.

Technically this is a breaking change, and instances upgrading from 1.x version of the plugin needs to update corresponding tt_content records in the database and eventually adopt addition, adjustments or overrides requiring to use the correct CType.

Impact 

The change relates to following plugins:

  • academicpersonsedit_profileediting
  • academicpersonsedit_profileswitcher
  • academicpersons_listanddetail
  • academicpersons_selectedcontracts
  • academicpersons_selectedprofiles

Affected Installations 

All installations using the above listed plugins prior V2.0.

Migration 

A TYPO3 UpgradeWizard academicPersonsEdit_pluginUpgradeWizard is provided to migrate plugins from CType=list to dedicated CTypes matching the new registration.

Breaking: Removed nullable form dto argument from all controller newActions() 

Description 

The newAction() of the EXT:academic_persons_edit controllers only displays the initial form for create a new entity submitting data submission to the createAction() [POST] and never handles the form again. Following that, there is no need to have form data as optional argument for the newAction() and beside that this would allow to prefill data used for the form using link manipulations as the action is a GET action, which we do not want to work.

Not verified if having the form object as nullable for action in place has been required in earlier extbase days, but today that is absolutely not the case and could only be uses to prefill form data calling that action with corresponding get arguments, which can be considered dangerous and needs to be omitted in the first place.

In case the validation for the createAction() is invalid extbase calls the errorAction() of the controller forwarding the request internally to the newAction() along with the validationResult, already omitting to send the form data argument and being null in any-case. Using the <f:form.* /> fluid ViewHelpers to render the form elements takes care of this and keep the entered values as values even if the form dto object is initialized with empty values, which means that we do not have to take care of that ourself.

Impact 

The optional (nullable) form arguments are removed from following actions:

  • ContractController->newAction()
  • EmailAdressController->newAction()
  • PhoneNumberController->newAction()
  • PhysicalAddressController->newAction()
  • ProfileInformationController->newAction()

Affected Installations 

All installations using the EXT:academic_persons_edit extension version prior V2.1.

Migration 

Making this visible, for example this

public function newAction(
    Profile $profile,
    ?ContractFormData $contractFormData = null,
): ResponseInterface { /* ... */ }
Copied!

to

public function newAction(
    Profile $profile,
): ResponseInterface { /* ... */ }
Copied!

Important Notes 

There may be use-cases in projects to provide kind of prefilled actions, albeit this should be only a edge-case. In these cases, the project should implement a custom controller and action to create the entity with the prefilled data and display (redirect) to the editAction(). If that case rises up in projects, we may revisit this here and eventually come up with another solution, considering security aspects more seriously in these cases.

Sitemap