Skip to content

Conversation

dididy
Copy link
Contributor

@dididy dididy commented Aug 24, 2025

What is this PR for?

Description:
Clicking the paragraph control’s Clone paragraph button that trigger COPY_PARAGRAPH. Upon receiving PARAGRAPH_ADDED, the Classic UI includes data.paragraph.text as expected, but the New UI receives an empty data.paragraph.text. Additionally, after cloning, the cursor is not positioned in the cloned paragraph in the New UI.

Expected:

  • Cloned paragraph contains the original text.
  • Cursor focuses/positions inside the cloned paragraph.

Actual (New UI):

  • data.paragraph.text is empty on PARAGRAPH_ADDED.
  • Cursor not focused / not moved to the cloned paragraph.

[Appropriate action - Classic UI]

2025-08-24.11.40.28.mov

[AS-IS]

2025-08-24.11.49.07.mov

[TO-BE]

2025-08-25.12.26.32.mov

What type of PR is it?

Bug Fix

Todos

What is the Jira issue?

How should this be tested?

Screenshots (if appropriate)

Questions:

  • Does the license files need to update? N
  • Is there breaking changes for older versions? N
  • Does this needs documentation? N

Copy link
Contributor

@tbonelee tbonelee left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for taking a look! From my side, data.paragraph.text appears on PARAGRAPH_ADDED in both UIs, but I might be mistaken. If there’s a case where it’s empty, I’d love the exact repro steps.
One possibility is that the New UI is skipping a valid PARAGRAPH as if it were short-circuited when another request bumps lastMsgIdSeq before the final reply—PARAGRAPH message (see message.ts, L187). I noticed EDITOR_SETTING messages being sent from the client and suspect they might be the trigger. If the only intended short-circuit is RUN_PARAGRAPH → synthetic PARAGRAPH_STATUS, would it make sense to remove the second filter() at L187 rather than adding a special-case branch at the NotebookServer? Happy to adjust based on your feedback.

@dididy
Copy link
Contributor Author

dididy commented Aug 26, 2025

I really appreciate about your review. 👍 👍

As you mentioned, the issue was caused by EDITOR_SETTING being triggered before PARAGRAPH, which led to short-circuit processing. However, modifying the condition in message.ts, L187 didn’t seem optimal from a performance standpoint.(I'm not entirely sure if that's really the case)

So I adjusted the timing in the relevant section of the app/pages/workspace/notebook/paragraph/code-editor/code-editor.component.ts instead. Because the getEditorSetting method that triggers EDITOR_SETTING is only executed when the parameter of setParagraphMode is true, and the only place it is used in this form is inside onDidChangeModelContent. Therefore, I modified it so that when setParagraphMode(true) is called in that area, the timing is adjusted using setTimeout.

After reverted NotebookServer code, everything worked as intended. Thanks for pointing out a better way to improve it.

Copy link
Contributor

@tbonelee tbonelee left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice fix! Since the setTimeout(..., 200) is a heuristic to dodge a race, could we add a short comment explaining (1) why it's needed and (2) a TODO o how to make this deterministic or order-independent?

@dididy
Copy link
Contributor Author

dididy commented Aug 27, 2025

Nice fix! Since the setTimeout(..., 200) is a heuristic to dodge a race, could we add a short comment explaining (1) why it's needed and (2) a TODO o how to make this deterministic or order-independent?

ab07359

Ensuring the execution order of EDITOR_SETTING seems necessary because it can otherwise cause a bug that passes through the short-circuit condition. Although this issue does not reproduce for everyone, controlling the execution timing appears to be required.

image

EDITOR_SETTING is executed when the first paragraph component is rendered. From the Classic UI console logs above, we can see that it runs after INTERPRETER_BINDINGS when first entering the note. Therefore, I added a variable editorSettingTriggerAllowed with an initial value of false in /app/pages/workspace/notebook/paragraph/code-editor/code-editor.component.ts. The getEditorSetting method now only executes if this variable is true.

