major
#29942
TL Views: Erwähnungen im HTML-Editor - Verweise auf Objekte (z.B. Projektmitglieder, Tickets) per @/# einfügen, die zu Links werden
Kontext
In einem HTML-Text (tl.model.wysiwyg:Html), etwa der Beschreibung eines Tickets, will der Anwender auf andere Objekte der Anwendung verweisen: auf Projektmitglieder, andere Tickets, Meilensteine. Heute muss er Namen abtippen; ein Verweis bleibt toter Text.
Gewünschtes Verhalten
- Beim Tippen eines Auslösezeichens (z.B. @` für Personen, `# für Tickets) im HTML-Editor der React-Oberfläche erscheint eine Vorschlagsliste passender Objekte, gefiltert durch den weiter getippten Text.
- Die Auswahl fügt einen Verweis auf das Objekt ein, der im Text als Link erscheint; ein Klick darauf öffnet das Objekt in der Oberfläche.
- Welche Objekte vorgeschlagen werden, berechnet ein TL-Script aus dem bearbeiteten Objekt und dem getippten Text (konfiguriert am Eingabeelement des HTML-Felds).
Technische Hinweise
- Der React-HTML-Editor ist Tiptap (com.top_logic.layout.react.wysiwyg).
- Baut auf #29887 auf (Vorschlagsliste als Part Listbox).
Lösung
Konfiguration. Das Eingabeelement WysiwygControlProvider eines HTML-Felds erhält eine Liste von Erwähnungen, eine je Auslösezeichen:
{{{#!xml <input-control class="com.top_logic.layout.react.wysiwyg.WysiwygControlProvider">
<mentions>
<mention trigger="@" presentation="chip" options="self -> query -> ...Projektmitglieder..."/>
<mention trigger="#" presentation="link" options="self -> query -> ...Tickets..."/>
</mentions>
</input-control> }}}
Die Funktion options erhält das bearbeitete Objekt und den hinter dem Auslösezeichen getippten Text und liefert die vorzuschlagenden Objekte; die Liste zeigt deren Beschriftung. Ein Auslösezeichen ist genau ein Zeichen und je Editor nur einmal vergeben, sonst meldet die Konfiguration einen Fehler.
Ablauf. Das Auslösezeichen und der folgende Suchtext werden im Browser erkannt (Tiptap-Hilfsmittel @tiptap/suggestion). Der Server wird nur gefragt, während eine Erwähnung getippt wird, und erhält nur Auslösezeichen und Suchtext - nicht den Inhalt des Editors. Normales Tippen erzeugt keine Anfrage. Die Vorschlagsliste ist der Part Listbox und wird damit auch von einer Komponentenbibliothek (MUI) gezeichnet; Pfeiltasten wählen, Enter oder Klick übernimmt, Escape schließt.
Gespeicherte Form. Ein Verweis wird als Objektverweis <a class="tlObject" href="?<Bookmark-Argumente>">Beschriftung</a> abgelegt (TLObjectLinkUtil), dem Format, das schon der klassische Editor schreibt und dem die Anzeige folgt; die Bookmark-Argumente benennen das Objekt stabil, nicht über eine Anzeige-URL.
Darstellung. Je Erwähnung wählbar (presentation):
- chip (Vorgabe): ein unteilbares Element, das als Ganzes gelöscht wird und dessen Text sich nicht verändern lässt; im HTML durch das Attribut data-mention (mit dem Auslösezeichen) gekennzeichnet, so dass es beim erneuten Öffnen wieder ein Chip ist.
- link: ein gewöhnlicher, editierbarer Objektverweis, dessen Beschriftung der Anwender anpassen kann.
Öffnen. In der Anzeige öffnet ein Klick auf den Verweis das Objekt (wie bisher), im Bearbeitungsmodus Strg/Cmd+Klick (ohne den umgebenden Absatz auszuwählen).
Objektverweise behalten ihre Form. Objektverweise schreibt der React-HTML-Editor ohne target="_blank" rel="noopener noreferrer nofollow", das die Link-Erweiterung jedem Link gibt; ein bestehender Objektverweis behält damit beim Bearbeiten seine gespeicherte Form. Gewöhnliche Weblinks öffnen weiterhin in einem neuen Fenster.
Browser-Verlauf beim Anzeigen eines Objekts. Das Anzeigen eines Objekts (Objektverweis, Link in Zelle oder Feld, <show-object>) ist genau ein Eintrag im Browser-Verlauf, auch wenn es dabei erst in einen anderen Bereich der Seitenleiste wechselt und dann bis zum Objekt hinunter navigiert. Bisher entstanden zwei Einträge, und "Zurück" landete auf einem Zwischenzustand, den der Anwender nie gesehen hatte. Technisch: RouteManager.beginNavigation() öffnet eine Navigation, die sich über mehrere, auch asynchrone Schritte erstrecken kann (z.B. während der Rückfrage zu ungespeicherten Änderungen); erst ihr Ende meldet die erreichte Adresse, einmal. RouteManager.navigate(Runnable) ist darauf aufgebaut.
Frames eines Tile-Stacks als Graph. Die <frame>-Deklarationen eines <tile-stack> beschreiben, welcher Frame auf welchen folgen darf:
- Jeder <frame> hat eine Pflicht-id, eindeutig im Stack. In einen <frame> geschachtelte <frame>`s sind die Frames, die auf ihn gelegt werden dürfen; `<frame-ref frame="<id>"/> nennt eine andere Deklaration als Nachfolger, auch eine umschließende - so lassen sich rekursive Strukturen (Ordner in Ordner) beschreiben. Deklarationen auf oberster Ebene folgen auf die Startsicht des Stacks.
- route ist optional; ein Frame ohne Route kann gelegt werden, hat aber keine Adresse (und alles darüber auch nicht).
- Lesen einer Adresse: jedes Segment wird nur gegen die Nachfolger des vorigen Frames geprüft; ein Segment an falscher Stelle beendet den wiederhergestellten Pfad dort. Schreiben der Adresse folgt dem Graphen entlang des tatsächlichen Pfads, so dass dieselbe Sicht an verschiedenen Stellen verschiedene Routen haben kann.
- Pfad-Parameter: ein <param>, dessen Name kein Platzhalter der Route ist, erscheint nicht in der Adresse und erhält beim Wiederherstellen den Wert des nächsten darunterliegenden Frames mit gleichnamigem Parameter. expr/`reverse` daran ist ein Konfigurationsfehler.
- <navigate-push frame="<id>"> legt einen Frame über seine Deklaration (Sicht, Beschriftung und Route kommen von dort). Beim Aufbau eines Frames wird geprüft, dass jeder Push darin einen Nachfolger nennt und seine Bindungen genau die Parameter dieses Frames belegen.
- Darstellungsziele (DisplayTargetService), <show-view>, <show-views>: ein Schritt, der in einen Tile-Stack hineinnavigiert, nennt den Frame (<show frame="<id>">); die Kette wird gegen den Graphen geprüft. Weitere Bindungen werden in die Kanäle der Frame-Sicht geschrieben. Ein <show view> für eine nirgends eingebundene Sicht ist ein Fehler. Dokumentiert im Entwicklerartikel view-layer/navigation.
{{{#!xml <tile-stack initial="projects/overview.view.xml" path="navPath">
<frame id="project" route="project/:project" view="projects/milestones.view.xml">
<param name="project" expr="objectId($project)" reverse="$value.objectResolve(tl.demo.projectManagement:ProjectScope)"/>
<frame id="milestones" route="milestones" view="projects/milestone-list.view.xml">
<param name="project"/>
...
</frame>
</frame>
</tile-stack> }}}
Demo. tl-demo-react: Beschreibung eines Tickets der Projektverwaltung - @` schlägt die Mitglieder des Projekts vor (als Chip), `# die übrigen Tickets des Projekts (als Link). Jede Ebene der Projekt-Navigation hat eine eigene Adresse, z.B. /view/projects/project/<id>/milestones/milestone/<id>/ticket/<id>, so dass "Zurück"/"Vor", Neuladen und direkte Links genau die jeweilige Seite zeigen. Im Kachel-Demo folgt die Person rekursiv auf die Person (/view/tiles-demo/person/<id>/person/<id>). Dokumentiert im Entwicklerartikel view-layer/forms.
Migration
Anwendungen mit <tile-stack>-Frames, <navigate-push> oder Darstellungszielen, die in einen Tile-Stack hineinnavigieren:
- Jeder <frame> eines <tile-stack> braucht eine im Stack eindeutige id: <frame id="person" route="person/:person" view="…">.
- <navigate-push view="pfad/x.view.xml" …> wird zu <navigate-push frame="<id der Frame-Deklaration>" …>. Der Push muss einen Nachfolger des Frames nennen, in dem er angezeigt wird (aus der Startsicht: einen <frame> der obersten Ebene), und bind-input-to plus <bind> müssen genau die Parameter des Frames belegen (Routen-Platzhalter und <param>`s). Für einen Wert, den ein Frame ohne Platzhalter erhält, `<param name="x"/> deklarieren. Jeder Frame, der gelegt wird, muss deklariert sein (bei Bedarf ohne route).
- Die <frame>-Deklarationen so schachteln, wie die Frames aufeinander gelegt werden; für Rekursion oder Wiederverwendung <frame-ref frame="<id>"/>. Eine flache Liste bedeutet jetzt: jeder Frame folgt nur auf die Startsicht.
- Darstellungsziele, <show-view>, <show-views>: ein Schritt, der in einen Tile-Stack hineinnavigiert, wird zu <show frame="<id>"> (die Bindungen müssen die Parameter des Frames abdecken). Schritte, die eingebundene Sichten aufdecken, behalten view="…".