Einordnung: Warum ein Import überhaupt eine UI braucht
Importfunktionen werden in Anwendungen häufig als rein technisches Detail betrachtet. Daten werden eingelesen, verarbeitet und stehen anschließend zur Verfügung – idealerweise ohne weitere Interaktion. In der Praxis ist ein Import jedoch selten ein unsichtbarer Vorgang. Er markiert einen Übergang zwischen bestehenden Systemzuständen, zwischen alten und neuen Daten, zwischen Vertrauen und Kontrolle. Genau an dieser Stelle entsteht die Notwendigkeit einer Benutzeroberfläche.
Table of Contents
- Einordnung: Warum ein Import überhaupt eine UI braucht
- Der Einstiegspunkt in der Anwendung
- Der Import-Dialog als geschlossener Arbeitsbereich
- Implementierung des ImportDialog in Vaadin
- Datei-Upload: Transport statt Interpretation
- Validierung als UI-Zustandswechsel
- Ergebnisdarstellung ohne Interpretation
- Aktionssteuerung und Apply-Logik
Im URL-Shortener ist der Import nicht als Hintergrundprozess konzipiert, sondern als expliziter, sichtbarer Vorgang. Die UI übernimmt dabei nicht die Rolle eines fachlichen Entscheiders, sondern die eines transparenten Vermittlers. Sie schafft einen klar abgegrenzten Raum, in dem ein Import stattfinden kann, ohne bestehende Daten unmittelbar zu verändern. Allein diese Entkopplung rechtfertigt bereits die Existenz einer eigenen Import-Oberfläche.
Der aktuelle Stand des Quelltexts ist auf GitHub verfügbar: https://github.com/svenruppert/url-shortener oder https://3g3.eu/url

Ein Import ist stets mit Unsicherheit verbunden. Selbst wenn das Datenformat bekannt ist, bleibt offen, wie sich eingehende Datensätze mit den bereits vorhandenen verhalten. Gibt es Überschneidungen? Entstehen Konflikte? Sind einzelne Einträge unvollständig oder ungültig? Diese Fragen lassen sich nicht allein durch einen technischen Prozess beantworten, sondern erfordern Sichtbarkeit. Die UI macht diesen Zwischenzustand greifbar, ohne ihn zu früh zu bewerten.
Statt den Import als atomaren Schritt zu behandeln, wird er im URL-Shortener als Zustand verstanden. Die Benutzeroberfläche zeigt diesen Zustand an, hält ihn fest und verhindert zugleich, dass er unbemerkt in einen produktiven Datenbestand übergeht. Dadurch wird der Import zu einem kontrollierten Vorgang, auch wenn die UI selbst keine inhaltliche Kontrolle ausübt.
Wichtig ist dabei die klare Rollenverteilung. Die Benutzeroberfläche interpretiert keine Daten, validiert keine Inhalte und trifft keine Entscheidungen über deren fachliche Korrektheit. Ihre Aufgabe besteht ausschließlich darin, den aktuellen technischen Stand des Imports sichtbar zu machen. Gerade diese bewusste Zurückhaltung ist entscheidend, denn sie verhindert, dass UI-Logik und Importlogik miteinander vermischt werden.
Der Import benötigt also keine UI, weil er komplex ist, sondern weil er einen Übergang darstellt. Die Oberfläche fungiert als Grenzfläche zwischen Datei und System, zwischen potenziellen Änderungen und bestehender Persistenz. Sie gibt dem Import einen Ort, einen Zeitpunkt und einen klaren Kontext – und genau darin liegt ihre Daseinsberechtigung.
Der Einstiegspunkt in der Anwendung
Der Import beginnt im URL-Shortener nicht beiläufig und auch nicht automatisch, sondern über einen bewusst vorgesehene Einstiegspunkt in der Anwendung. Er ist dort verortet, wo Benutzer mit bestehenden Kurz-URLs arbeiten und ihren aktuellen Datenbestand überblicken. Damit wird der Import klar als Ausnahmehandlung positioniert und nicht als Teil des alltäglichen Bearbeitungsflusses.


Der Einstieg erfolgt über eine explizite Aktion, die einen modalen Dialog öffnet. Diese Aktion erzeugt lediglich eine neue Instanz des Import-Dialogs und übermittelt ihm die notwendigen Abhängigkeiten. Weitere Logik ist an dieser Stelle bewusst nicht vorgesehen. Mit dem Öffnen des Dialogs verlässt der Benutzer den normalen Arbeitskontext der Anwendung und betritt einen klar abgegrenzten Bereich, in dem der Import vollständig gekapselt abläuft.
Der modale Charakter des Dialogs erfüllt dabei eine wichtige Funktion. Er signalisiert, dass der Import kein paralleler Prozess ist, der nebenbei ausgeführt wird, sondern ein zusammenhängender Ablauf mit eigenem Zustand. Solange der Dialog geöffnet ist, bleibt der restliche UI-Zustand unverändert. Es erfolgt keine implizite Aktualisierung und keine automatische Datenübernahme.
Aus Sicht der Benutzerführung entsteht damit ein klarer Schnitt. Der Import wird nicht als Erweiterung der Tabellenansicht verstanden, sondern als temporärer Arbeitsraum. Diese Trennung verhindert, dass Importaktionen mit regulären Bearbeitungsschritten vermischt werden. Gleichzeitig schafft sie eine mentale Orientierung: Alles, was innerhalb dieses Dialogs geschieht, gehört zum Import – und nichts darüber hinaus.
Technisch betrachtet übernimmt die UI an dieser Stelle keine weitere Verantwortung. Der Einstiegspunkt stößt weder eine Validierung noch eine Serverkommunikation an, sondern delegiert den gesamten Importvorgang an den Dialog selbst. Erst mit dem bewussten Schritt des Benutzers, eine Datei hochzuladen, beginnt der eigentliche Importprozess.
Ein Ausschnitt aus der entsprechenden View verdeutlicht dieses Prinzip. Der Button öffnet ausschließlich den Import-Dialog und übergibt einen optionalen Callback, der nach erfolgreichem Anwenden des Imports eine Rückkehr in den normalen UI-Kontext ermöglicht. Button importButton = new Button(“Import”);
Button importButton = new Button("Import");
importButton.addClickListener(event -> {
ImportDialog dialog =
new ImportDialog(urlShortenerClient, () -> {
refreshGrid();
});
dialog.open();
});
Der Code macht deutlich, dass der Einstiegspunkt lediglich den Dialog instanziiert und öffnet. Es findet weder eine Vorabprüfung noch eine Interaktion mit Server- oder Import-APIs statt. Der Callback dient ausschließlich der nachgelagerten Aktualisierung der View und ist nicht Teil des Importprozesses selbst.
Damit definiert der Einstiegspunkt nicht nur, wo der Import gestartet wird, sondern auch, wie er wahrgenommen wird. Er ist eine klare Zäsur im Nutzungskontext und bildet die Grundlage für alle folgenden Schritte im Import-Dialog.
Der Import-Dialog als geschlossener Arbeitsbereich
Mit dem Öffnen des Import-Dialogs wechselt die Anwendung bewusst in einen klar abgegrenzten Arbeitsmodus. Der Dialog ist nicht als bloßes Overlay gedacht, sondern als eigenständiger UI-Raum mit eigener Logik, eigenem Zustand und klar definierten Grenzen. Alles, was innerhalb dieses Dialogs geschieht, gehört ausschließlich zum Import – und bleibt so lange ohne Auswirkung auf den bestehenden Datenbestand, bis der Vorgang explizit abgeschlossen wird.
Diese Abschottung ist ein zentrales Gestaltungselement. Während der Dialog geöffnet ist, bleibt die restliche Anwendung in ihrem vorherigen Zustand eingefroren. Tabellen, Filter oder aktive Selektionen werden weder aktualisiert noch beeinflusst. Dadurch entsteht eine saubere Trennung zwischen laufender Arbeit und dem Importvorgang, die sowohl technische als auch mentale Orientierung schafft.
Der Import-Dialog ist als eigenständige Vaadin-Komponente implementiert. Er wird beim Öffnen vollständig initialisiert und besitzt keinen impliziten Rückkanal zur aufrufenden View. Diese Entkopplung ermöglicht, dass der Dialog seinen internen Zustand unabhängig verwalten kann. Weder beim Öffnen noch während der Interaktion wird davon ausgegangen, dass bereits Importdaten vorhanden sind. Der initiale Zustand ist stets neutral und leer.
Strukturell gliedert sich der Dialog in klar voneinander getrennte Bereiche, die jeweils eine bestimmte Phase des Imports repräsentieren. Diese Bereiche sind jedoch nicht als Schritt-für-Schritt-Wizard umgesetzt, sondern koexistieren innerhalb eines gemeinsamen Rahmens. Dadurch bleibt der Dialog jederzeit vollständig sichtbar und vermeidet implizite Übergänge, die den tatsächlichen Zustand verschleiern könnten.
Besonders wichtig ist dabei, dass der Dialog keine fachliche Interpretation der Importdaten vornimmt. Er stellt lediglich UI-Flächen bereit, in denen Ergebnisse angezeigt werden, sobald sie vom Server geliefert werden. Ob diese Flächen leer bleiben oder gefüllt werden, hängt ausschließlich vom aktuellen Importzustand ab, nicht von Annahmen oder Erwartungen der UI.
Auch die Steuerungselemente des Dialogs folgen diesem Prinzip. Aktionen wie Validierung oder Anwendung des Imports sind nicht dauerhaft verfügbar, sondern an den internen Zustand des Dialogs gebunden. Solange keine entsprechenden Importdaten vorliegen, bleibt der Dialog in einem passiven Zustand. Die UI erzwingt weder eine Reihenfolge noch einen Fortschritt, sondern reagiert ausschließlich auf klar definierte Zustandsänderungen.
Der Import-Dialog fungiert damit als kontrollierter Container für einen potenziell kritischen Vorgang. Er kapselt den Import vollständig, macht dessen aktuellen Stand sichtbar und verhindert zugleich, dass unvollständige oder ungeprüfte Daten unbemerkt wirksam werden. Diese klare Begrenzung bildet die Grundlage für alle weiteren Schritte im Importprozess und erklärt, warum der Dialog bewusst als geschlossener Arbeitsbereich konzipiert wurde.
Implementierung des ImportDialog in Vaadin
Der Import-Dialog im URL-Shortener zeigt sehr gut, wie wenig „Framework-Magie“ es braucht, um mit Vaadin Flow eine vollständige, zustandsgetriebene UI zu bauen. Die gesamte Funktionalität entsteht aus wenigen, gut verständlichen Bausteinen: einem Dialog als Container, Standardkomponenten wie Upload, Grid, Tabs und Button sowie einigen klaren Zustandsvariablen, die den Dialog über den Importprozess hinweg tragen. Das Ergebnis ist eine Oberfläche, die den Import als eigenen Arbeitsraum abgrenzt und den Quelltext dennoch überschaubar bleibt.
Der Einstieg ist bereits bemerkenswert simpel. Der Dialog ist eine ganz normale Java-Klasse, die Dialog erweitert. Dadurch ist die UI kein deklaratives Konstrukt und auch kein Template, sondern vollständig im Java-Code nachvollziehbar. Gleichzeitig bleibt der Dialog bewusst zustandsbehaftet: Er hält sowohl die hochgeladenen ZIP-Bytes als auch die stagingId, die aus der serverseitigen Validierung resultiert.
public final class ImportDialog
extends Dialog
implements HasLogger {
private final URLShortenerClient client;
private final Upload upload = new Upload();
private byte[] zipBytes;
private final Button btnValidate = new Button("Validate");
private final Button btnApply = new Button("Apply Import");
private final Button btnClose = new Button("Close");
private final Div applyHint = new Div();
private String stagingId;
Mit dieser Struktur ist bereits klar, wie Vaadin Flow hier arbeitet: Komponenten sind Objekte, und der Dialog besitzt sie wie gewöhnliche Felder. Die UI ist damit automatisch ansprechbar, ohne dass man „UI-IDs“ oder Bindings benötigt. Gleichzeitig wird der Importzustand nicht irgendwo extern gehalten, sondern gehört genau in den Dialog, in dem er sichtbar gemacht wird.
Der Konstruktor zeigt dann den Kern der Vaadin-Mechanik. Mit wenigen Zeilen wird der Dialog als modaler, resizable Container konfiguriert. Es gibt keine separate Konfigurationsdatei und kein DSL, sondern reine Java-Aufrufe. Damit lässt sich der visuelle Rahmen des Import-Dialogs unmittelbar nachvollziehen.
setHeaderTitle("Import");
setModal(true);
setResizable(true);
setDraggable(true);
setWidth("1100px");
setHeight("750px");
Direkt danach wird sichtbar, wie Vaadin Flow typischerweise Interaktionen modelliert. Die Buttons werden zunächst deaktiviert, und erst über Ereignisse werden sie freigeschaltet. Diese Initialisierung ist ein zentraler Teil des Zustandsmodells. Der Dialog startet immer neutral und erzwingt keinen Fortschritt, solange keine ZIP-Datei vorhanden ist.
btnValidate.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
btnValidate.setEnabled(false);
btnApply.addThemeVariants(ButtonVariant.LUMO_SUCCESS);
btnApply.setEnabled(false);
btnClose.addClickListener(_ -> close());
btnValidate.addClickListener(_ -> validate());
btnApply.addClickListener(_ -> applyImport());
Das Zusammenspiel aus UI und Datei-Upload lässt sich ebenso direkt ablesen. Der Upload wird auf ZIP-Dateien begrenzt und erhält eine Maximalgröße. Der entscheidende Punkt ist hier der In-Memory-Handler: Sobald die Datei erfolgreich hochgeladen ist, werden die Bytes in zipBytes abgelegt und der nächste Schritt freigeschaltet, indem btnValidate aktiviert wird. Der Übergang von „Upload vorhanden“ zu „Validierung möglich“ ist damit eine einzige, sehr verständliche Zustandsänderung.
upload.setAcceptedFileTypes(".zip", APPLICATION_ZIP);
upload.setMaxFiles(1);
upload.setMaxFileSize(IMPORT_MAX_ZIP_BYTES);
UploadHandler inMemoryUploadHandler = UploadHandler
.inMemory((metadata, bytes) -> {
String fileName = metadata.fileName();
long contentLength = metadata.contentLength();
logger().info("uploaded file: fileName: {} , contentLength {}", fileName, contentLength);
zipBytes = bytes;
logger().info("setting zipBytes..");
btnValidate.setEnabled(true);
});
upload.setUploadHandler(inMemoryUploadHandler);
Die eigentliche UI-Struktur entsteht anschließend in buildContent(). Auch hier zeigt sich die Stärke von Vaadin Flow: Layouts sind Komponenten, und der Dialog wird schlicht durch das Zusammensetzen von Komponenten aufgebaut. Überschriften, Upload-Bereich, Preview-Summary, Tabs und ein zentraler Inhaltscontainer werden in einem VerticalLayout zusammengeführt. Der Code liest sich dabei wie eine Beschreibung der Oberfläche.
var root = new VerticalLayout(
new H3("Upload ZIP"),
upload,
new H3("Preview"),
summary,
applyRow,
tabs,
tabContent
);
root.setSizeFull();
root.setPadding(false);
renderTab(tabContent);
return root;
Für die Ergebnisdarstellung nutzt der Dialog zwei Grid-Instanzen und schaltet über Tabs zwischen ihnen um. Entscheidend ist hier, dass die Tab-Interaktion keine Logik im Backend auslöst, sondern lediglich die sichtbare Projektion wechselt. Das Umschalten ist in Vaadin Flow ein einzelner Listener, der den Container leert und die passende Kombination aus Paging-Bar und Grid einsetzt.
tabs.addSelectedChangeListener(e -> renderTab(tabContent));
private void renderTab(VerticalLayout container) {
container.removeAll();
container.setSizeFull();
if (tabs.getSelectedTab() == tabInvalid) {
container.add(pagingInvalid, gridInvalid);
container.expand(gridInvalid);
} else {
container.add(pagingConflicts, gridConflicts);
container.expand(gridConflicts);
}
}
An dieser Stelle wird deutlich, warum die Implementierung trotz mehrerer Zustände so kompakt bleibt. Vaadin Flow liefert die UI-Bausteine, und der Dialog verbindet sie über wenige, klar definierte Zustandsvariablen und Listener. Der eigentliche Importprozess bleibt dabei serverseitig. Die UI muss lediglich wissen, wann ein Zustand erreicht ist und wie dieser angezeigt werden soll.
Damit ist der ImportDialog ein gutes Beispiel dafür, wie Vaadin Flow Komplexität reduziert: Nicht durch Verstecken von Logik, sondern durch ein direktes Programmiermodell, in dem UI-Komponenten, Ereignisse und Zustand im selben, gut lesbaren Java-Code zusammenlaufen. Gerade für eine minimalistische Importfunktion entsteht so schnell eine vollständige, robuste Oberfläche, ohne dass zusätzliche Framework-Schichten nötig wären.
Datei-Upload: Transport statt Interpretation
Der eigentliche Importprozess beginnt im Import-Dialog nicht mit einer fachlichen Entscheidung, sondern mit einem rein technischen Schritt: dem Hochladen einer ZIP-Datei. Dieser Moment markiert den Übergang vom leeren, neutralen Dialogzustand hin zu einem potenziell verarbeitbaren Import, ohne bereits irgendeine inhaltliche Bewertung vorzunehmen.
Die Upload-Komponente ist im Dialog fest verankert und von Beginn an sichtbar. Sie bildet bewusst den ersten Interaktionspunkt im geschlossenen Arbeitsbereich. Dabei ist ihre Aufgabe klar begrenzt. Der Upload dient ausschließlich dazu, eine Datei entgegenzunehmen und deren Inhalt temporär im UI-Kontext verfügbar zu machen. Weder die Struktur noch die Semantik der enthaltenen Daten spielen an dieser Stelle eine Rolle.
Diese Haltung spiegelt sich direkt im Quelltext wider. Der Upload ist so konfiguriert, dass genau eine ZIP-Datei akzeptiert wird und die maximale Dateigröße nicht überschritten werden darf. Diese Einschränkungen dienen nicht der fachlichen Validierung, sondern ausschließlich der technischen Absicherung des UI-Prozesses.
upload.setAcceptedFileTypes(".zip", APPLICATION_ZIP);
upload.setMaxFiles(1);
upload.setMaxFileSize(IMPORT_MAX_ZIP_BYTES);
Sobald eine Datei ausgewählt und hochgeladen wird, übernimmt ein In-Memory-Upload-Handler die Verarbeitung. Der Dialog speichert den vollständigen Inhalt der ZIP-Datei als Byte-Array in einem internen Feld. Zu diesem Zeitpunkt wird keinerlei Prüfung vorgenommen, ob die Datei tatsächlich Importdaten enthält oder ob diese korrekt strukturiert sind. Für die UI ist die Datei zunächst nichts weiter als ein binärer Block.
UploadHandler inMemoryUploadHandler = UploadHandler
.inMemory((metadata, bytes) -> {
zipBytes = bytes;
btnValidate.setEnabled(true);
});
upload.setUploadHandler(inMemoryUploadHandler);
Diese wenigen Zeilen markieren einen entscheidenden UI-Zustandswechsel. Mit dem erfolgreichen Abschluss des Uploads wird die Schaltfläche zur Validierung aktiviert. Mehr passiert an dieser Stelle bewusst nicht. Der Upload allein löst keine Server-Kommunikation aus und erzeugt auch keinen Importzustand. Er signalisiert lediglich, dass nun genügend Informationen vorliegen, um den nächsten Schritt explizit durch den Benutzer auszulösen.
Auch Fehler beim Upload werden ausschließlich aus technischer Perspektive behandelt. Wird eine Datei abgelehnt, etwa aufgrund eines falschen Datentyps oder einer zu großen Dateigröße, reagiert die UI mit einer entsprechenden Benachrichtigung. Eine inhaltliche Bewertung der Datei findet jedoch weiterhin nicht statt.
upload.addFileRejectedListener(event -> {
String errorMessage = event.getErrorMessage();
Notification notification = Notification.show(errorMessage, 5000,
Notification.Position.MIDDLE);
notification.addThemeVariants(NotificationVariant.LUMO_ERROR);
});
Der Upload-Abschnitt des Dialogs verdeutlicht damit einen zentralen Gestaltungsgrundsatz: Die Benutzeroberfläche interpretiert keine Importdaten. Sie stellt lediglich sicher, dass eine Datei technisch korrekt entgegengenommen wird, und überführt diesen Zustand in eine klar erkennbare nächste Handlungsmöglichkeit.
Erst mit dem bewussten Klick auf die Schaltfläche „Validate“ verlässt der Dialog diesen rein technischen Vorbereitungszustand. Der Datei-Upload bildet damit die notwendige, aber bewusst inhaltsfreie Grundlage für alle weiteren Schritte des Importprozesses.
Validierung als UI-Zustandswechsel
Mit dem Klick auf die Schaltfläche „Validate“ verlässt der Import-Dialog erstmals seinen rein vorbereitenden Zustand. Bis zu diesem Zeitpunkt wurden ausschließlich technische Voraussetzungen geschaffen: eine Datei wurde hochgeladen und im Speicher gehalten, ohne dass deren Inhalt interpretiert oder weiterverarbeitet wurde. Die Methode validate() markiert nun den klaren Übergang von einem passiven UI-Zustand zu einem zustandsverändernden Schritt, der den Import erstmals sichtbar macht.
Aus Sicht der Benutzeroberfläche ist die Validierung kein fachlicher Prüfprozess, sondern ein koordinierter Ablauf mehrerer UI-Aktualisierungen, die alle durch eine einzige Benutzeraktion ausgelöst werden. Der Dialog übernimmt dabei bewusst keine inhaltliche Verantwortung. Er fordert den Server auf, den hochgeladenen ZIP-Inhalt zu prüfen, und verarbeitet ausschließlich die technische Antwort, die er darauf erhält.
Der Einstieg in die Methode ist entsprechend defensiv gehalten. Zunächst wird geprüft, ob überhaupt eine ZIP-Datei im Dialogzustand vorhanden ist. Fehlt diese, reagiert die UI mit einer kurzen Benachrichtigung und bricht den Vorgang ab. An dieser Stelle wird keine Ausnahme propagiert und kein interner Zustand verändert. Die Validierung ist damit klar an eine vorherige, explizite Benutzeraktion gebunden.
if (zipBytes == null || zipBytes.length == 0) {
Notification.show("No ZIP uploaded.", 2500, Notification.Position.TOP_CENTER);
return;
}
Erst wenn diese Voraussetzung erfüllt ist, wird der eigentliche Validierungsschritt ausgelöst. Der Dialog übergibt den vollständigen Inhalt der ZIP-Datei an den Client und ruft den dedizierten Server-Endpunkt zur Import-Vorschau auf. Für die UI ist dieser Aufruf eine Black Box. Sie kennt weder die internen Validierungsregeln noch die Kriterien, nach denen Einträge als neu, konfliktbehaftet oder ungültig eingestuft werden.
String previewJson = client.importValidateRaw(zipBytes);
Die Antwort des Servers wird vollständig als JSON-String entgegengenommen. Anstatt diesen in ein festes Datenmodell zu überführen, extrahiert die UI gezielt einzelne Werte, die für die Darstellung des Importzustands relevant sind. Dazu gehören die erzeugte stagingId sowie die Zählwerte für neue, konfliktbehaftete und ungültige Einträge. Diese Werte werden unmittelbar in der Oberfläche sichtbar gemacht und bilden den ersten konkreten Importzustand, den der Dialog anzeigt.
this.stagingId = extractJsonString(previewJson, "stagingId");
int newItems = extractJsonInt(previewJson, "newItems", 0);
int conflicts = extractJsonInt(previewJson, "conflicts", 0);
int invalid = extractJsonInt(previewJson, "invalid", 0);
Mit dem Setzen dieser Werte verändert sich der Charakter des Dialogs spürbar. Der zuvor leere Preview-Bereich wird befüllt, und der Import erhält erstmals eine Identität in Form der stagingId. Dennoch bleibt die UI weiterhin strikt beschreibend. Sie bewertet nicht, ob die Zahlen plausibel sind oder in einem bestimmten Verhältnis zueinander stehen. Sie zeigt lediglich den aktuellen technischen Stand an.
Parallel dazu liest der Dialog die Ergebnislisten direkt aus der Server-Antwort. Über Iteratoren werden die JSON-Arrays für Konflikte und ungültige Einträge ausgelesen und in UI-interne Zeilenobjekte überführt. Diese werden anschließend den entsprechenden Grids zugewiesen. Auch hier findet keine Interpretation statt. Ist ein Array leer oder nicht vorhanden, bleiben die Tabellen leer – ein Zustand, den die UI als vollständig gültig behandelt.
for (String obj : new ItemsArrayIterator(r, "conflictItems")) {
Map<String, String> m = JsonUtils.parseJson(obj);
conflictRows.add(ConflictRow.from(m));
}
Nach der Befüllung der Grids werden Paging-Informationen aktualisiert und die zugehörigen UI-Komponenten synchronisiert. Auch dieser Schritt erfolgt ausschließlich auf Basis der bereits ermittelten Ergebnislisten, ohne weitere Rückkopplung zum Server.
Mit Abschluss der Methode validate() befindet sich der Import-Dialog in einem neuen, stabilen Zustand. Die hochgeladenen Daten sind serverseitig geprüft, die Ergebnisse sind sichtbar, und der Dialog kann nun entscheiden, welche weiteren Aktionen dem Benutzer angeboten werden. Wichtig ist dabei, dass die Validierung selbst keinen Import auslöst. Sie verändert ausschließlich den UI-Zustand und schafft Transparenz über das, was ein anschließender Import bewirken würde.
Die Methode validate() ist damit der zentrale Dreh- und Angelpunkt des gesamten Importdialogs. Sie verbindet Upload und Ergebnisdarstellung, ohne selbst fachliche Entscheidungen zu treffen. Genau in dieser Rolle wird sie zum Kern des UI-gesteuerten Importprozesses.
Ergebnisdarstellung ohne Interpretation
Nach Abschluss der Validierung befindet sich der Import-Dialog in einem Zustand, in dem erstmals konkrete Ergebnisse vorliegen. Diese Ergebnisse werden jedoch nicht interpretiert, gewichtet oder weiterverarbeitet, sondern ausschließlich dargestellt. Kapitel 6 beschreibt genau diesen Moment: die Transformation von serverseitig gelieferten Rohinformationen in sichtbare UI-Strukturen – ohne dass die Benutzeroberfläche daraus eigene Schlüsse zieht.
Zentral für dieses Kapitel ist die Erkenntnis, dass der Dialog keine fachliche Sicht auf Konflikte oder ungültige Einträge besitzt. Er kennt weder deren Bedeutung noch deren Auswirkungen. Stattdessen übernimmt er eine rein mechanische Aufgabe: Er nimmt Listen von Ergebnissen entgegen und projiziert sie auf vorbereitete UI-Komponenten.
Diese Projektion erfolgt über zwei getrennte Tabellen, die jeweils einem spezifischen Ergebnistyp zugeordnet sind. Konflikte und ungültige Einträge werden bewusst nicht gemeinsam dargestellt, sondern in separaten Kontexten gehalten. Der Dialog bietet dafür zwei Tabs an, zwischen denen der Benutzer wechseln kann. Dieser Wechsel verändert jedoch ausschließlich die Sicht auf die Daten, nicht deren Inhalt oder Zustand.
Die technische Grundlage dieser Darstellung bilden zwei Grid-Komponenten, die bereits beim Aufbau des Dialogs vollständig konfiguriert werden. Spalten, Breiten und Darstellungslogik sind festgelegt, bevor überhaupt bekannt ist, ob jemals Daten angezeigt werden. Die Grids existieren somit unabhängig vom Vorhandensein von Ergebnissen und stellen eine stabile Projektionsfläche dar.
gridConflicts.addColumn(ConflictRow::shortCode).setHeader("shortCode").setAutoWidth(true).setFlexGrow(0);
gridConflicts.addColumn(ConflictRow::diff).setHeader("diff").setAutoWidth(true).setFlexGrow(0);
gridConflicts.addColumn(ConflictRow::existingUrl).setHeader("existingUrl").setAutoWidth(true);
gridConflicts.addColumn(ConflictRow::incomingUrl).setHeader("incomingUrl").setAutoWidth(true);
Die Befüllung dieser Grids erfolgt nicht über ein klassisches Datenmodell, sondern über eine direkte Iteration über JSON-Fragmente aus der Server-Antwort. Die UI liest dabei jedes einzelne Objekt aus den entsprechenden Arrays und wandelt es in eine einfache Zeilenrepräsentation um. Diese Zeilenobjekte enthalten exakt die Felder, die angezeigt werden sollen, nicht mehr und nicht weniger.
for (String obj : new ItemsArrayIterator(r, "conflictItems")) {
Map<String, String> m = JsonUtils.parseJson(obj);
conflictRows.add(ConflictRow.from(m));
}
Bemerkenswert ist dabei, dass der Dialog keinerlei Annahmen darüber trifft, wie viele Einträge erwartet werden oder ob bestimmte Felder zwingend vorhanden sein müssen. Ist ein Array leer oder kann es nicht gelesen werden, bleibt die zugehörige Tabelle leer. Dieser Zustand wird von der UI nicht als Fehler behandelt, sondern als gültiges Ergebnis der Validierung.
Auch die Tab-Steuerung folgt diesem Prinzip der Neutralität. Der aktuell ausgewählte Tab bestimmt lediglich, welche Tabelle sichtbar ist. Beim Umschalten werden keine Daten neu geladen und keine Zustände verändert. Die UI zeigt ausschließlich einen anderen Ausschnitt desselben Importzustands.
tabs.addSelectedChangeListener(e -> renderTab(tabContent));
Ergänzt wird die Ergebnisdarstellung durch eine einfache Paging-Komponente, die ausschließlich auf den bereits geladenen Ergebnissen operiert. Sie dient der besseren Übersicht bei größeren Datenmengen, ist jedoch vollständig lokal und führt keine weiteren Server-Abfragen aus. Auch hier wird kein inhaltlicher Filter angewendet; das Paging ist rein darstellungsorientiert.
Das Zusammenspiel aus Grids, Tabs und Paging ergibt eine Benutzeroberfläche, die bewusst zurückhaltend agiert. Sie zeigt, was vorhanden ist, und zeigt nichts, was nicht eindeutig geliefert wurde. Weder wird versucht, fehlende Daten zu kompensieren, noch werden implizite Annahmen über den „eigentlichen“ Zustand des Imports getroffen.
Damit wird deutlich, dass die Ergebnisdarstellung im Import-Dialog kein Analysewerkzeug ist, sondern ein Spiegel. Sie reflektiert exakt den vom Server gelieferten Zustand und überlässt jede weitergehende Bewertung den nachfolgenden Schritten des Importprozesses.
Aktionssteuerung und Apply-Logik
Nachdem Upload, Validierung und Ergebnisdarstellung abgeschlossen sind, verbleibt im Import-Dialog eine zentrale Frage: Darf der Import tatsächlich angewendet werden? Die Beantwortung dieser Frage erfolgt nicht implizit und auch nicht automatisch. Stattdessen wird sie vollständig durch den UI-Zustand gesteuert.
Die Grundlage dieser Steuerung bildet kein zusätzlicher Serveraufruf, sondern der bereits bekannte Importzustand. Alle Informationen, die für die Entscheidung relevant sind, liegen der UI zu diesem Zeitpunkt bereits vor. Die Methode updateApplyState() fungiert dabei als zentrales Scharnier, das diesen Zustand interpretiert und in konkrete UI-Aktivierungen oder -Deaktivierungen übersetzt.
Ausgangspunkt ist ein strikt defensiver Default-Zustand. Unabhängig davon, was zuvor geschehen ist, wird die Apply-Schaltfläche zunächst deaktiviert. Die UI geht nie davon aus, dass ein Import automatisch anwendbar ist. Erst wenn alle Bedingungen explizit erfüllt sind, wird dieser Zustand aufgehoben.
btnApply.setEnabled(false);
btnApply.setText("Apply Import");
Der erste harte Prüfpunkt ist das Vorhandensein einer stagingId. Ohne diese Kennung existiert aus UI-Sicht kein valider Importkontext. Selbst wenn bereits Daten angezeigt werden sollten, bleibt der Import nicht anwendbar, solange kein serverseitig bestätigter Staging-Zustand vorliegt. Die UI behandelt diesen Fall nicht als Fehler, sondern als unvollständigen Zustand.
Anschließend werden die beiden Ergebnisdimensionen betrachtet, die bereits in den vorherigen Kapiteln sichtbar gemacht wurden: ungültige Einträge und Konflikte. Ungültige Einträge blockieren den Import grundsätzlich. Sobald mindestens ein ungültiger Datensatz vorhanden ist, bleibt die Apply-Funktion deaktiviert, und der Dialog kommuniziert diesen Zustand explizit über einen Hinweistext. Die UI erzwingt damit keine Korrektur, macht aber deutlich, dass ein Import unter diesen Bedingungen nicht möglich ist.
Konflikte werden hingegen anders behandelt. Sie stellen aus UI-Sicht keinen absoluten Ausschluss dar, sondern eine bewusste Entscheidungssituation. Der Dialog bietet hierfür eine Checkbox an, mit der der Benutzer festlegen kann, ob konfliktbehaftete Einträge beim Anwenden des Imports übersprungen werden sollen. Erst durch diese explizite Zustimmung wird der Import trotz vorhandener Konflikte freigegeben.
Diese Differenzierung zeigt sich direkt im Zusammenspiel von Checkbox, Hinweistext und Apply-Schaltfläche. Das Aktivieren oder Deaktivieren der Checkbox führt unmittelbar zu einer Neubewertung des UI-Zustands, ohne dass dafür weitere Daten geladen oder neu berechnet werden müssen. Die UI reagiert hier ausschließlich auf den bereits bekannten Zustand.
if (conflicts > 0 && !chkSkipConflicts.getValue()) {
applyHint.setText("Apply disabled: " + conflicts + " conflict(s). Tick “Skip conflicts on apply” to proceed.");
return;
}
Wird der Import schließlich freigegeben, ändert sich nicht nur die Aktivierung der Schaltfläche, sondern auch deren Beschriftung. Damit signalisiert die UI klar, unter welchen Bedingungen der Import ausgeführt wird. Dieser visuelle Hinweis ist Teil der Aktionssteuerung und dient der Transparenz, nicht der Durchsetzung fachlicher Regeln.
Mit dem Klick auf „Apply Import“ verlässt der Dialog seinen reinen Anzeige- und Entscheidungsmodus. Erst an dieser Stelle wird ein weiterer Serveraufruf ausgelöst, der den zuvor validierten Import tatsächlich anwendet. Bis zu diesem Moment hat die UI ausschließlich Zustände verwaltet, angezeigt und Entscheidungen eingefordert. Die Aktionssteuerung bildet damit den bewussten Abschluss des Importdialogs. Sie bündelt alle zuvor aufgebauten Informationen und überführt sie in eine explizite Benutzerentscheidung. Gerade diese Zurückhaltung – nichts automatisch anzuwenden, nichts zu implizieren – macht den Dialog zu einem kontrollierten und nachvollziehbaren Werkzeug innerhalb der Anwendung.