This is an automated email from the ASF dual-hosted git repository.

voidmatcha pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/zeppelin.git


The following commit(s) were added to refs/heads/master by this push:
     new 3faf553a53 [ZEPPELIN-6533] Apply the notebook search term after the 
paragraph views exist
3faf553a53 is described below

commit 3faf553a532df600f4d9b7544e833b3390d1c51c
Author: κΉ€μ˜ˆλ‚˜ <[email protected]>
AuthorDate: Fri Aug 7 00:22:23 2026 +0900

    [ZEPPELIN-6533] Apply the notebook search term after the paragraph views 
exist
    
    ### What is this PR for?
    The notebook component reads the `term` query param in `ngOnInit`, which 
runs before `<at>ViewChildren` resolves and before the note arrives over the 
WebSocket. The initial term was applied to a paragraph query list that did not 
exist yet, and nothing re-applied it once the paragraphs rendered, so opening a 
note through a `?term=...` deep link (for example clicking a notebook search 
result) never highlighted the matching text.
    
    This keeps the term on the notebook component and re-applies it in 
`ngAfterViewInit` and whenever the paragraph query list changes. The code 
editor keeps the term as well, because Monaco loads asynchronously and would 
otherwise ignore a term that arrived before the editor was ready.
    
    The guard suggested in the issue 
(`listOfNotebookParagraphComponent?.forEach(...)`) is already on master, so 
`onParagraphSearch` does not throw today. The access stays guarded here.
    
    ### What type of PR is it?
    Bug Fix
    
    ### Todos
    * [x] - Apply the search term once the paragraph views exist
    * [x] - Apply the search term once the Monaco editor is ready
    * [x] - Add an e2e regression test for the `term` deep link
    
    ### What is the Jira issue?
    * https://issues.apache.org/jira/browse/ZEPPELIN-6533
    
    ### How should this be tested?
    * Automated: `e2e/tests/notebook/search/editor-search.spec.ts` gains 
"highlights the term carried by a deep link when the notebook opens". Run it 
with `npm run e2e:fast -- tests/notebook/search/editor-search.spec.ts` in 
`zeppelin-web-angular`. The new test fails on master (0 highlights) and passes 
with this change; the rest of the spec and the notebook keyboard spec stay 
green.
    * Manual: create a note, put `alpha target beta target gamma target` in a 
paragraph, then open `/#/notebook/<noteId>?term=target` coming from another 
page. Every occurrence of `target` is highlighted. The same applies when 
clicking a result on the notebook search page, which navigates with `paragraph` 
and `term` query params.
    
    ### Screenshots (if appropriate)
    Before: nothing is highlighted when the note opens through the deep link.
    After: the three `target` occurrences are highlighted.
    
    ### Questions:
    * Does the license files need to update? No
    * Is there breaking changes for older versions? No
    * Does this needs documentation? No
    
    Closes #5378 from kimyenac/ZEPPELIN-6533.
    
    Signed-off-by: YONGJAE LEE <[email protected]>
---
 .../e2e/models/editor-search-page.ts               | 24 ++++++++++++++
 .../tests/notebook/search/editor-search.spec.ts    | 37 ++++++++++++++++++++++
 zeppelin-web-angular/e2e/utils.ts                  | 30 ++++++++++++++++++
 .../pages/workspace/notebook/notebook.component.ts | 21 ++++++++++--
 .../paragraph/code-editor/code-editor.component.ts |  4 +++
 .../notebook/paragraph/paragraph.component.ts      | 14 ++++++--
 6 files changed, 126 insertions(+), 4 deletions(-)

