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 f68ea3d1e7 [ZEPPELIN-6676] Correct notebook runtime info, GUI and 
Angular object value shapes
f68ea3d1e7 is described below

commit f68ea3d1e73d346d071ffe2ac36347c9cd4e85c8
Author: Minho Jang <[email protected]>
AuthorDate: Wed Oct 7 12:20:35 2026 +0900

    [ZEPPELIN-6676] Correct notebook runtime info, GUI and Angular object value 
shapes
    
    ### What is this PR for?
    
    Several SDK declarations do not match the servers notebook payloads. Model 
runtime information as arbitrary property maps, form parameters as JSON 
scalar/array/object values, note forms as typed Input maps, and paragraph 
settings as GUI data separate from editor settings.
    
    Support legacy form labels and preserve structured values through existing 
controls. Checkbox indexes remain inside the control; emitted parameters 
contain the original values. Preserve supplied falsy values and match Select 
values after JSON hydration.
    
    Also correct scoped Note parameter, binding and metadata maps, and allow 
non-string Angular object values through public update/bind helpers.
    
    Server evidence:
    - 
`zeppelin-server/src/main/java/org/apache/zeppelin/notebook/Paragraph.java:79-83,98,668-683`;
 `ParagraphRuntimeInfo.java:31-56`
    - 
`zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/GUI.java:46-64,103-145`;
 `Input.java:48-64`
    - 
`zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/ui/OptionInput.java:33-39`;
 `Select.java:23-29`; `CheckBox.java:26-32`
    - 
`zeppelin-server/src/main/java/org/apache/zeppelin/notebook/Note.java:79-93,986-1022`;
 
`zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/AngularObject.java:44-54`
    - 
`zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java:1388-1437`;
 `ConnectionManager.java:65-69`; 
`zeppelin-server/src/main/java/org/apache/zeppelin/scheduler/QuartzSchedulerService.java:103-124`
    
    ### What type of PR is it?
    
    Bug Fix
    
    ### Todos
    
    - [x] Correct structural/scalar declarations and affected consumers.
    - [x] Add compiler contracts and form-control regression tests.
    
    ### What is the Jira issue?
    
    https://issues.apache.org/jira/browse/ZEPPELIN-6676
    
    Slice 2 of 3: structural and scalar shape corrections. Slice 1 merged in 
#5539. Angular object removal compatibility will follow as a separate PR after 
this slice merges.
    
    ### How should this be tested?
    
    Run from the repository root after installing frontend dependencies:
    
    ```sh
    cd zeppelin-web-angular
    npm run typecheck:sdk-contracts
    npm run test:shell
    npm run check:websocket-contract
    npx ngc -p src/tsconfig.json --noEmit
    ```
    
    ### Questions:
    
    - Does the license files need to update? No.
    - Is there breaking changes for older versions? TypeScript callers must 
adapt incorrect GUI/editor assumptions and narrow unknown values before use. No 
server wire-format change.
    - Does this needs documentation? Examples of corrected shapes are included 
in the compiler contracts.
    
    
    Closes #5544 from miinhho/ZEPPELIN-6676-upstream-value-shapes.
    
    Signed-off-by: YONGJAE LEE <[email protected]>
---
 .../zeppelin-sdk/src/angular-object-values.spec.ts |  53 ++++++++
 .../src/interfaces/message-notebook.interface.ts   |  26 ++--
 .../src/interfaces/message-paragraph.interface.ts  |  35 ++++--
 .../src/interfaces/note-value-shapes.spec.ts       |  46 +++++++
 .../src/interfaces/notebook-value-shapes.spec.ts   |  76 ++++++++++++
 .../projects/zeppelin-sdk/src/message.ts           |   5 +-
 .../src/app/core/paragraph-base/paragraph-base.ts  |  20 +--
 .../dynamic-forms/dynamic-forms.component.html     |  11 +-
 .../dynamic-forms/dynamic-forms.component.spec.ts  |  76 ++++++++++++
 .../dynamic-forms.component.testbed.spec.ts        | 138 +++++++++++++++++++++
 .../share/dynamic-forms/dynamic-forms.component.ts |  66 +++++++---
 .../src/app/services/message.service.ts            |   5 +-
 12 files changed, 491 insertions(+), 66 deletions(-)

