This is an automated email from the ASF dual-hosted git repository. ephraimanierobi pushed a commit to branch v3-1-test in repository https://gitbox.apache.org/repos/asf/airflow.git
commit 1e20c1c70d79a49802f30595b4952ac6ed990572 Author: Stuart Buckingham <[email protected]> AuthorDate: Tue Nov 25 12:17:15 2025 -0800 Fix for object rendering in HITL interface (#58611) (cherry picked from commit caab2a99d11ed563b990fec43071d724d69b1c27) --- 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, }; }); }
