enhancement
major
minor
major
minor
major
#29590
React view: Mehrwertige String-Werte werden in Tabellenzellen und Anzeigefeldern ohne Trennzeichen aneinandergehängt
Betrifft 8.0.0-SNAPSHOT (2026-09-15), tl-layout-view / tl-layout-react.
Gefunden beim Bau der Demo-Seite "Tabellenspalten" in tl-demo-react (#29554): Die Einbettung <embedded-columns reference="milestone.tickets"><column attribute="title"/></embedded-columns> sammelt über den mehrwertigen Schritt tickets die Titel aller Tickets des Meilensteins. Die Zelle zeigt die Titel jedoch als "T1T2" – direkt aneinandergehängt, ohne Trennzeichen.
Analyse
Das ist nicht der Spaltenmechanismus, sondern die generische typbasierte Anzeige mehrwertiger Werte:
- ColumnProviderService.displayContent(ColumnType, Object) zeigt jeden aufgelösten Zelltyp über FieldControlService.createDisplayControl(context, type, value), also über das schreibgeschützte Formularfeld des Typs (mit FieldSpec.multiple = true für mehrwertige Spalten).
- Für tl.core:String wählt FieldControlRegistry das Textfeld (TLTextInput). Die Multiplizität geht dabei nicht in die Wahl des Controls ein; der Wert – eine Liste von Strings – wird unverändert als JSON-Array an den Client übertragen.
- TLTextInput.tsx rendert im nicht-editierbaren Zustand {(value as string) ?? ''}. React gibt ein Array von Strings als benachbarte Textknoten aus, daher "T1T2".
Derselbe Pfad wird von jedem schreibgeschützten String-Feld mit multiple genommen (Anzeigeformular über ein mehrwertiges String-Attribut, <computed-column type="tl.core:String" multiple="true">, gesammelte Werte eingebetteter Spalten). Betroffen sind alle primitiven Typen, deren Control nur einen Einzelwert kennt: Zahl- und Datums-Controls prüfen instanceof Number bzw. instanceof Date und zeigen für eine Liste eine leere Zelle, die Checkbox macht aus einer Liste false. Im Bearbeitungsmodus schreibt das Textfeld die Liste als einen einzigen String zurück. Für Referenzen und Enumerationen läuft die Anzeige über das Select-Control, das Listen kennt.
Sortierung, Filter und Suchtext einer mehrwertigen Spalte laufen dagegen bereits über MetaLabelProvider / CollectionLabelProvider und liefern "T1, T2" – nur die gerenderte Zelle weicht ab.
Bei der Verifikation zeigte sich außerdem, dass ein mehrwertiges Datumsattribut gar nicht gespeichert werden kann: PrimitiveStorage.toJson kennt nur Zahlen, Wahrheitswerte und Strings und scheitert an einem Date-Element mit ClassCastException; die Datentypen tl.core:Date, Day, Time, DateTime und Boolean tragen keine <supports-multiple/>-Annotation, so dass der Modelleditor die Mehrwertigkeit für sie nicht anbietet.
Erwartung
Mehrwertige primitive Werte werden in der Anzeige mit einem Trennzeichen dargestellt (wie in der klassischen Oberfläche: Komma-getrennte Labels, bei mehrzeiligen Texten je Wert eine Zeile), im Bearbeitungsmodus mit einem Control, das eine Liste von Werten editieren kann. Die Entscheidung gehört an eine Stelle (FieldControlService/FieldControlRegistry: Typ × Multiplizität → Control), nicht in die einzelnen Controls.
Reproduktion: tl-demo-react, Seite "Tabellenspalten", Reiter "Eingebettete Spalten", Spalte "Meilenstein / Tickets / Titel".
Lösung
Entscheidung an einer Stelle. FieldControlRegistry.createControl(context, field, model, provider) entscheidet Typ × Multiplizität: Ist das Feld mehrwertig und editiert der Provider Einzelwerte, wird der Provider in ein Werte-Listen-Control eingepackt. Jeder Auflösungsschritt des FieldControlService (vom Display benanntes Control, TLInputControl-Annotation, konfiguriertes Control je Typ, Registry) läuft über diesen Helfer. Ein Provider, der selbst Sammlungen editiert, nimmt sich über die Default-Methode ReactFieldControlProvider.editsCollections() aus (SelectControlProvider, ConfigFieldControlProvider, AnnotationsFieldControlProvider).
Werte-Listen-Control. Das generische ReactValueListControl in tl-layout-react hält je Element ein Kind-Control, das der Element-Provider über ein ListElementFieldModel erzeugt (liest und schreibt Index i des Listen-Feldmodells; Schreiben legt die Sammlung als Liste in Elementreihenfolge zurück). Damit werden alle Element-Controls unverändert wiederverwendet: Zahl- und Datumsformate, URL-Links, Checkboxen, mehrzeiliger Text. Die Client-Komponente TLValueList zeigt die Kinder im Anzeigemodus inline mit ", " getrennt (wie Sortier-/Filter-/Suchtext der Spalte), bei mehrzeiligem Text je Wert eine Zeile. Im Bearbeitungsmodus bekommt jedes Element sein eigenes Eingabe-Control mit Entfernen-Schaltfläche, dazu eine Hinzufügen-Schaltfläche.
Umsortieren. Bei einem geordneten Attribut (ordered="true") trägt jede Zeile des Listen-Editors einen Anfasser, mit dem der Wert per Drag-and-drop an eine andere Position gezogen wird; mit Fokus auf dem Anfasser verschieben die Pfeiltasten auf/ab den Wert um eine Position. Dazu kennt FieldSpec die Ordnung des Felds (ordered, aus TLStructuredTypePart.isOrdered()), das Control nimmt das Kommando moveElement(index, targetIndex) entgegen und schreibt die umgestellte Liste zurück; die Element-Controls bleiben an ihrer Position und erhalten die verschobenen Werte. Die Drag-Logik (Start, Zielhälfte, korrigierter Einfügeindex, Anzeige des Einfügepunkts) liegt in einem gemeinsamen Hook der React-Bridge, den auch die Spaltenauswahl der Tabelle benutzt. Über dasselbe Flag erlaubt das Dropdown-Select für geordnete Referenzattribute das Umsortieren der gewählten Werte (customOrder), das bisher nie eingeschaltet war. Bei einem ungeordneten Attribut (Menge) gibt es keinen Anfasser.
Eingebettete Spalten. Eine Spalte, die ein einwertiges Attribut über einen mehrwertigen Schritt erreicht (ColumnType.collected()), wird mit der Multiplizität der Spalte angezeigt, nicht mit der des Attributs; Annotationen, Optionen und Control-Annotation kommen weiterhin vom Attribut.
Formate. Element-Formate (Zahl, Datum) werden auch für mehrwertige Felder ermittelt (FieldControlService.numberFormat/dateFormat(annotations, type) liefern das Format eines Werts). Der Suchtext, die Sortierung und der Textfilter einer mehrwertigen Spalte formatieren jedes Element mit diesem Format und verbinden mit ", ", so dass Suche, Sortierung, Filter und Zelle übereinstimmen.
Speicherung mehrwertiger Datumswerte. PrimitiveStorage schreibt ein Date-Element als JSON-Zahl (Millisekunden seit der Epoche), null als JSON-null, und weist andere Speicherobjekte mit einer IllegalArgumentException ab. Beim Lesen wird ein Zahl-Literal ohne Genauigkeitsverlust als Long (sonst Double) gelesen und für eine Speicherzuordnung mit Anwendungstyp Date in ein Datum gewandelt; bisher gespeicherte Werte bleiben lesbar. Die Datentypen tl.core:Date, Day, Time, DateTime und Boolean erhalten <supports-multiple/> (Tristate nicht: sein dritter Zustand ist null).
Demo. demo.react:Demo erhält die mehrwertigen Attribute aliases (String), notes (mehrzeiliger String), scores (Integer), ratios (Double mit Format), deadlines (Date), flags (Boolean); die Attributtabelle, das Detailformular, die Anlege- und Bearbeitungsdialoge und die editierbare Tabelle zeigen sie, "Beispiele erzeugen" befüllt sie, und die Seite "Tabellenspalten" bekommt neben den eingebetteten Spalten die mehrwertige berechnete Spalte "Titel im Meilenstein".
Nicht enthalten: die Annotation TLCollectionSeparator (betrifft nur den klassischen Texteditor).
Test
- TestReactValueListControl, TestFieldControlRegistry (tl-layout-react): Element-Controls, Schreiben eines Elements, Hinzufügen/Entfernen, Verschieben (vor/zurück, ungültige Indizes, schreibgeschützt, ungeordnet), externe Änderung, Editierbarkeit, Set-Wert, Registry-Entscheidung.
- TestFieldControlServiceMultiplicity, TestDateFormat (tl-layout-view): mehrwertige String/Integer/Date-Werte → Werte-Liste, Ordnungs-Flag aus dem Attribut, Element-Formate, gesammelte Attributspalte, Suchtext einer mehrwertigen Zahl-/Datumsspalte.
- TestPrimitiveStorage (tl-element): Roundtrip von Datums-, Long-, Boolean- und String-Listen, null-Element, große Long-Werte, alte Bruchzahl-Literale, nicht speicherbarer Typ.
- tl-demo-react: "Attribute" (Tabelle, Bearbeiten-Dialog mit Umsortieren per Drag und Pfeiltasten, Auswahl + Formular, Editierbare Tabelle) und "Tabellenspalten" → "Eingebettete Spalten".
Migration
Die Annotation <supports-multiple/> an den Datentypen tl.core:Boolean, Date, Day, Time und DateTime wird in bestehenden Datenbanken durch die automatische kbase-Migration Ticket_29590_multi_valued_datatypes (Modul tl-element) nachgezogen; ein manueller Schritt ist nicht nötig. Mehrwertige Datumsattribute konnten bisher nicht gespeichert werden, es gibt daher keine Altdaten in anderem Format; bestehende Listen von Zahlen, Wahrheitswerten und Strings werden unverändert gelesen.