diff --git a/zeppelin-web-angular/e2e/models/editor-search-page.ts 
b/zeppelin-web-angular/e2e/models/editor-search-page.ts
index d47ba32fc4..a7fbc2d60c 100644
--- a/zeppelin-web-angular/e2e/models/editor-search-page.ts
+++ b/zeppelin-web-angular/e2e/models/editor-search-page.ts
@@ -22,6 +22,8 @@ export class EditorSearchPage extends BasePage {
   readonly replaceInput: Locator;
   readonly matchesCount: Locator;
   readonly matchHighlights: Locator;
+  readonly termHighlights: Locator;
+  readonly showHideCodeButton: Locator;
   readonly nextMatchButton: Locator;
   readonly previousMatchButton: Locator;
   readonly toggleReplaceButton: Locator;
@@ -42,6 +44,11 @@ export class EditorSearchPage extends BasePage {
     this.matchesCount = this.findWidget.locator('.matchesCount').first();
     // Monaco decorates every match with .findMatch and the active one with 
.currentFindMatch.
     this.matchHighlights = this.editor.locator('.findMatch, 
.currentFindMatch');
+    // The `term` query param highlights through Zeppelin's own decoration 
class, not Monaco's find widget.
+    this.termHighlights = this.editor.locator('.editor-search-highlight');
+    this.showHideCodeButton = page
+      .locator('zeppelin-notebook-paragraph-control 
a[nzTooltipTitle="Show/hide the code"]')
+      .first();
     this.nextMatchButton = this.findWidget.locator('.button.next, 
[title^="Next Match"]').first();
     this.previousMatchButton = this.findWidget.locator('.button.previous, 
[title^="Previous Match"]').first();
     this.toggleReplaceButton = this.findWidget.locator('.button.toggle, 
[title^="Toggle Replace"]').first();
@@ -54,6 +61,23 @@ export class EditorSearchPage extends BasePage {
     await expect(this.editor).toBeVisible({ timeout: 15000 });
   }
 
+  async openNotebookWithSearchTerm(noteId: string, term: string): 
Promise<void> {
+    await this.navigateToNotebookWithSearchTerm(noteId, term);
+    await expect(this.editor).toBeVisible({ timeout: 15000 });
+  }
+
+  // Separate from openNotebookWithSearchTerm: a paragraph whose editor starts 
hidden renders no
+  // Monaco instance, so the caller cannot wait for the editor before acting.
+  async navigateToNotebookWithSearchTerm(noteId: string, term: string): 
Promise<void> {
+    await 
this.page.goto(`/#/notebook/${noteId}?term=${encodeURIComponent(term)}`);
+    await waitForZeppelinReady(this.page);
+  }
+
+  async showCode(): Promise<void> {
+    await this.showHideCodeButton.click();
+    await expect(this.editor).toBeVisible({ timeout: 15000 });
+  }
+
   async setEditorContent(content: string): Promise<void> {
     await this.editor.click();
     // Key off the browser, not the host: Monaco follows the browser UA's 
keymap, and
diff --git 
a/zeppelin-web-angular/e2e/tests/notebook/search/editor-search.spec.ts 
b/zeppelin-web-angular/e2e/tests/notebook/search/editor-search.spec.ts
index a7420914f1..3960559c1f 100644
--- a/zeppelin-web-angular/e2e/tests/notebook/search/editor-search.spec.ts
+++ b/zeppelin-web-angular/e2e/tests/notebook/search/editor-search.spec.ts
@@ -17,6 +17,8 @@ import {
   createTestNotebook,
   PAGES,
   performLoginIfRequired,
+  setParagraphEditorHidden,
+  setParagraphText,
   skipWhenAuthenticationIsStillRequired,
   waitForNotebookLinks,
   waitForZeppelinReady
@@ -77,6 +79,41 @@ test.describe('Notebook editor search', () => {
     await expect(editorSearchPage.matchHighlights).toHaveCount(3);
   });
 
+  test('highlights the term carried by a deep link when the notebook opens', 
async ({ page }) => {
+    const { noteId, paragraphId } = await createTestNotebook(page);
+
+    await test.step('Given a paragraph containing the term three times', async 
() => {
+      await setParagraphText(page, noteId, paragraphId, 'alpha target beta 
target gamma target');
+    });
+
+    await test.step('When the notebook is opened with the term in the query 
string', async () => {
+      await editorSearchPage.openNotebookWithSearchTerm(noteId, 'target');
+    });
+
+    await test.step('Then every occurrence is highlighted', async () => {
+      await expect(editorSearchPage.termHighlights).toHaveCount(3);
+    });
+  });
+
+  test('highlights the term carried by a deep link when a hidden editor is 
shown', async ({ page }) => {
+    const { noteId, paragraphId } = await createTestNotebook(page);
+
+    await test.step('Given a paragraph whose editor is hidden and contains the 
term three times', async () => {
+      await setParagraphText(page, noteId, paragraphId, 'alpha target beta 
target gamma target');
+      await setParagraphEditorHidden(page, noteId, paragraphId, true);
+    });
+
+    await test.step('When the notebook is opened with the term and the code is 
shown again', async () => {
+      await editorSearchPage.navigateToNotebookWithSearchTerm(noteId, 
'target');
+      await expect(editorSearchPage.editor).toHaveCount(0);
+      await editorSearchPage.showCode();
+    });
+
+    await test.step('Then every occurrence is highlighted', async () => {
+      await expect(editorSearchPage.termHighlights).toHaveCount(3);
+    });
+  });
+
   test('replaces all matches in the editor search widget', async ({ page }) => 
{
     const { noteId } = await createTestNotebook(page);
 
diff --git a/zeppelin-web-angular/e2e/utils.ts 
b/zeppelin-web-angular/e2e/utils.ts
index cfc3c110e1..d4500d3f6e 100644
--- a/zeppelin-web-angular/e2e/utils.ts
+++ b/zeppelin-web-angular/e2e/utils.ts
@@ -482,6 +482,36 @@ const createNotebookViaRest = async (
   return { noteId, paragraphId };
 };
 
+export const setParagraphText = async (
+  page: Page,
+  noteId: string,
+  paragraphId: string,
+  text: string
+): Promise<void> => {
+  const response = await 
page.request.put(`/api/notebook/${noteId}/paragraph/${paragraphId}`, {
+    data: { text },
+    failOnStatusCode: false
+  });
+  if (!response.ok()) {
+    throw new Error(`Update paragraph REST request failed: 
${response.status()} ${await response.text()}`);
+  }
+};
+
+export const setParagraphEditorHidden = async (
+  page: Page,
+  noteId: string,
+  paragraphId: string,
+  editorHide: boolean
+): Promise<void> => {
+  const response = await 
page.request.put(`/api/notebook/${noteId}/paragraph/${paragraphId}/config`, {
+    data: { editorHide },
+    failOnStatusCode: false
+  });
+  if (!response.ok()) {
+    throw new Error(`Update paragraph config REST request failed: 
${response.status()} ${await response.text()}`);
+  }
+};
+
 interface CreateTestNotebookWithNameOptions {
   folderPath?: string | null;
   namePrefix?: string;
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 408ca1af28..552e0f8c8d 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
@@ -11,6 +11,7 @@
  */
 
 import {
+  AfterViewInit,
   ChangeDetectionStrategy,
   ChangeDetectorRef,
   Component,
@@ -57,9 +58,10 @@ import { NotebookParagraphComponent } from 
'./paragraph/paragraph.component';
   changeDetection: ChangeDetectionStrategy.OnPush,
   standalone: false
 })
-export class NotebookComponent extends MessageListenersManager implements 
OnInit, OnDestroy {
+export class NotebookComponent extends MessageListenersManager implements 
OnInit, AfterViewInit, OnDestroy {
   @ViewChildren(NotebookParagraphComponent) listOfNotebookParagraphComponent!: 
QueryList<NotebookParagraphComponent>;
   private destroy$ = new Subject<void>();
+  private searchTerm = '';
   note?: Exclude<Note['note'], undefined>;
   permissions?: Permissions;
   selectId: string | null = null;
@@ -272,7 +274,8 @@ export class NotebookComponent extends 
MessageListenersManager implements OnInit
   }
 
   onParagraphSearch(term: string) {
-    this.listOfNotebookParagraphComponent?.forEach(comp => 
comp.highlightMatches(term || ''));
+    this.searchTerm = term || '';
+    this.highlightSearchTerm();
   }
 
   saveParagraph(id: string) {
@@ -485,6 +488,13 @@ export class NotebookComponent extends 
MessageListenersManager implements OnInit
       });
   }
 
+  ngAfterViewInit(): void {
+    this.highlightSearchTerm();
+    
this.listOfNotebookParagraphComponent.changes.pipe(takeUntil(this.destroy$)).subscribe(()
 => {
+      this.highlightSearchTerm();
+    });
+  }
+
   removeParagraphFromNgZ(): void {
     if (this.note && Array.isArray(this.note.paragraphs)) {
       this.note.paragraphs.forEach(p => {
@@ -501,4 +511,11 @@ export class NotebookComponent extends 
MessageListenersManager implements OnInit
     this.destroy$.complete();
     this.titleService.setTitle('Zeppelin');
   }
+
+  // The term can arrive before the paragraphs exist: the query param 
subscription emits during
+  // ngOnInit, and the paragraphs themselves are only rendered once the note 
arrives over the
+  // WebSocket. Keep the term and (re)apply it whenever the paragraph views 
change.
+  private highlightSearchTerm(): void {
+    this.listOfNotebookParagraphComponent?.forEach(comp => 
comp.highlightMatches(this.searchTerm));
+  }
 }
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 093a34e11c..ccb1a1669b 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
@@ -66,6 +66,7 @@ export class NotebookParagraphCodeEditorComponent
   private editor?: IStandaloneCodeEditor;
   private monacoDisposables: IDisposable[] = [];
   private highlightDecorations: DecorationIdentifier[] = [];
+  private searchTerm = '';
   height = 18;
   interpreterName?: string;
 
@@ -217,6 +218,8 @@ export class NotebookParagraphCodeEditorComponent
     this.initEditorFocus();
     this.initCompletionService(this.editor);
     this.setEditorValue(this.editor);
+    // A term requested before Monaco finished loading was only stored, not 
applied yet.
+    this.highlightMatches(this.searchTerm);
     setTimeout(() => {
       this.autoAdjustEditorHeight();
     });
@@ -356,6 +359,7 @@ export class NotebookParagraphCodeEditorComponent
   }
 
   highlightMatches(term: string) {
+    this.searchTerm = term;
     if (!this.editor || !term) {
       // Remove previous highlights if term is empty
       this.highlightDecorations = 
this.editor?.deltaDecorations(this.highlightDecorations, []) || [];
diff --git 
a/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/paragraph.component.ts
 
b/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/paragraph.component.ts
index 8f72c2bbf3..186b4c595c 100644
--- 
a/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/paragraph.component.ts
+++ 
b/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/paragraph.component.ts
@@ -84,8 +84,6 @@ export class NotebookParagraphComponent
   implements OnInit, OnChanges, OnDestroy, AfterViewInit, 
AngularKeyboardEventHandler
 {
   @HostBinding('attr.tabindex') tabindex = '-1';
-  @ViewChild(NotebookParagraphCodeEditorComponent, { static: false })
-  notebookParagraphCodeEditorComponent?: NotebookParagraphCodeEditorComponent;
   @ViewChildren(NotebookParagraphResultComponent)
   notebookParagraphResultComponents!: 
QueryList<NotebookParagraphResultComponent>;
   @Input() paragraph!: ParagraphItem;
@@ -145,9 +143,11 @@ export class NotebookParagraphComponent
   @Output() readonly openSearchMenu = new EventEmitter();
 
   private destroy$ = new Subject<void>();
+  private searchTerm = '';
 
   private mode: Mode = 'command';
   waitConfirmFromEdit = false;
+  notebookParagraphCodeEditorComponent?: NotebookParagraphCodeEditorComponent;
 
   private keyBinderService: KeyBinder;
 
@@ -170,7 +170,17 @@ export class NotebookParagraphComponent
     }
   }
 
+  // The code editor sits behind an @if on `config.editorHide`, so it can 
mount long after the
+  // search term arrived, and it mounts as a fresh instance that knows nothing 
about the term.
+  // Setter injection replays the retained term the moment the editor becomes 
available.
+  @ViewChild(NotebookParagraphCodeEditorComponent, { static: false })
+  set codeEditorComponent(component: NotebookParagraphCodeEditorComponent | 
undefined) {
+    this.notebookParagraphCodeEditorComponent = component;
+    component?.highlightMatches(this.searchTerm);
+  }
+
   highlightMatches(searchText: string) {
+    this.searchTerm = searchText;
     this.notebookParagraphCodeEditorComponent?.highlightMatches(searchText);
   }
 

Reply via email to