major
#29781
TL Views: Aktionsketten mit benannten Argumenten – Auslöser liefern mehrere Werte, Skripte verwenden sie als implizite Variablen, Aktionen mit Ergebnis führen Folgeaktionen in <with-result> aus
Kontext
In der React-Oberfläche (.view.xml-Schicht, com.top_logic.layout.view) führen Kommandos, Drops und andere Auslöser eine Kette von Aktionen aus (ViewActionChain). Bisher floss durch die Kette genau ein Wert: der Auslöser lieferte ihn, jede Aktion bekam das Ergebnis der vorherigen als Eingabe.
Das passte nicht zu Auslösern mit mehreren Parametern. Ein Drop (#29776) hat neben den gezogenen Objekten Referenzobjekte (Zielzeile bzw. Elternknoten und Einfügeposition). refuse-if bekam sie direkt als Argumente, die Aktionskette dagegen nur die gezogenen Objekte; die Referenzen mussten über target-channel/`parent-channel`/`before-channel` in Kanäle geschrieben werden, die in der View deklariert und in <execute-script> erneut als <inputs> aufgeführt wurden (die Drag-&-Drop-Demo brauchte dafür 9 Kanäle). In der klassischen Oberfläche erhalten canDrop und handleDrop dieselben Argumente direkt.
Positionsargumente (a -> b -> c -> …) lösen das nur für wenige Argumente mit offensichtlicher Reihenfolge. Jedes Skript muss alle Argumente erneut deklarieren; eine falsche Reihenfolge fällt nicht auf, und ein Skript, das mehr Parameter deklariert als es Argumente erhält, liefert eine Funktion statt eines Ergebnisses und hat keine Wirkung.
Lösung
Benannte Argumente
- Die Argumente einer Kette sind benannte Werte. Der Auslöser legt fest, welche Namen es gibt:
- Kommando: je Eintrag in inputs ein Argument mit dem Namen des Kanals; inputs="selection as ticket, other" bzw. <input channel="selection" as="ticket"/> benennt es um. inputs ersetzt die bisherige Eigenschaft input eines Kommandos; der erste Eintrag ist das Hauptargument. Ein Kommando ohne Eingaben und ohne Auslöserwert hat das Argument input.
- Auslöser, die einen Wert direkt liefern, geben ihm einen Namen, der am Auslöser konfiguriert werden kann; ohne Angabe gilt ein Standardname: Aktivierung einer Tabellenzeile (auch Zeilenbefehle) row und eines Baumknotens node (activate-as), Bestätigen eines value-input value (submit-as), Link in html link (link-as), Klick in ein Diagramm element (as), hochgeladene Datei file (as). Dieser Wert ist das Hauptargument; die inputs des Kommandos stehen zusätzlich zur Verfügung.
- Drop: die gezogenen Objekte (Standardname objects, umbenannt mit as) und die Referenzen der Drop-Art – target (auf eine Zeile/einen Knoten), before (geordnet), parent (geordnet im Baum/in der Baum-Tabelle); umbenannt mit target-as, parent-as, before-as. Eine Umbenennung für eine Referenz, die die Drop-Art nicht hat, ist ein Konfigurationsfehler.
Skripte mit impliziten Variablen
- Skripte in der Kette (<execute-script>, if, switch/`case`, confirm, notify, download, scroll-to-anchor, start-job mit den Skripten des Auftrags, evaluate-script) und refuse-if haben keinen Lambda-Kopf mehr: Sie verwenden die Argumente direkt als Variablen, z. B. refuse-if="$folder.recursion(p -> $p.container()).containsSome($nodes)". Die eigenen inputs einer Aktion sind zusätzliche Variablen mit dem Namen des Kanals. Die Fälle eines switch sehen dessen Wert als value (umbenannt mit as), Auftragsskripte den Auftrag als job (bei Blöcken zusätzlich state und chunk).
- Beim Übersetzen wird der Ausdruck mit genau den Namen umhüllt, die an dieser Stelle definiert sind; ein unbekannter Name ist ein Konfigurationsfehler beim Laden der View. Der Skript-Editor bietet die Namen über @ScriptContextVariables zur Vervollständigung an; Laufzeit und Editor ermitteln sie aus derselben Definition (ActionScopeDefinition).
- Das Übersetzen eines Ausdrucks mit impliziten Variablen ist als allgemeine Hilfsfunktion in com.top_logic.model.search verfügbar (QueryExecutor.compile(Expr, List), Expr.Define.withVariables, ModelReferenceChecker.checkModelElements(Expr, List)); die bestehenden Verwender impliziter Variablen (konfigurierte Skripte, OpenAPI-Server, Kafka-Verarbeitung, Flussdiagramme) nutzen sie.
- Ein Skript der View-Schicht, das einen Lambda-Kopf behält, ist ein Konfigurationsfehler beim Laden der View (statt wirkungslos eine Funktion zu liefern). Ein Test je Modul mit Views (TestViewScripts) prüft alle *.view.xml des Moduls darauf, ohne die Anwendung zu starten.
Skripte außerhalb der Kette
Auch alle übrigen Skripte der View-Schicht haben keinen Lambda-Kopf mehr. Eine Übersicht, welche Namen ein Skript wo sieht, steht im Entwicklerartikel view-layer/channels (Abschnitt "Which names a script sees").
- Eingabekanäle (inputs von Kanälen und Elementen, input einzelner Elemente wie visible-if, switch, progress, scroll-link) sind Variablen mit dem Namen des Kanals; inputs="selection as ticket" bzw. input="selection as ticket" benennt sie um, wie bei Kommandos. Betrifft u. a. <derived-channel> (expr; reverse sieht den geschriebenen Wert als value), <channel init>, Tabellenzeilen (rows), Filtervoreinstellungen, Exportdateiname, items von <object-list> und <kanban-board>, Kalender-objects, Diagrammdaten, <alert>, Optionen von <value-input>, Kachelbeschriftungen. Skripte in eingebetteten Konfigurationen (Filter, Spalten, Tooltips) sehen die Eingaben des umgebenden Elements.
- Werte, die ein Element selbst übergibt, haben je Eigenschaft einen dokumentierten Standardnamen, den das liefernde Element mit *-as umbenennen kann:
- Tabellen: row (row-as an <table>, <tree-table>, <composition-table>) in row-class und Spaltenskripten; value (value-as an der Spalte) für den eingegebenen Wert in update und den Zellwert in Skriptfiltern; column (column-as an <dynamic-columns>); fest members (Aggregation), left/`right` (Vergleich), params (Skriptfilter).
- Bäume: node (node-as) in children/`parents`. link/`remove` von <object-list> und die Karten eines <kanban-board>: element (element-as); Spalten des Boards column (column-as), on-reorder zusätzlich objects. Kalender: entry (entry-as) in den Ereignis-Skripten, start/`end`/`allDay`/`title` in on-move/`on-resize`/`on-create`.
- Assistent: step (step-as an <dynamic-steps>). Routenparameter: der Wert unter dem Namen des Parameters, reverse den URL-Text als value. Diagramm-Tooltips: der Datenpunkt als element (as). <flow-diagram> createChart: die Eingaben und die Handler unter ihren Namen.
- Ein Eingabename, der mit einem übergebenen Namen kollidiert, ist ein Konfigurationsfehler mit Hinweis auf as bzw. *-as.
- Ausführbarkeitsregeln: Die Regeln eines Kommandos (<visible-if>, <disabled-if> und die Regeln seiner Aktionen) werden über die benannten Argumente des Kommandos ausgewertet, wie die Kette (alle inputs und der Wert des Auslösers). Bisher sah eine Regel nur die erste Eingabe, nur Änderungen dieses Kanals lösten eine Neuprüfung aus, und Zustand der Schaltfläche und Prüfung pro Zeile sahen unterschiedliche Werte. Jetzt werden alle Eingaben beobachtet, und alle Prüfungen bilden die Argumente auf demselben Weg. Regeln außerhalb von Kommandos sehen ihren Wert unter dem Namen des liefernden Elements: Ziehen row/`node`/`element`, Ablegen target, Bearbeitbarkeit eines Formulars der Name seines Eingabekanals. Einfache Regeln (z. B. NullInputHidden) entscheiden weiter über das Hauptargument.
- <bind expr> und label-expr von <show-view>/`<show-views>` sehen die Argumente der Kette; die im DisplayTargetService konfigurierten Ziele sehen das angezeigte Objekt als object.
Fluss durch die Kette
- Die Argumente fließen unverändert durch die Kette. Aktionen sind Effekte; ihr Ergebnis ersetzt die Argumente der folgenden Aktionen nicht.
- Aktionen mit einem nützlichen Ergebnis führen Folgeaktionen in ihrem Element <with-result> aus. Diese laufen, nachdem die Aktion vollständig ausgeführt ist – einschließlich eines Commits, den die Aktion selbst durchführt; eine Folgeaktion bekommt also nie ein nicht festgeschriebenes Objekt. Sie sehen die äußeren Argumente und zusätzlich das Ergebnis unter dem Namen result bzw. dem mit <with-result as="…"> vergebenen Namen; ein innerer Name verdeckt einen gleichnamigen äußeren. Der Name wird nur angegeben, wenn eine Folgeaktion ihn verwendet.
- Betrifft <execute-script>, create-transient, persist-transient, read-form-object, store-form-state, link-element, remove-element, start-job, create-account, set-password, issue-access-token, evaluate-script, <with-transaction> und die Lade- und Bearbeitungsaktionen der Administration und des Modell-Editors. Umgesetzt als allgemeine Konfigurationseigenschaft (WithResultActions, WithResult) und ein Kettenschritt, der jede Aktion umhüllt, nicht pro Aktion.
- <open-dialog> liefert kein Ergebnis (die Kette läuft weiter, sobald der Dialog offen ist).
- <if> und <switch> führen ihre Aktionen mit denselben Argumenten aus.
- Transaktionen: <with-transaction> führt die in ihm stehenden Aktionen mit denselben Argumenten in einer Transaktion aus; sein Ergebnis ist das der letzten inneren Aktion. Aktionen, die den Zustand der Oberfläche ändern (Kanal schreiben, Dialog öffnen/schließen, Navigation, Meldungen, Bestätigungen, Downloads, …), dürfen nicht in der Transaktion stehen – schlägt der Commit fehl, zeigte etwa ein Kanal ein ungültiges Objekt; das ist ein Konfigurationsfehler beim Laden der View. Sie stehen im <with-result> der Transaktion, das erst nach erfolgreichem Commit läuft. Das gilt auch für das <with-result> einer Aktion innerhalb der Transaktion, da deren eigene Transaktion erst mit der äußeren festgeschrieben wird.
- Aktionen, die einen einzelnen Wert verarbeiten (<write-channel>, <open-dialog>, <delete-object>, <show-object>, …), lesen das mit value="<name>" benannte Argument (WithValueArgument). Ohne Angabe: das innerste Ergebnis, sonst das Hauptargument des Auslösers.
- <write-channel clear="true"/> leert einen Kanal.
- Die Kanal-Eigenschaften der Drops (target-channel, parent-channel, before-channel) bleiben optional für Fälle, in denen die Referenzen als Kanalzustand gebraucht werden.
Beispiele
Drop: {{{#!xml <drop accept="node" target="ordered" as="nodes" parent-as="folder" before-as="next"
refuse-if="$folder.recursion(p -> $p.container()).containsSome($nodes)">
<execute-script function="… $folder.add(…, $nodes) …"/>
</drop> }}}
Anlegen eines Meilensteins im Dialog (Kanal model mit dem transienten Entwurf): {{{#!xml <generic-command inputs="model">
<store-form-state/>
<persist-transient container="container" reference="milestones">
<with-result>
<write-channel name="selection"/>
<close-dialog/>
</with-result>
</persist-transient>
</generic-command> }}}
Anlegen in einer Transaktion, Ergebnis benannt: {{{#!xml <with-transaction>
<persist-transient>
<with-result as="created">
<execute-script function="$created.set(demo.tickets:Ticket#status, demo.tickets:TicketStatus#closed)"/>
</with-result>
</persist-transient>
<with-result>
<write-channel name="selection"/>
</with-result>
</with-transaction> }}}
Auswahl nach dem Löschen leeren: {{{#!xml <generic-command inputs="selection">
<delete-object/>
<write-channel name="selection" clear="true"/>
</generic-command> }}}
Zeilenklasse einer Tabelle mit umbenannter Zeile: {{{#!xml <table inputs="filterState as state" row-as="ticket"
row-class="if($ticket.get(demo.tickets:Ticket#done) && $state == 'all', 'tl-status-info', null)">
}}}
Technische Hinweise
- ViewAction erhält die benannten Argumente (ActionArguments); eine Default-Methode reicht für Aktionen mit Einzelwert das gewählte Argument weiter, sodass bestehende Aktionen unverändert bleiben. Aktionen, die den Zustand der Oberfläche ändern, melden das über ViewAction#changesViewState().
- Die Namen an einer Stelle der Konfiguration (ActionScope, inklusive der Kennzeichnung "innerhalb einer Transaktion") werden beim Instanziieren der Kette über den InstantiationContext weitergereicht.
- Skripte außerhalb der Kette werden über InputScript übersetzt: Eingabenamen plus die per @Supplies an der Eigenschaft deklarierten Namen; Laufzeit und Editor lesen dieselbe Deklaration.
- Die Engine-Views, die Views der Entwicklerwerkzeuge, des Agentenzugangs, der Selbstbedienung, der Skript-Konsole, die React-Demo und die Entwicklerdokumentation (tl-docs/view-layer) sind umgestellt; die Drag-&-Drop-Demo kommt ohne Hilfskanäle aus.
Migration
- Eine Aktion, die das Ergebnis der vorherigen Aktion verwendet, wird in deren <with-result> verschoben (z. B. <write-channel> in <execute-script><with-result>…</with-result></execute-script>). Das Attribut as einer Aktion wird zu <with-result as="…">. Folgeaktionen nach <if> oder <switch>, die deren Ergebnis verwenden, werden in das <with-result> der jeweiligen Ergebnis-Aktion darin verschoben; Folgeaktionen nach <with-transaction>, die dessen Ergebnis verwenden, in dessen <with-result>.
- Aktionen, die den Zustand der Oberfläche ändern und innerhalb von <with-transaction> stehen, werden in dessen <with-result> verschoben.
- input eines Kommandos wird zu inputs (inputs="kanal"); inputs-Kanäle sind in Skripten Variablen mit dem Namen des Kanals (bzw. dem as-Namen) statt führender Positionsargumente. input an Elementen sowie an <drag>/`<drop>` bleibt ein einzelner Kanal (jetzt mit optionalem as).
- Skripte in Aktionsketten und refuse-if verlieren ihren Lambda-Kopf und verwenden die benannten Argumente als Variablen (z. B. objects -> target -> … wird zu einem Ausdruck über $objects und $target; ein Aktivierungskommando verwendet $row bzw. $node, ein value-input $value).
- Auftragsskripte (start-job) verwenden den Auftrag als Variable $job statt als ersten Parameter.
- Auch alle übrigen Skripte in .view.xml-Dateien und in Konfigurationen des DisplayTargetService verlieren ihren Lambda-Kopf: Eingabekanäle heißen wie der Kanal (bzw. ihr as-Name), vom Element übergebene Werte haben die oben genannten Standardnamen (z. B. row -> … in row-class wird zu $row, node -> … in children zu $node, t -> $t.… mit inputs="selectedTicket" zu $selectedTicket.…). Ausführbarkeitsskripte eines Kommandos verwenden die Namen der Kommando-Eingaben (z. B. s -> jobIsRunning($s) mit inputs="jobState" wird zu jobIsRunning($jobState)). Ein verbliebener Lambda-Kopf wird beim Laden der View als Konfigurationsfehler gemeldet.
- <delete-object/> gefolgt von <write-channel> zum Leeren der Auswahl wird zu <write-channel ... clear="true"/>.
- Java: ViewActionChain.run/`nest` erwarten ActionArguments; ViewCommand.Config#getInput() entfällt zugunsten von getInputs(). WithResultActions hat statt getAs()/`getActions()` die Eigenschaft getWithResult() (WithResult mit getAs() und getActions()). Inputs#getInputs() liefert List<InputRef> statt List<ChannelRef>. ViewExecutabilityRule hat zusätzlich isExecutableFor(ActionArguments), das Regeln mit mehreren Argumenten überschreiben. Eine Regel, die sich nicht instanziieren lässt, ist ein Konfigurationsfehler statt stillschweigend ignoriert.