diff --git 
a/zeppelin-web-angular/projects/zeppelin-sdk/src/angular-object-values.spec.ts 
b/zeppelin-web-angular/projects/zeppelin-sdk/src/angular-object-values.spec.ts
new file mode 100644
index 0000000000..59ac839563
--- /dev/null
+++ 
b/zeppelin-web-angular/projects/zeppelin-sdk/src/angular-object-values.spec.ts
@@ -0,0 +1,53 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { afterEach, describe, expect, expectTypeOf, it, vi } from 'vitest';
+
+import { OP } from './interfaces/message-operator.interface';
+import { Message } from './message';
+
+afterEach(() => vi.restoreAllMocks());
+
+it('accepts arbitrary values through the public Angular object helpers', () => 
{
+  
expectTypeOf<Parameters<Message['angularObjectUpdate']>[3]>().toEqualTypeOf<unknown>();
+  
expectTypeOf<Parameters<Message['angularObjectClientBind']>[2]>().toEqualTypeOf<unknown>();
+});
+
+describe('Angular object send values', () => {
+  it.each([false, 42, null, ['a', false], { count: 0, enabled: false }])(
+    'preserves %j in update and bind payloads',
+    value => {
+      const message = new Message();
+      const send = vi.spyOn(message, 'send').mockReturnValue('message-id');
+
+      message.angularObjectUpdate('note', 'paragraph', 'value', value, 
'group');
+      message.angularObjectClientBind('note', 'value', value, 'paragraph');
+
+      expect(send).toHaveBeenNthCalledWith(1, OP.ANGULAR_OBJECT_UPDATED, {
+        noteId: 'note',
+        paragraphId: 'paragraph',
+        name: 'value',
+        value,
+        interpreterGroupId: 'group'
+      });
+      expect(send).toHaveBeenNthCalledWith(2, OP.ANGULAR_OBJECT_CLIENT_BIND, {
+        noteId: 'note',
+        name: 'value',
+        value,
+        paragraphId: 'paragraph'
+      });
+      expect(
+        JSON.parse(JSON.stringify(send.mock.calls)).map(([, data]: [OP, { 
value: unknown }]) => data.value)
+      ).toEqual([value, value]);
+    }
+  );
+});
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 e4058a3f12..1e3bea95f0 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
@@ -10,7 +10,7 @@
  * limitations under the License.
  */
 
-import { ImportParagraphItem, ParagraphItem } from 
'./message-paragraph.interface';
+import { DynamicFormParams, DynamicForms, ImportParagraphItem, ParagraphItem } 
from './message-paragraph.interface';
 
 interface ID {
   id: string;
@@ -73,24 +73,24 @@ export interface ImportNote {
 }
 
 export interface NoteAngularObjects {
-  // eslint-disable-next-line  @typescript-eslint/no-explicit-any
-  [key: string]: any;
+  [interpreterGroupId: string]: NoteAngularObject[];
 }
 
-export interface NoteInfo {
-  // eslint-disable-next-line  @typescript-eslint/no-explicit-any
-  [key: string]: any;
+export interface NoteAngularObject {
+  name: string;
+  // Gson omits null object values and scope identifiers.
+  object?: unknown;
+  noteId?: string;
+  paragraphId?: string;
 }
 
-export interface NoteParams {
-  // eslint-disable-next-line  @typescript-eslint/no-explicit-any
-  [key: string]: any;
+export interface NoteInfo {
+  [key: string]: unknown;
 }
 
-export interface NoteForms {
-  // eslint-disable-next-line  @typescript-eslint/no-explicit-any
-  [key: string]: any;
-}
+export type NoteParams = DynamicFormParams;
+
+export type NoteForms = DynamicForms;
 
 export interface RemoveNoteForms {
   noteId: string;
diff --git 
a/zeppelin-web-angular/projects/zeppelin-sdk/src/interfaces/message-paragraph.interface.ts
 
b/zeppelin-web-angular/projects/zeppelin-sdk/src/interfaces/message-paragraph.interface.ts
index 0025b1976a..833726c6de 100644
--- 
a/zeppelin-web-angular/projects/zeppelin-sdk/src/interfaces/message-paragraph.interface.ts
+++ 
b/zeppelin-web-angular/projects/zeppelin-sdk/src/interfaces/message-paragraph.interface.ts
@@ -16,17 +16,23 @@ export enum DynamicFormsType {
   TextBox = 'TextBox',
   Password = 'Password',
   Select = 'Select',
-  CheckBox = 'CheckBox'
+  CheckBox = 'CheckBox',
+  LegacyInput = 'input',
+  LegacySelect = 'select',
+  LegacyCheckBox = 'checkbox'
 }
 
+// JSON representation of Java Object values in GUI and Input payloads.
+export type FormValue = string | number | boolean | null | FormValue[] | { 
[key: string]: FormValue };
+
 export interface DynamicFormsItem {
-  defaultValue: string | string[];
+  defaultValue: FormValue;
   hidden: boolean;
   name: string;
   displayName?: string;
   type: DynamicFormsType;
   argument?: string;
-  options?: Array<{ value: string; displayName?: string }>;
+  options?: Array<{ value: FormValue; displayName?: string }>;
 }
 
 export interface DynamicForms {
@@ -34,7 +40,12 @@ export interface DynamicForms {
 }
 
 export interface DynamicFormParams {
-  [key: string]: string | string[];
+  [key: string]: FormValue;
+}
+
+export interface ParagraphSettings {
+  params: DynamicFormParams;
+  forms: DynamicForms;
 }
 
 export interface ParagraphEditorSetting {
@@ -43,8 +54,6 @@ export interface ParagraphEditorSetting {
   isOutputHidden?: boolean;
   completionKey?: EditorCompletionKey;
   completionSupport?: boolean;
-  params: DynamicFormParams;
-  forms: DynamicForms;
 }
 
 // TODO(hsuanxyz)
@@ -130,10 +139,10 @@ export interface ParasInfo {
 }
 
 export interface RuntimeInfos {
-  jobUrl: RuntimeInfosJobUrl;
+  [propertyName: string]: ParagraphRuntimeInfo;
 }
 
-interface RuntimeInfosJobUrl {
+export interface ParagraphRuntimeInfo {
   propertyName: string;
   label: string;
   tooltip: string;
@@ -142,8 +151,8 @@ interface RuntimeInfosJobUrl {
   interpreterSettingId: string;
 }
 
-interface RuntimeInfosValuesItem {
-  jobUrl: string;
+export interface RuntimeInfosValuesItem {
+  [key: string]: string;
 }
 
 export interface ParagraphItem {
@@ -151,7 +160,7 @@ export interface ParagraphItem {
   user: string;
   dateUpdated: string;
   config: ParagraphConfig;
-  settings: ParagraphEditorSetting;
+  settings: ParagraphSettings;
   results?: ParagraphResults;
   // eslint-disable-next-line  @typescript-eslint/no-explicit-any
   apps: any[];
@@ -227,7 +236,7 @@ export interface AngularObjectUpdated {
   noteId: string;
   paragraphId: string;
   name: string;
-  value: string;
+  value: unknown;
   interpreterGroupId: string;
 }
 
@@ -260,7 +269,7 @@ export interface AngularObjectUpdate {
 export interface AngularObjectClientBind {
   noteId: string;
   name: string;
-  value: string;
+  value: unknown;
   paragraphId: string;
 }
 
diff --git 
a/zeppelin-web-angular/projects/zeppelin-sdk/src/interfaces/note-value-shapes.spec.ts
 
b/zeppelin-web-angular/projects/zeppelin-sdk/src/interfaces/note-value-shapes.spec.ts
new file mode 100644
index 0000000000..f9cc7a8618
--- /dev/null
+++ 
b/zeppelin-web-angular/projects/zeppelin-sdk/src/interfaces/note-value-shapes.spec.ts
@@ -0,0 +1,46 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { assertType, expectTypeOf, it } from 'vitest';
+
+import { Note, NoteAngularObject, NoteAngularObjects, NoteInfo, NoteParams } 
from './message-notebook.interface';
+import { DynamicFormParams } from './message-paragraph.interface';
+
+type NoteValueFields = Pick<NonNullable<Note['note']>, 'noteParams' | 
'angularObjects' | 'info'>;
+
+it('matches the Note parameter, angular-object list and metadata maps', () => {
+  expectTypeOf<NoteParams>().toEqualTypeOf<DynamicFormParams>();
+  
expectTypeOf<NoteAngularObjects[string]>().toEqualTypeOf<NoteAngularObject[]>();
+  expectTypeOf<NoteAngularObject>().toEqualTypeOf<{
+    name: string;
+    object?: unknown;
+    noteId?: string;
+    paragraphId?: string;
+  }>();
+  expectTypeOf<NoteInfo[string]>().toEqualTypeOf<unknown>();
+  assertType<NoteAngularObjects>({ spark: [{ name: 'empty' }] });
+});
+
+it('accepts Note values with structured parameters and omitted object or scope 
fields', () => {
+  assertType<NoteValueFields>({
+    noteParams: { count: 0, enabled: false, selected: ['a', { id: 42 }] },
+    angularObjects: {
+      spark: [
+        { name: 'global', object: { nested: [1, false] } },
+        { name: 'note', object: 42, noteId: 'note' },
+        { name: 'paragraph', object: false, noteId: 'note', paragraphId: 
'paragraph' },
+        { name: 'empty' }
+      ]
+    },
+    info: { isRunning: false, startTime: '2026-01-01T00:00:00Z', custom: { 
nested: [1, true] } }
+  });
+});
diff --git 
a/zeppelin-web-angular/projects/zeppelin-sdk/src/interfaces/notebook-value-shapes.spec.ts
 
b/zeppelin-web-angular/projects/zeppelin-sdk/src/interfaces/notebook-value-shapes.spec.ts
new file mode 100644
index 0000000000..041f571036
--- /dev/null
+++ 
b/zeppelin-web-angular/projects/zeppelin-sdk/src/interfaces/notebook-value-shapes.spec.ts
@@ -0,0 +1,76 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { assertType, expectTypeOf, it } from 'vitest';
+
+import { NoteForms } from './message-notebook.interface';
+import {
+  AngularObjectClientBind,
+  AngularObjectUpdated,
+  DynamicFormParams,
+  DynamicForms,
+  DynamicFormsType,
+  ParagraphEditorSetting,
+  ParagraphItem,
+  ParagraphSettings,
+  RuntimeInfos
+} from './message-paragraph.interface';
+
+it('accepts arbitrary runtime-info properties and string-map entries', () => {
+  assertType<RuntimeInfos>({
+    sparkStages: {
+      propertyName: 'sparkStages',
+      label: 'Stages',
+      tooltip: 'Spark stages',
+      group: 'spark',
+      interpreterSettingId: 'spark',
+      values: [{ stage: '1', state: 'RUNNING' }]
+    }
+  });
+  assertType<RuntimeInfos>({});
+});
+
+it('models GUI separately from interpreter editor settings', () => {
+  expectTypeOf<ParagraphItem['settings']>().toEqualTypeOf<ParagraphSettings>();
+  expectTypeOf<ParagraphEditorSetting>().not.toHaveProperty('params');
+  expectTypeOf<ParagraphEditorSetting>().not.toHaveProperty('forms');
+  assertType<ParagraphSettings>({
+    params: { count: 0, enabled: false, selected: [42, true, { id: 'a' }], 
empty: null },
+    forms: {}
+  });
+  assertType<DynamicFormParams>({ value: { nested: [1, false, null] } });
+});
+
+it('preserves the typed Input map and legacy form labels', () => {
+  expectTypeOf<NoteForms>().toEqualTypeOf<DynamicForms>();
+  assertType<NoteForms>({
+    select: {
+      type: DynamicFormsType.LegacySelect,
+      name: 'select',
+      hidden: false,
+      defaultValue: 42,
+      options: [{ value: 42 }, { value: { id: 'a' } }]
+    },
+    checkbox: {
+      type: DynamicFormsType.LegacyCheckBox,
+      name: 'checkbox',
+      hidden: false,
+      defaultValue: [true, { id: 'a' }]
+    },
+    input: { type: DynamicFormsType.LegacyInput, name: 'input', hidden: false, 
defaultValue: '' }
+  });
+});
+
+it('does not constrain Angular object values to strings', () => {
+  expectTypeOf<AngularObjectUpdated['value']>().toEqualTypeOf<unknown>();
+  expectTypeOf<AngularObjectClientBind['value']>().toEqualTypeOf<unknown>();
+});
diff --git a/zeppelin-web-angular/projects/zeppelin-sdk/src/message.ts 
b/zeppelin-web-angular/projects/zeppelin-sdk/src/message.ts
index 6950877b80..b5a830c915 100644
--- a/zeppelin-web-angular/projects/zeppelin-sdk/src/message.ts
+++ b/zeppelin-web-angular/projects/zeppelin-sdk/src/message.ts
@@ -324,7 +324,7 @@ export class Message {
     noteId: string,
     paragraphId: string,
     name: string,
-    value: string,
+    value: unknown,
     interpreterGroupId: string
   ): void {
     this.send<OP.ANGULAR_OBJECT_UPDATED>(OP.ANGULAR_OBJECT_UPDATED, {
@@ -336,8 +336,7 @@ export class Message {
     });
   }
 
-  // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  angularObjectClientBind(noteId: string, name: string, value: any, 
paragraphId: string): void {
+  angularObjectClientBind(noteId: string, name: string, value: unknown, 
paragraphId: string): void {
     this.send<OP.ANGULAR_OBJECT_CLIENT_BIND>(OP.ANGULAR_OBJECT_CLIENT_BIND, {
       noteId,
       name,
diff --git a/zeppelin-web-angular/src/app/core/paragraph-base/paragraph-base.ts 
b/zeppelin-web-angular/src/app/core/paragraph-base/paragraph-base.ts
index 2724781573..5645b6b743 100644
--- a/zeppelin-web-angular/src/app/core/paragraph-base/paragraph-base.ts
+++ b/zeppelin-web-angular/src/app/core/paragraph-base/paragraph-base.ts
@@ -22,9 +22,10 @@ import {
   ParagraphConfigResult,
   ParagraphConfigResults,
   ParagraphEditorSetting,
-  ParagraphItem,
+  ParagraphSettings,
   ParagraphStates,
   isTerminalParagraphState,
+  ParagraphItem,
   ParagraphIResultsMsgItem,
   WebSocketMessage
 } from '@zeppelin/sdk';
@@ -55,10 +56,7 @@ export abstract class ParagraphBase extends 
MessageListenersManager {
   configs: ParagraphConfigResults = {};
   progress = 0;
   colWidthOption = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
-  editorSetting: ParagraphEditorSetting = {
-    params: {},
-    forms: {}
-  };
+  editorSetting: ParagraphEditorSetting = {};
   private readonly outputState = new ParagraphOutputState();
   private readonly pendingParagraphSaves = new Map<string, { sequence: number; 
originalText?: string }>();
   private paragraphSaveSequence = 0;
@@ -110,10 +108,7 @@ export abstract class ParagraphBase extends 
MessageListenersManager {
   getEditorSetting(data: MessageReceiveDataTypeMap[OP.EDITOR_SETTING]) {
     if (this.paragraph?.id === data.paragraphId) {
       this.paragraph.config.editorSetting = {
-        ...(this.paragraph.config.editorSetting ?? {
-          params: {},
-          forms: {}
-        }),
+        ...this.paragraph.config.editorSetting,
         ...data.editor
       };
       this.cdr.markForCheck();
@@ -404,7 +399,7 @@ export abstract class ParagraphBase extends 
MessageListenersManager {
     }
   }
 
-  initializeDefault(config: ParagraphConfig, settings: ParagraphEditorSetting) 
{
+  initializeDefault(config: ParagraphConfig, settings: ParagraphSettings) {
     const forms = settings.forms;
 
     if (!config.colWidth) {
@@ -434,10 +429,7 @@ export abstract class ParagraphBase extends 
MessageListenersManager {
     }
 
     if (!config.editorSetting) {
-      config.editorSetting = {
-        params: {},
-        forms: {}
-      };
+      config.editorSetting = {};
     } else if (config.editorSetting.editOnDblClick) {
       this.editorSetting.isOutputHidden = config.editorSetting.editOnDblClick;
     }
diff --git 
a/zeppelin-web-angular/src/app/pages/workspace/share/dynamic-forms/dynamic-forms.component.html
 
b/zeppelin-web-angular/src/app/pages/workspace/share/dynamic-forms/dynamic-forms.component.html
index c0e125ba44..997dbe3bfe 100644
--- 
a/zeppelin-web-angular/src/app/pages/workspace/share/dynamic-forms/dynamic-forms.component.html
+++ 
b/zeppelin-web-angular/src/app/pages/workspace/share/dynamic-forms/dynamic-forms.component.html
@@ -18,7 +18,7 @@
           <label class="item-label" nz-col nzSpan="6">{{ form.displayName 
}}:</label>
         }
         <div class="control-wrap" nz-col [nzSpan]="form.displayName ? 16 : 24">
-          @switch (form.type) {
+          @switch (canonicalFormType(form.type)) {
             @case (formType.TextBox) {
               <input
                 nz-input
@@ -37,9 +37,14 @@
               />
             }
             @case (formType.Select) {
-              <nz-select [nzDisabled]="disable" 
[(ngModel)]="paramDefs[form.name]" (ngModelChange)="onFormChange()">
+              <nz-select
+                [nzDisabled]="disable"
+                [compareWith]="compareOptionValues"
+                [(ngModel)]="paramDefs[form.name]"
+                (ngModelChange)="onFormChange()"
+              >
                 @for (opt of form.options; track opt) {
-                  <nz-option [nzLabel]="opt.displayName || opt.value" 
[nzValue]="opt.value"></nz-option>
+                  <nz-option [nzLabel]="opt.displayName || 
optionLabel(opt.value)" [nzValue]="opt.value"></nz-option>
                 }
               </nz-select>
             }
diff --git 
a/zeppelin-web-angular/src/app/pages/workspace/share/dynamic-forms/dynamic-forms.component.spec.ts
 
b/zeppelin-web-angular/src/app/pages/workspace/share/dynamic-forms/dynamic-forms.component.spec.ts
new file mode 100644
index 0000000000..4e79921baf
--- /dev/null
+++ 
b/zeppelin-web-angular/src/app/pages/workspace/share/dynamic-forms/dynamic-forms.component.spec.ts
@@ -0,0 +1,76 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { describe, expect, it, vi } from 'vitest';
+
+import { DynamicFormsType } from '@zeppelin/sdk';
+
+import { NotebookParagraphDynamicFormsComponent } from 
'./dynamic-forms.component';
+
+describe('dynamic form wire values', () => {
+  it.each([0, false, '', null])('preserves an existing parameter %j', value => 
{
+    const component = new NotebookParagraphDynamicFormsComponent();
+    component.formDefs = {
+      field: { name: 'field', type: DynamicFormsType.TextBox, hidden: false, 
defaultValue: 'default' }
+    };
+    component.paramDefs = { field: value };
+    component.setForms();
+    expect(component.paramDefs.field).toEqual(value);
+  });
+
+  it.each([DynamicFormsType.CheckBox, DynamicFormsType.LegacyCheckBox])(
+    'round-trips arbitrary values for %s through checkbox option indexes',
+    type => {
+      const component = new NotebookParagraphDynamicFormsComponent();
+      component.formDefs = {
+        field: {
+          name: 'field',
+          type,
+          hidden: false,
+          defaultValue: [],
+          options: [{ value: false }, { value: 42 }, { value: { id: 'a' } }]
+        }
+      };
+      component.paramDefs = { field: [false, { id: 'a' }] };
+      component.setForms();
+      expect(component.checkboxValues.field).toEqual([0, 2]);
+      expect(component.checkboxGroups.field).toEqual([
+        { label: 'false', value: 0 },
+        { label: '42', value: 1 },
+        { label: '{"id":"a"}', value: 2 }
+      ]);
+      const onFormChange = vi.spyOn(component, 'onFormChange');
+      component.checkboxChange([1, 2], 'field');
+      expect(component.paramDefs.field).toEqual([42, { id: 'a' }]);
+      expect(onFormChange).toHaveBeenCalledOnce();
+    }
+  );
+
+  it.each([
+    [DynamicFormsType.LegacyInput, DynamicFormsType.TextBox],
+    [DynamicFormsType.LegacySelect, DynamicFormsType.Select],
+    [DynamicFormsType.LegacyCheckBox, DynamicFormsType.CheckBox]
+  ])('renders legacy %s with the %s control', (legacy, current) => {
+    const component = new NotebookParagraphDynamicFormsComponent();
+    expect(component.canonicalFormType(legacy)).toBe(current);
+  });
+
+  it('uses a default only for an absent parameter', () => {
+    const component = new NotebookParagraphDynamicFormsComponent();
+    component.formDefs = {
+      field: { name: 'field', type: DynamicFormsType.Select, hidden: false, 
defaultValue: 42 }
+    };
+    component.paramDefs = {};
+    component.setForms();
+    expect(component.paramDefs.field).toBe(42);
+  });
+});
diff --git 
a/zeppelin-web-angular/src/app/pages/workspace/share/dynamic-forms/dynamic-forms.component.testbed.spec.ts
 
b/zeppelin-web-angular/src/app/pages/workspace/share/dynamic-forms/dynamic-forms.component.testbed.spec.ts
new file mode 100644
index 0000000000..3fc663d61d
--- /dev/null
+++ 
b/zeppelin-web-angular/src/app/pages/workspace/share/dynamic-forms/dynamic-forms.component.testbed.spec.ts
@@ -0,0 +1,138 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { NO_ERRORS_SCHEMA, provideZoneChangeDetection } from '@angular/core';
+import { TestBed } from '@angular/core/testing';
+import { FormsModule } from '@angular/forms';
+import { DynamicForms, DynamicFormsType, FormValue } from '@zeppelin/sdk';
+import { NzCheckboxModule } from 'ng-zorro-antd/checkbox';
+import { NzSelectModule } from 'ng-zorro-antd/select';
+import { describe, expect, it } from 'vitest';
+
+import { NotebookParagraphDynamicFormsComponent } from 
'./dynamic-forms.component';
+import template from './dynamic-forms.component.html?raw';
+
+describe('Select form hydration (TestBed)', () => {
+  it.each([
+    { kind: 'object', value: { id: 'a' } },
+    { kind: 'array', value: ['a', 42] }
+  ])('displays an independently decoded $kind option', async ({ value }) => {
+    await TestBed.configureTestingModule({
+      declarations: [NotebookParagraphDynamicFormsComponent],
+      imports: [FormsModule, NzSelectModule],
+      providers: [provideZoneChangeDetection()],
+      schemas: [NO_ERRORS_SCHEMA]
+    })
+      .overrideComponent(NotebookParagraphDynamicFormsComponent, {
+        set: { template, templateUrl: undefined, styles: [], styleUrls: [] }
+      })
+      .compileComponents();
+
+    const fixture = 
TestBed.createComponent(NotebookParagraphDynamicFormsComponent);
+    const formDefs: DynamicForms = {
+      field: {
+        name: 'field',
+        type: DynamicFormsType.Select,
+        hidden: false,
+        defaultValue: value,
+        options: [{ value, displayName: 'Option A' }]
+      }
+    };
+    const wire = JSON.parse(JSON.stringify({ forms: formDefs, params: { field: 
value } })) as {
+      forms: DynamicForms;
+      params: { field: FormValue };
+    };
+    expect(wire.params.field).not.toBe(wire.forms.field.options![0].value);
+    fixture.componentInstance.formDefs = wire.forms;
+    fixture.componentInstance.paramDefs = wire.params;
+    fixture.detectChanges();
+    await fixture.whenStable();
+    fixture.detectChanges();
+
+    const selected = 
fixture.nativeElement.querySelector('.ant-select-selection-item') as 
HTMLElement | null;
+    expect(selected).not.toBeNull();
+    expect(selected?.textContent).toContain('Option A');
+    expect(fixture.componentInstance.paramDefs.field).toEqual(value);
+  });
+});
+
+describe('Checkbox form interaction (TestBed)', () => {
+  it.each([DynamicFormsType.CheckBox, DynamicFormsType.LegacyCheckBox])(
+    'submits original %s values and restores selection after a broadcast',
+    async type => {
+      await TestBed.configureTestingModule({
+        declarations: [NotebookParagraphDynamicFormsComponent],
+        imports: [FormsModule, NzCheckboxModule],
+        providers: [provideZoneChangeDetection()],
+        schemas: [NO_ERRORS_SCHEMA]
+      })
+        .overrideComponent(NotebookParagraphDynamicFormsComponent, {
+          set: { template, templateUrl: undefined, styles: [], styleUrls: [] }
+        })
+        .compileComponents();
+
+      const fixture = 
TestBed.createComponent(NotebookParagraphDynamicFormsComponent);
+      const formDefs: DynamicForms = {
+        field: {
+          name: 'field',
+          type,
+          hidden: false,
+          defaultValue: [],
+          options: [false, 0, '', { id: 'a' }, ['a', 42]].map(value => ({ 
value }))
+        }
+      };
+      fixture.componentRef.setInput('formDefs', formDefs);
+      fixture.componentRef.setInput('paramDefs', { field: [] });
+      fixture.detectChanges();
+      await fixture.whenStable();
+      fixture.detectChanges();
+
+      const inputs = 
Array.from(fixture.nativeElement.querySelectorAll('input[type="checkbox"]')) as 
HTMLInputElement[];
+      expect(inputs).toHaveLength(5);
+      for (const input of inputs) {
+        input.click();
+        fixture.detectChanges();
+        await fixture.whenStable();
+      }
+      inputs[1].click();
+      fixture.detectChanges();
+      await fixture.whenStable();
+      fixture.detectChanges();
+
+      const expectedValues = [false, '', { id: 'a' }, ['a', 42]];
+      
expect(fixture.componentInstance.paramDefs.field).toEqual(expectedValues);
+      const submitted: string[] = [];
+      fixture.componentInstance.formChange.subscribe(() => {
+        submitted.push(JSON.stringify(fixture.componentInstance.paramDefs));
+      });
+      fixture.nativeElement.dispatchEvent(new KeyboardEvent('keydown', { key: 
'Enter', bubbles: true }));
+      expect(submitted).toEqual([JSON.stringify({ field: expectedValues })]);
+
+      const broadcast = JSON.parse(JSON.stringify({ forms: formDefs, params: 
JSON.parse(submitted[0]) })) as {
+        forms: DynamicForms;
+        params: { field: FormValue };
+      };
+      fixture.componentRef.setInput('formDefs', broadcast.forms);
+      fixture.componentRef.setInput('paramDefs', broadcast.params);
+      fixture.detectChanges();
+      await fixture.whenStable();
+      fixture.detectChanges();
+
+      const restored = Array.from(
+        fixture.nativeElement.querySelectorAll('input[type="checkbox"]')
+      ) as HTMLInputElement[];
+      expect(restored.map(input => input.checked)).toEqual([true, false, true, 
true, true]);
+      
expect(fixture.componentInstance.paramDefs.field).toEqual(expectedValues);
+      expect(submitted).toHaveLength(1);
+    }
+  );
+});
diff --git 
a/zeppelin-web-angular/src/app/pages/workspace/share/dynamic-forms/dynamic-forms.component.ts
 
b/zeppelin-web-angular/src/app/pages/workspace/share/dynamic-forms/dynamic-forms.component.ts
index a5d9795d6f..a97561ef51 100644
--- 
a/zeppelin-web-angular/src/app/pages/workspace/share/dynamic-forms/dynamic-forms.component.ts
+++ 
b/zeppelin-web-angular/src/app/pages/workspace/share/dynamic-forms/dynamic-forms.component.ts
@@ -23,11 +23,22 @@ import {
   SimpleChanges
 } from '@angular/core';
 import { Subject } from 'rxjs';
+import { isEqual } from 'lodash';
 import { debounceTime, takeUntil } from 'rxjs/operators';
 
 import { NzCheckboxOption } from 'ng-zorro-antd/checkbox';
 
-import { DynamicForms, DynamicFormsItem, DynamicFormsType, DynamicFormParams } 
from '@zeppelin/sdk';
+import { DynamicForms, DynamicFormsItem, DynamicFormsType, DynamicFormParams, 
FormValue } from '@zeppelin/sdk';
+
+const canonicalFormTypes: Record<DynamicFormsType, DynamicFormsType> = {
+  TextBox: DynamicFormsType.TextBox,
+  Password: DynamicFormsType.Password,
+  Select: DynamicFormsType.Select,
+  CheckBox: DynamicFormsType.CheckBox,
+  input: DynamicFormsType.TextBox,
+  select: DynamicFormsType.Select,
+  checkbox: DynamicFormsType.CheckBox
+};
 
 @Component({
   selector: 'zeppelin-notebook-paragraph-dynamic-forms',
@@ -50,11 +61,12 @@ export class NotebookParagraphDynamicFormsComponent 
implements OnInit, OnChanges
   formChange$ = new Subject<void>();
   forms: DynamicFormsItem[] = [];
   formType = DynamicFormsType;
+  compareOptionValues = isEqual;
   checkboxGroups: {
     [key: string]: NzCheckboxOption[];
   } = {};
   checkboxValues: {
-    [key: string]: Array<string | number>;
+    [key: string]: number[];
   } = {};
 
   @HostListener('keydown.enter')
@@ -71,27 +83,47 @@ export class NotebookParagraphDynamicFormsComponent 
implements OnInit, OnChanges
   setForms() {
     this.forms = Object.values(this.formDefs);
     this.checkboxGroups = {};
-    this.forms.forEach(e => {
-      if (!this.paramDefs[e.name]) {
-        this.paramDefs[e.name] = e.defaultValue;
+    this.checkboxValues = {};
+    this.forms.forEach(form => {
+      if (this.paramDefs[form.name] === undefined) {
+        this.paramDefs[form.name] = form.defaultValue;
       }
-      if (e.type === DynamicFormsType.CheckBox) {
-        // CheckBox type should have defined 'options'.
-        // ng-zorro v19 split nz-checkbox-group into `nzOptions` (the {label, 
value}
-        // choices) and an ngModel that holds the selected values directly, 
instead
-        // of a single array of {label, value, checked} objects.
-        this.checkboxGroups[e.name] = e.options!.map(opt => ({
-          label: opt.displayName || opt.value,
-          value: opt.value
+
+      if (this.canonicalFormType(form.type) === DynamicFormsType.CheckBox) {
+        const options = form.options ?? [];
+        const param = this.paramDefs[form.name];
+        const selectedValues = Array.isArray(param) ? param : [];
+
+        // NG-Zorro checkboxes require primitive values. Option indexes 
preserve
+        // Zeppelin's scalar, object and array values at the UI boundary.
+        this.checkboxGroups[form.name] = options.map((option, index) => ({
+          label: option.displayName || this.optionLabel(option.value),
+          value: index
         }));
-        const param = this.paramDefs[e.name];
-        this.checkboxValues[e.name] = Array.isArray(param) ? [...param] : [];
+
+        this.checkboxValues[form.name] = [];
+        options.forEach((option, index) => {
+          if (selectedValues.some(value => isEqual(value, option.value))) {
+            this.checkboxValues[form.name].push(index);
+          }
+        });
       }
     });
   }
 
-  checkboxChange(value: Array<string | number>, name: string) {
-    this.paramDefs[name] = value as string[];
+  canonicalFormType(type: DynamicFormsType): DynamicFormsType {
+    return canonicalFormTypes[type];
+  }
+
+  optionLabel(value: FormValue): string {
+    return typeof value === 'string' ? value : JSON.stringify(value);
+  }
+
+  checkboxChange(value: number[], name: string) {
+    const options = this.formDefs[name].options ?? [];
+    this.paramDefs[name] = value
+      .filter(index => index >= 0 && index < options.length)
+      .map(index => options[index].value);
     this.onFormChange();
   }
 
diff --git a/zeppelin-web-angular/src/app/services/message.service.ts 
b/zeppelin-web-angular/src/app/services/message.service.ts
index 1b5746482d..269e28dbb5 100644
--- a/zeppelin-web-angular/src/app/services/message.service.ts
+++ b/zeppelin-web-angular/src/app/services/message.service.ts
@@ -198,14 +198,13 @@ export class MessageService extends Message implements 
OnDestroy {
     noteId: string,
     paragraphId: string,
     name: string,
-    value: string,
+    value: unknown,
     interpreterGroupId: string
   ): void {
     super.angularObjectUpdate(noteId, paragraphId, name, value, 
interpreterGroupId);
   }
 
-  // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  angularObjectClientBind(noteId: string, name: string, value: any, 
paragraphId: string): void {
+  angularObjectClientBind(noteId: string, name: string, value: unknown, 
paragraphId: string): void {
     super.angularObjectClientBind(noteId, name, value, paragraphId);
   }
 

Reply via email to