From f27a7781c447f381b483c4f2ba06a7ccc6c51ab6 Mon Sep 17 00:00:00 2001 From: YONGJAE LEE Date: Wed, 3 Jun 2026 12:03:32 +0900 Subject: [PATCH 1/3] Focus paragraph editor on clone/insert in New UI --- .../pages/workspace/notebook/notebook.component.ts | 12 ++++++++++++ .../paragraph/code-editor/code-editor.component.ts | 11 ++++++++--- .../src/app/services/message.service.ts | 6 ++++++ 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.component.ts b/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.component.ts index 6905a5fc4e5..a70201766d2 100644 --- a/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.component.ts +++ b/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.component.ts @@ -157,6 +157,18 @@ export class NotebookComponent extends MessageListenersManager implements OnInit definedNote.paragraphs[paragraphIndex].focus = true; this.cdr.markForCheck(); + + // Focus the editor only for a clone/insert initiated by this client (not auto-append on run or remote inserts). + // Defer a tick so the new paragraph's editor child exists, since `focus = true` alone misses it. + if (this.messageService.localAddFocusPending) { + this.messageService.localAddFocusPending = false; + const addedId = data.paragraph.id; + setTimeout(() => { + const added = this.listOfNotebookParagraphComponent?.find(e => e.paragraph.id === addedId); + added?.focusEditor(); + added?.notebookParagraphCodeEditorComponent?.setRestorePosition(); + }); + } } @MessageListener(OP.SAVE_NOTE_FORMS) diff --git a/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/code-editor/code-editor.component.ts b/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/code-editor/code-editor.component.ts index c212de77cf7..6ff4c460849 100644 --- a/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/code-editor/code-editor.component.ts +++ b/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/code-editor/code-editor.component.ts @@ -94,19 +94,24 @@ export class NotebookParagraphCodeEditorComponent this.position = e.position; }); }), - editor.onDidChangeModelContent(() => { + editor.onDidChangeModelContent(e => { this.ngZone.run(() => { const model = editor.getModel(); if (!model) { throw new Error('Model content changed but model not found.'); } this.text = model.getValue(); - this.textChanged.emit(this.text); - this.setParagraphMode(true); this.autoAdjustEditorHeight(); setTimeout(() => { this.autoAdjustEditorHeight(); }); + // A flush is a programmatic setValue (editor init, remote content update, patch), not a user edit. + // Such changes must not mark the paragraph dirty. + if (e.isFlush) { + return; + } + this.textChanged.emit(this.text); + this.setParagraphMode(true); }); }) ); diff --git a/zeppelin-web-angular/src/app/services/message.service.ts b/zeppelin-web-angular/src/app/services/message.service.ts index 8949e5e1c79..4cf67c33817 100644 --- a/zeppelin-web-angular/src/app/services/message.service.ts +++ b/zeppelin-web-angular/src/app/services/message.service.ts @@ -38,6 +38,10 @@ import { TicketService } from './ticket.service'; providedIn: 'root' }) export class MessageService extends Message implements OnDestroy { + // Set by a local clone/insert so the PARAGRAPH_ADDED handler focuses the new + // paragraph's editor — not on auto-append or other clients' inserts. + localAddFocusPending = false; + constructor( private baseUrlService: BaseUrlService, private ticketService: TicketService, @@ -167,6 +171,7 @@ export class MessageService extends Message implements OnDestroy { } insertParagraph(newIndex: number): void { + this.localAddFocusPending = true; super.insertParagraph(newIndex); } @@ -177,6 +182,7 @@ export class MessageService extends Message implements OnDestroy { paragraphConfig: ParagraphConfig, paragraphParams: ParagraphParams ): void { + this.localAddFocusPending = true; super.copyParagraph(newIndex, paragraphTitle, paragraphData, paragraphConfig, paragraphParams); } From 9930acd53ed2f1955fde5930013776001c8f0604 Mon Sep 17 00:00:00 2001 From: YONGJAE LEE Date: Sun, 7 Jun 2026 23:34:22 +0900 Subject: [PATCH 2/3] Focus only paragraphs created by the requesting client --- .../zeppelin/socket/NotebookServer.java | 13 +++--- .../interfaces/message-notebook.interface.ts | 1 + .../workspace/notebook/notebook.component.ts | 3 +- .../src/app/services/message.service.ts | 45 +++++++++++++++---- 4 files changed, 46 insertions(+), 16 deletions(-) diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java b/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java index 20343d8a0a5..090272ce5d9 100644 --- a/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java +++ b/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java @@ -692,16 +692,17 @@ private void broadcastParagraphs(Map userParagraphMap, Paragr inlineBroadcastParagraphs(userParagraphMap, msgId); } - private void inlineBroadcastNewParagraph(Note note, Paragraph para) { + private void inlineBroadcastNewParagraph(Note note, Paragraph para, String msgId) { LOGGER.info("Broadcasting paragraph on run call instead of note."); int paraIndex = note.getParagraphs().indexOf(para); - Message message = new Message(OP.PARAGRAPH_ADDED).put("paragraph", para).put("index", paraIndex); + Message message = + new Message(OP.PARAGRAPH_ADDED).withMsgId(msgId).put("paragraph", para).put("index", paraIndex); connectionManager.broadcast(note.getId(), message); } - private void broadcastNewParagraph(Note note, Paragraph para) { - inlineBroadcastNewParagraph(note, para); + private void broadcastNewParagraph(Note note, Paragraph para, String msgId) { + inlineBroadcastNewParagraph(note, para, msgId); } private void inlineBroadcastNoteList() { @@ -1451,7 +1452,7 @@ private String insertParagraph(NotebookSocket conn, @Override public void onSuccess(Paragraph p, ServiceContext context) throws IOException { super.onSuccess(p, context); - broadcastNewParagraph(p.getNote(), p); + broadcastNewParagraph(p.getNote(), p, fromMessage.msgId); } }); @@ -1555,7 +1556,7 @@ public void onSuccess(Paragraph p, ServiceContext context) StringUtils.isEmpty(p.getScriptText())) && isTheLastParagraph) { Paragraph newPara = p.getNote().addNewParagraph(p.getAuthenticationInfo()); - broadcastNewParagraph(p.getNote(), newPara); + broadcastNewParagraph(p.getNote(), newPara, fromMessage.msgId); } } }); diff --git a/zeppelin-web-angular/projects/zeppelin-sdk/src/interfaces/message-notebook.interface.ts b/zeppelin-web-angular/projects/zeppelin-sdk/src/interfaces/message-notebook.interface.ts index 986aed0b910..665e8dfd71f 100644 --- a/zeppelin-web-angular/projects/zeppelin-sdk/src/interfaces/message-notebook.interface.ts +++ b/zeppelin-web-angular/projects/zeppelin-sdk/src/interfaces/message-notebook.interface.ts @@ -167,6 +167,7 @@ export interface ImportNoteReceived { export interface ParagraphAdded { index: number; + msgId?: string; paragraph: ParagraphItem; } diff --git a/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.component.ts b/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.component.ts index a70201766d2..1cb8d2b288b 100644 --- a/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.component.ts +++ b/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.component.ts @@ -160,8 +160,7 @@ export class NotebookComponent extends MessageListenersManager implements OnInit // Focus the editor only for a clone/insert initiated by this client (not auto-append on run or remote inserts). // Defer a tick so the new paragraph's editor child exists, since `focus = true` alone misses it. - if (this.messageService.localAddFocusPending) { - this.messageService.localAddFocusPending = false; + if (this.messageService.consumeLocalAddFocusMsgId(data.msgId)) { const addedId = data.paragraph.id; setTimeout(() => { const added = this.listOfNotebookParagraphComponent?.find(e => e.paragraph.id === addedId); diff --git a/zeppelin-web-angular/src/app/services/message.service.ts b/zeppelin-web-angular/src/app/services/message.service.ts index 4cf67c33817..4b06fdd0a1e 100644 --- a/zeppelin-web-angular/src/app/services/message.service.ts +++ b/zeppelin-web-angular/src/app/services/message.service.ts @@ -12,6 +12,7 @@ import { Inject, Injectable, OnDestroy, Optional } from '@angular/core'; import { Observable } from 'rxjs'; +import { take } from 'rxjs/operators'; import { MessageInterceptor, MESSAGE_INTERCEPTOR } from '@zeppelin/interfaces'; import { @@ -22,6 +23,7 @@ import { MessageSendDataTypeMap, Note, NoteConfig, + OP, ParagraphConfig, ParagraphParams, PersonalizedMode, @@ -38,9 +40,7 @@ import { TicketService } from './ticket.service'; providedIn: 'root' }) export class MessageService extends Message implements OnDestroy { - // Set by a local clone/insert so the PARAGRAPH_ADDED handler focuses the new - // paragraph's editor — not on auto-append or other clients' inserts. - localAddFocusPending = false; + private readonly localAddFocusMsgIds = new Set(); constructor( private baseUrlService: BaseUrlService, @@ -51,7 +51,11 @@ export class MessageService extends Message implements OnDestroy { } interceptReceived(data: WebSocketMessage): WebSocketMessage { - return this.messageInterceptor ? this.messageInterceptor.received(data) : super.interceptReceived(data); + const received = this.messageInterceptor ? this.messageInterceptor.received(data) : super.interceptReceived(data); + if (received.op === OP.PARAGRAPH_ADDED && received.data && received.msgId) { + (received.data as MessageReceiveDataTypeMap[OP.PARAGRAPH_ADDED]).msgId = received.msgId; + } + return received; } bootstrap(): void { @@ -82,6 +86,31 @@ export class MessageService extends Message implements OnDestroy { return super.receive(op); } + consumeLocalAddFocusMsgId(msgId: string | undefined): boolean { + if (!msgId) { + return false; + } + return this.localAddFocusMsgIds.delete(msgId); + } + + private captureLocalAddFocusMsgId(sendMessage: () => void): void { + let msgId: string | undefined; + const subscription = super + .sent() + .pipe(take(1)) + .subscribe(message => { + msgId = message.msgId; + }); + try { + sendMessage(); + } finally { + subscription.unsubscribe(); + } + if (msgId) { + this.localAddFocusMsgIds.add(msgId); + } + } + opened(): Observable { return super.opened(); } @@ -171,8 +200,7 @@ export class MessageService extends Message implements OnDestroy { } insertParagraph(newIndex: number): void { - this.localAddFocusPending = true; - super.insertParagraph(newIndex); + this.captureLocalAddFocusMsgId(() => super.insertParagraph(newIndex)); } copyParagraph( @@ -182,8 +210,9 @@ export class MessageService extends Message implements OnDestroy { paragraphConfig: ParagraphConfig, paragraphParams: ParagraphParams ): void { - this.localAddFocusPending = true; - super.copyParagraph(newIndex, paragraphTitle, paragraphData, paragraphConfig, paragraphParams); + this.captureLocalAddFocusMsgId(() => + super.copyParagraph(newIndex, paragraphTitle, paragraphData, paragraphConfig, paragraphParams) + ); } angularObjectUpdate( From 0f338a8fbde6a3f2806c8a1aeb57d4ca9db72178 Mon Sep 17 00:00:00 2001 From: YONGJAE LEE Date: Mon, 8 Jun 2026 12:54:50 +0900 Subject: [PATCH 3/3] Address review feedback --- .../paragraph/code-editor/code-editor.component.ts | 2 +- .../src/app/services/message.service.ts | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/code-editor/code-editor.component.ts b/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/code-editor/code-editor.component.ts index 6ff4c460849..093a34e11cc 100644 --- a/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/code-editor/code-editor.component.ts +++ b/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/code-editor/code-editor.component.ts @@ -105,13 +105,13 @@ export class NotebookParagraphCodeEditorComponent setTimeout(() => { this.autoAdjustEditorHeight(); }); + this.setParagraphMode(true); // A flush is a programmatic setValue (editor init, remote content update, patch), not a user edit. // Such changes must not mark the paragraph dirty. if (e.isFlush) { return; } this.textChanged.emit(this.text); - this.setParagraphMode(true); }); }) ); diff --git a/zeppelin-web-angular/src/app/services/message.service.ts b/zeppelin-web-angular/src/app/services/message.service.ts index 4b06fdd0a1e..9b86a7d42d6 100644 --- a/zeppelin-web-angular/src/app/services/message.service.ts +++ b/zeppelin-web-angular/src/app/services/message.service.ts @@ -94,20 +94,19 @@ export class MessageService extends Message implements OnDestroy { } private captureLocalAddFocusMsgId(sendMessage: () => void): void { - let msgId: string | undefined; const subscription = super .sent() .pipe(take(1)) .subscribe(message => { - msgId = message.msgId; + if (message.msgId) { + this.localAddFocusMsgIds.add(message.msgId); + } }); try { sendMessage(); - } finally { + } catch (error) { subscription.unsubscribe(); - } - if (msgId) { - this.localAddFocusMsgIds.add(msgId); + throw error; } }