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

bbovenzi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new caab2a99d11 Fix for object rendering in HITL interface (#58611)
caab2a99d11 is described below

commit caab2a99d11ed563b990fec43071d724d69b1c27
Author: Stuart Buckingham <[email protected]>
AuthorDate: Tue Nov 25 12:17:15 2025 -0800

    Fix for object rendering in HITL interface (#58611)
---
 airflow-core/src/airflow/ui/src/utils/hitl.test.ts | 65 ++++++++++++++++++++++
 airflow-core/src/airflow/ui/src/utils/hitl.ts      | 33 ++++++++++-
 2 files changed, 95 insertions(+), 3 deletions(-)

diff --git a/airflow-core/src/airflow/ui/src/utils/hitl.test.ts 
b/airflow-core/src/airflow/ui/src/utils/hitl.test.ts
new file mode 100644
index 00000000000..daead151359
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/utils/hitl.test.ts
@@ -0,0 +1,65 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 type { TFunction } from "i18next";
+import { describe, it, expect, vi } from "vitest";
+
+import type { HITLDetail } from "openapi/requests/types.gen";
+
+import { getHITLParamsDict } from "./hitl";
+
+const mockTranslate = vi.fn((key: string) => key) as unknown as TFunction;
+
+const createMockHITLDetail = (overrides?: Partial<HITLDetail>): HITLDetail =>
+  ({
+    assigned_users: [],
+    body: "Test Body",
+    chosen_options: [],
+    created_at: new Date().toISOString(),
+    defaults: ["Option1"],
+    multiple: false,
+    options: ["Option1", "Option2"],
+    params: {},
+    params_input: {},
+    response_received: false,
+    subject: "Test Subject",
+    task_instance: {
+      dag_id: "test_dag",
+      dag_run_id: "test_run",
+      map_index: -1,
+      state: "deferred",
+      task_id: "test_task",
+    },
+    ...overrides,
+  }) as HITLDetail;
+
+describe("getHITLParamsDict", () => {
+  it("correctly types object parameters as 'object' instead of 'string'", () 
=> {
+    const hitlDetail = createMockHITLDetail({
+      params: {
+        objectParam: { key: "value", nested: { data: 123 } },
+      },
+    });
+
+    const searchParams = new URLSearchParams();
+    const paramsDict = getHITLParamsDict(hitlDetail, mockTranslate, 
searchParams);
+
+    expect(paramsDict.objectParam?.schema.type).toBe("object");
+    expect(paramsDict.objectParam?.value).toEqual({ key: "value", nested: { 
data: 123 } });
+  });
+});
diff --git a/airflow-core/src/airflow/ui/src/utils/hitl.ts 
b/airflow-core/src/airflow/ui/src/utils/hitl.ts
index bd3a4051754..6fb7745ded3 100644
--- a/airflow-core/src/airflow/ui/src/utils/hitl.ts
+++ b/airflow-core/src/airflow/ui/src/utils/hitl.ts
@@ -70,7 +70,7 @@ export const getHITLParamsDict = (
   searchParams: URLSearchParams,
 ): ParamsSpec => {
   const paramsDict: ParamsSpec = {};
-  const { preloadedHITLOptions } = getPreloadHITLFormData(searchParams, 
hitlDetail);
+  const { preloadedHITLOptions, preloadedHITLParams } = 
getPreloadHITLFormData(searchParams, hitlDetail);
   const isApprovalTask =
     hitlDetail.options.includes("Approve") &&
     hitlDetail.options.includes("Reject") &&
@@ -113,9 +113,36 @@ export const getHITLParamsDict = (
       }
       const paramData = hitlDetail.params[key] as ParamsSpec | undefined;
 
+      // Check if there's a preloaded value from URL params
+      let finalValue = preloadedHITLParams[key] ?? value;
+
+      // If preloaded value is a string that might be JSON, try to parse it
+      if (typeof finalValue === "string" && finalValue.trim().startsWith("{")) 
{
+        try {
+          const parsed: unknown = JSON.parse(finalValue);
+
+          if (typeof parsed === "object" && parsed !== null && 
!Array.isArray(parsed)) {
+            finalValue = parsed;
+          }
+        } catch {
+          // If parsing fails, keep the string value
+        }
+      }
+
       const description: string =
         paramData && typeof paramData.description === "string" ? 
paramData.description : "";
 
+      // Determine the type based on the final value
+      let valueType: string;
+
+      if (typeof finalValue === "number") {
+        valueType = "number";
+      } else if (typeof finalValue === "object" && finalValue !== null && 
!Array.isArray(finalValue)) {
+        valueType = "object";
+      } else {
+        valueType = "string";
+      }
+
       const schema: ParamSchema = {
         const: undefined,
         description_md: "",
@@ -129,7 +156,7 @@ export const getHITLParamsDict = (
         minLength: undefined,
         section: undefined,
         title: key,
-        type: typeof value === "number" ? "number" : "string",
+        type: valueType,
         values_display: undefined,
         ...(paramData?.schema && typeof paramData.schema === "object" ? 
paramData.schema : {}),
       };
@@ -137,7 +164,7 @@ export const getHITLParamsDict = (
       paramsDict[key] = {
         description,
         schema,
-        value: paramData?.value ?? value,
+        value: paramData?.value ?? finalValue,
       };
     });
   }

Reply via email to