Then, in /app/pages/workspace/notebook/notebook.component.ts, I added code that sets editorSettingTriggerAllowed to true for all paragraphs and calls getEditorSetting when receiving INTERPRETER_BINDINGS.

스크린샷 2025-08-27 오후 11 10 40 <= Classic UI
스크린샷 2025-08-27 오후 11 13 32 <= New UI unrevised result 스크린샷 2025-08-27 오후 11 16 59 <= New UI revised result

The part where I noticed the issue was at the moment of receiving PARAGRAPH_ADDED op. When a new paragraph component is added by PARAGRAPH_ADDED, getEditorSetting is immediately executed. This triggers EDITOR_SETTING, but since execution is now blocked by editorSettingTriggerAllowed, as I fixed, it does not run at that point. Instead, editorSettingTriggerAllowed is set to true and getEditorSetting is executed when the PARAGRAPH op(/app/core/paragraph-base/paragraph-base.ts) is received.

Copy link
Contributor

@tbonelee tbonelee left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the thorough update — I’ve left some questions and requested changes.

@@ -54,6 +55,7 @@ export abstract class ParagraphBase extends MessageListenersManager {

// Initialized by `ViewChildren` in the class which extends ParagraphBase
notebookParagraphResultComponents!: QueryList<NotebookParagraphResultComponent>;
notebookParagraphCodeEditorComponent!: NotebookParagraphCodeEditorComponent;
Copy link
Contributor

@tbonelee tbonelee Aug 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this non-null assertion should be removed and changed to optional. I found it to be undefined when paragraph.config.editorHide === true in the %md editor case. Maybe you could check some errors after running %md interpreter.
(zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/paragraph.component.html)

    <zeppelin-notebook-paragraph-code-editor
      *ngIf="!paragraph.config.editorHide && !viewOnly"
...

I mistakenly added non-null assertion for notebookParagraphCodeEditorComponent in NotebookParagraphComponent in a previous commit, but that was incorrect. Sorry for the confusion.

@@ -109,6 +109,10 @@ export class NotebookComponent extends MessageListenersManager implements OnInit

@MessageListener(OP.INTERPRETER_BINDINGS)
loadInterpreterBindings(data: MessageReceiveDataTypeMap[OP.INTERPRETER_BINDINGS]) {
this.listOfNotebookParagraphComponent.forEach(item => {
item.notebookParagraphCodeEditorComponent.editorSettingTriggerAllowed = true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should handle some case where notebookParagraphCodeEditorComponent is undefined.
Also, should we trigger editor settings for all paragraphs here? Would calling this.setParagraphMode(true) in editor.onDidChangeModelContent() be sufficient to get editor settings only to the paragraph whose interpreter has changed?

Copy link
Contributor Author

@dididy dididy Sep 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In testing, the interpreter setting works correctly when this.setParagraphMode(true) is executed inside editor.onDidChangeModelContent().

Since this runs before the code that executes when receiving INTERPRETER_BINDINGS above (editor.onDidChangeModelContent()), the code that runs after editorSettingTriggerAllowed = true was modified to directly call getEditorSetting() instead of using setParagraphMode(true) when receive PARAGRAPH or INTERPRETER_BINDINGS.

c90cd09

Because if the interpreter is already set inside this.setParagraphMode(true), getEditorSetting() will not be executed due to the condition, and since setParagraphMode(true) has already run, there is no need to execute it again.

@@ -142,6 +144,8 @@ export abstract class ParagraphBase extends MessageListenersManager {
}
this.cdr.markForCheck();
});
this.notebookParagraphCodeEditorComponent.editorSettingTriggerAllowed = true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should handle the case where this.notebookParagraphCodeEditorComponent is undefined

@dididy
Copy link
Contributor Author

dididy commented Sep 4, 2025

c90cd09

  • Made notebookParagraphCodeEditorComponent optional.

5e35dbf

  • Added a comment for editorSettingTriggerAllowed.

6c993a0

  • There are two related actions: CLONE_PARAGRAPH and INSERT_PARAGRAPH. Each needs to be handled slightly differently.

Behavior

  • CLONE_PARAGRAPH
    1. send > CLONE_PARAGRAPH
    2. receive > PARAGRAPH_ADDED → PARAGRAPH → trigger EDITOR_SETTING
  • INSERT_PARAGRAPH
    1. send > INSERT_PARAGRAPH
    2. receive > trigger EDITOR_SETTING after PARAGRAPH_ADDED

Implementation

  • CLONE_PARAGRAPH was already handled correctly.
  • For INSERT_PARAGRAPH, we added:
    • A variable isTriggeredByInsertParagraph in notebook.component.ts.
    • Set it to true when insertParagraph in paragraph.component.ts is executed.
  • This allows branching based on whether PARAGRAPH_ADDED was triggered by an INSERT_PARAGRAPH or a CLONE_PARAGRAPH.

Reasoning

  • CLONE_PARAGRAPH: EDITOR_SETTING runs after receiving PARAGRAPH.
  • INSERT_PARAGRAPH: EDITOR_SETTING runs when PARAGRAPH_ADDED is received.

@dididy
Copy link
Contributor Author

dididy commented Sep 7, 2025

I rebased due to conflicts caused by merged PRs.

Comment on lines 161 to 171
definedNote.paragraphs[paragraphIndex].focus = true;
const addedParagraph = this.listOfNotebookParagraphComponent.find((_, index) => index === paragraphIndex)
?.notebookParagraphCodeEditorComponent;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think something like this.cdr.detectChanges() should be called, so that the paragraph component is added for the new paragraph in definedNote.paragraph.

Copy link
Contributor Author

@dididy dididy Sep 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

5aab81a c496536

Upon review, components using ChangeDetectionStrategy.OnPush—as you mentioned—do not detect changes in the same reference array unless the reference itself is replaced. Since I am currently modifying the array with splice (which does change the reference in place), Angular does not automatically recognize the change. Therefore, calling detectChanges or markForCheck is necessary.

The reason why Clone Paragraph or Add Paragraph (Insert Paragraph) in the current PR appeared to work correctly is due to Angular’s behavior of updating the view during events triggered within the component. Consequently, when executed via a button click, the focus handling works as expected, but when triggered via a shortcut key, the focus does not update properly.

To address this, after receiving PARAGRAPH_ADDED and performing a splice to update the array, we now explicitly call detectChanges to force change detection, which has been confirmed to work correctly.

1cdff50

Even after handling it this way, the focus event did not work correctly when triggered via shortcut keys. To address this, in paragraph.component.ts, I added a blur event on the current paragraph when executing cloneParagraph and insertParagraph.


I rebased again due to conflicts caused by merging the latest PR.

tbonelee pushed a commit that referenced this pull request Sep 7, 2025
### What is this PR for?
**Description:**
Cursor behavior in the New UI’s paragraph needs to be fixed for several cursor related actions, including double-clicking, running all above/below, adding(clone), and removing paragraphs.

When **cloneParagraph()** is called, it internally calls **addParagraph()**, which has already been tested. The same addParagraph-related code is also applied in #5044. If either PR is merged first, I will rebase accordingly. I have also confirmed that **cloneParagraph()** works correctly through #5044.

Due to timing issues, I used `setTimeout` for **removeParagraph()** and **doubleClickParagraph()**. Since this is in the UI area, it likely won’t have major side effects, but I will look into it further.

**Expected:**
- When **doubleClickParagraph()** is executed, the cursor should move to the end of the paragraph.
- When **runAllAbove()** or **runAllBelow()** is executed, the current cursor position should be remembered, and after execution, focus should return to the previous cursor position.
- When **addParagraph()** is executed, the newly added paragraph should receive focus.
- When **removeParagraph()** is executed, focus should move to the paragraph that takes the deleted paragraph’s place.

**Actual (New UI):**
- When **doubleClickParagraph()** is executed, the cursor moves to the beginning instead of the end.
- After **runAllAbove()** or **runAllBelow()**, focus is lost completely.
- When **addParagraph()** is executed, the new paragraph does not automatically receive focus.
- After **removeParagraph()**, focus may not move to the correct paragraph.

**[Appropriate action - Classic UI]**

https://github.com/user-attachments/assets/fc0066f7-4e03-4e3b-9d5b-2f33df415ba7

Run all above -> Run all below -> Double click .md paragraph  -> Add paragraph -> Delete paragraph

**[AS-IS]**

https://github.com/user-attachments/assets/f699f788-cf29-4c4c-8c47-2ef34d7962f0

Run all above -> Run all below -> Double click .md paragraph  -> Add paragraph -> Delete paragraph

**[TO-BE]**

https://github.com/user-attachments/assets/1206c524-103f-4328-85ee-04408073b628

Run all above -> Run all below -> Double click .md paragraph  -> Add paragraph -> Delete paragraph


### What type of PR is it?
Bug Fix

### Todos

### What is the Jira issue?
* [[ZEPPELIN-6298](https://issues.apache.org/jira/browse/ZEPPELIN-6298)]

### How should this be tested?

### Screenshots (if appropriate)

### Questions:
* Does the license files need to update? N
* Is there breaking changes for older versions? N
* Does this needs documentation? N


Closes #5057 from dididy/fix/ZEPPELIN-6298.

Signed-off-by: ChanHo Lee <[email protected]>
tbonelee pushed a commit that referenced this pull request Sep 7, 2025
### What is this PR for?
**Description:**
Cursor behavior in the New UI’s paragraph needs to be fixed for several cursor related actions, including double-clicking, running all above/below, adding(clone), and removing paragraphs.

When **cloneParagraph()** is called, it internally calls **addParagraph()**, which has already been tested. The same addParagraph-related code is also applied in #5044. If either PR is merged first, I will rebase accordingly. I have also confirmed that **cloneParagraph()** works correctly through #5044.

Due to timing issues, I used `setTimeout` for **removeParagraph()** and **doubleClickParagraph()**. Since this is in the UI area, it likely won’t have major side effects, but I will look into it further.

**Expected:**
- When **doubleClickParagraph()** is executed, the cursor should move to the end of the paragraph.
- When **runAllAbove()** or **runAllBelow()** is executed, the current cursor position should be remembered, and after execution, focus should return to the previous cursor position.
- When **addParagraph()** is executed, the newly added paragraph should receive focus.
- When **removeParagraph()** is executed, focus should move to the paragraph that takes the deleted paragraph’s place.

**Actual (New UI):**
- When **doubleClickParagraph()** is executed, the cursor moves to the beginning instead of the end.
- After **runAllAbove()** or **runAllBelow()**, focus is lost completely.
- When **addParagraph()** is executed, the new paragraph does not automatically receive focus.
- After **removeParagraph()**, focus may not move to the correct paragraph.

**[Appropriate action - Classic UI]**

https://github.com/user-attachments/assets/fc0066f7-4e03-4e3b-9d5b-2f33df415ba7

Run all above -> Run all below -> Double click .md paragraph  -> Add paragraph -> Delete paragraph

**[AS-IS]**

https://github.com/user-attachments/assets/f699f788-cf29-4c4c-8c47-2ef34d7962f0

Run all above -> Run all below -> Double click .md paragraph  -> Add paragraph -> Delete paragraph

**[TO-BE]**

https://github.com/user-attachments/assets/1206c524-103f-4328-85ee-04408073b628

Run all above -> Run all below -> Double click .md paragraph  -> Add paragraph -> Delete paragraph

### What type of PR is it?
Bug Fix

### Todos

### What is the Jira issue?
* [[ZEPPELIN-6298](https://issues.apache.org/jira/browse/ZEPPELIN-6298)]

### How should this be tested?

### Screenshots (if appropriate)

### Questions:
* Does the license files need to update? N
* Is there breaking changes for older versions? N
* Does this needs documentation? N

Closes #5057 from dididy/fix/ZEPPELIN-6298.

Signed-off-by: ChanHo Lee <[email protected]>
(cherry picked from commit 4fbfaec)
Signed-off-by: ChanHo Lee <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants