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

github-merge-queue[bot] pushed a commit to branch 
gh-readonly-queue/main/pr-8436-09b4e74560a0cb16c557816711639493564f9d3c
in repository https://gitbox.apache.org/repos/asf/texera.git

commit 27f21a9ba2d215767aec385a77d82d426deaf77c
Author: yangzhang75 <[email protected]>
AuthorDate: Sat Sep 5 17:55:59 2026 +0000

    feat(workflow-form): share the operator property panel's field-type rules 
(#8436)
    
    ### What changes were proposed in this PR?
    
    Field-type infrastructure the Form View reader will build on (parent
    issue #8011). Frontend only. The refactor half is behaviour-preserving;
    the one exposability rule it adds has a single visible effect, and only
    with the feature flag on: in the authoring panel, a code-editor property
    no longer offers the expose checkbox (an ordinary property still does).
    
    * Extract the operator property panel's custom-widget decisions --
    scattered inline through the field map (file picker, model picker,
    HuggingFace image/audio uploaders, dataset selector, code box,
    Projection's drag-reorder list) -- into one pure helper,
    `customFormlyFieldType`, and have the panel call it. Same widget for
    every property as before; the mapping is now a single source of truth a
    later view (the Form View) can share, so a selectable or uploadable
    property never silently degrades to a plain text box there.
    * Add the companion rule `NON_FORM_FIELD_TYPES` (the code editor): a
    property whose control cannot be a form field is not offered for
    exposure, since writing code is not "filling in a value". A drag-reorder
    property (e.g. Projection's columns) stays exposable -- it just renders
    without the drag in the form.
    
    Sub-field addressing (`childPath` / `arrayItemOf`) is intentionally not
    here: it lives on the form component and lands with the render PR that
    consumes it.
    
    ### Any related issues, documentation, discussions?
    
    Closes #8021. Part of the Form View feature (parent issue #8011); builds
    on the merged panel/choose work.
    
    ### How was this PR tested?
    
    Unit tests (vitest). `custom-formly-type.spec.ts` covers the helper at
    100% (statements and branches) -- every widget rule, each guarded
    negative, and the default. The shared
    `operator-property-edit-frame.component.spec.ts` (250 tests) is green,
    confirming the extraction leaves the panel's behaviour unchanged, plus a
    new test that a code-editor property is not offered for exposure while
    an ordinary one is. Every line added to the panel is covered. `ng build`
    is clean.
    
    The one visible effect (with the flag on): in the authoring panel a
    code-editor property no longer offers the expose checkbox, while an
    ordinary property still does. Screenshot available on request.
    
    ### Was this PR authored or co-authored using generative AI tooling?
    
    Yes. Co-authored with Claude (Anthropic), reviewed line by line by the
    author before submission.
    
    Co-authored-by: Claude Opus 4.8 <[email protected]>
---
 .../operator-property-edit-frame.component.spec.ts | 25 +++++++
 .../operator-property-edit-frame.component.ts      | 52 ++++++--------
 .../app/workspace/util/custom-formly-type.spec.ts  | 82 ++++++++++++++++++++++
 .../src/app/workspace/util/custom-formly-type.ts   | 76 ++++++++++++++++++++
 4 files changed, 206 insertions(+), 29 deletions(-)

diff --git 
a/frontend/src/app/workspace/component/property-editor/operator-property-edit-frame/operator-property-edit-frame.component.spec.ts
 
b/frontend/src/app/workspace/component/property-editor/operator-property-edit-frame/operator-property-edit-frame.component.spec.ts
index f320f5495d..5326030d78 100644
--- 
a/frontend/src/app/workspace/component/property-editor/operator-property-edit-frame/operator-property-edit-frame.component.spec.ts
+++ 
b/frontend/src/app/workspace/component/property-editor/operator-property-edit-frame/operator-property-edit-frame.component.spec.ts
@@ -2988,5 +2988,30 @@ describe("OperatorPropertyEditFrameComponent", () => {
       expect(nestedTableName).toBeDefined();
       expect(nestedTableName?.props?.["toggleExposed"]).toBeUndefined();
     });
+
+    // Writing code is not "filling in a value", so a code-editor property is 
never offered for
+    // exposure; an ordinary property beside it still is.
+    it("does not offer exposure on a code-editor property", () => {
+      const formBindingService = TestBed.inject(FormBindingService);
+      vi.spyOn(formBindingService, "isExposed").mockReturnValue(false);
+      component.exposeChoosing = true;
+      component.currentOperatorId = "op-code";
+
+      component.setFormlyFormBinding({
+        type: "object",
+        properties: {
+          code: { type: "string", description: "input your code here" },
+          limit: { type: "number" },
+        },
+      });
+
+      const topLevel = component.formlyFields?.[0]?.fieldGroup ?? [];
+      const codeField = topLevel.find(f => f.key === "code");
+      expect(codeField?.type).toBe("codearea");
+      expect(codeField?.props?.["toggleExposed"]).toBeUndefined();
+      // an ordinary property is still offered
+      const decorated = topLevel.filter(f => f.props?.["toggleExposed"] !== 
undefined).map(f => f.key);
+      expect(decorated).toEqual(["limit"]);
+    });
   });
 });
diff --git 
a/frontend/src/app/workspace/component/property-editor/operator-property-edit-frame/operator-property-edit-frame.component.ts
 
b/frontend/src/app/workspace/component/property-editor/operator-property-edit-frame/operator-property-edit-frame.component.ts
index daaa137536..be354825fa 100644
--- 
a/frontend/src/app/workspace/component/property-editor/operator-property-edit-frame/operator-property-edit-frame.component.ts
+++ 
b/frontend/src/app/workspace/component/property-editor/operator-property-edit-frame/operator-property-edit-frame.component.ts
@@ -38,6 +38,7 @@ import {
   hideTypes,
 } from "../../../types/custom-json-schema.interface";
 import { isDefined } from "../../../../common/util/predicate";
+import { customFormlyFieldType, NON_FORM_FIELD_TYPES } from 
"../../../util/custom-formly-type";
 import { ExecutionState, OperatorState, OperatorStatistics } from 
"src/app/workspace/types/execute-workflow.interface";
 import { DynamicSchemaService } from 
"../../../service/dynamic-schema/dynamic-schema.service";
 import { WorkflowCompilingService } from 
"../../../service/compile-workflow/workflow-compiling.service";
@@ -905,17 +906,18 @@ export class OperatorPropertyEditFrameComponent 
implements OnInit, OnChanges, On
         };
       }
 
-      // if the title is fileName, then change it to custom autocomplete input 
template
-      if (mappedField.key === "fileName") {
-        mappedField.type = "inputautocomplete";
-      }
-
-      if (mappedField.key === "huggingFaceModel") {
-        mappedField.type = "huggingface";
-      }
-
-      if (mappedField.key === "modelId" && 
this.currentOperatorSchema?.operatorType === "HuggingFace") {
-        mappedField.type = "huggingface";
+      // The custom widget this property renders as (file picker, model 
picker, uploaders, dataset
+      // selector, code box, drag-reorder list). Extracted to 
customFormlyFieldType so a later view
+      // (the Form View) renders the same control; each field's extra 
behaviour -- the task-driven
+      // hide rules below, the Projection reorder callback -- stays here.
+      const customType = customFormlyFieldType({
+        key: mappedField.key,
+        operatorType: this.currentOperatorSchema?.operatorType,
+        description: mapSource?.description,
+        currentType: mappedField.type,
+      });
+      if (customType) {
+        mappedField.type = customType;
       }
 
       if (mappedField.key === "task" && 
this.currentOperatorSchema?.operatorType === "HuggingFace") {
@@ -965,7 +967,7 @@ export class OperatorPropertyEditFrameComponent implements 
OnInit, OnChanges, On
           return undefined;
         };
         if (hfKey === "imageInput") {
-          mappedField.type = "huggingface-image-upload";
+          // type ("huggingface-image-upload") is set by customFormlyFieldType 
above
           mappedField.expressions = {
             ...mappedField.expressions,
             hide: (field: FormlyFieldConfig) => {
@@ -997,7 +999,7 @@ export class OperatorPropertyEditFrameComponent implements 
OnInit, OnChanges, On
           };
         }
         if (hfKey === "audioInput") {
-          mappedField.type = "huggingface-audio-upload";
+          // type ("huggingface-audio-upload") is set by customFormlyFieldType 
above
           mappedField.expressions = {
             ...mappedField.expressions,
             hide: (field: FormlyFieldConfig) => {
@@ -1108,14 +1110,6 @@ export class OperatorPropertyEditFrameComponent 
implements OnInit, OnChanges, On
         }
       }
 
-      if (mappedField.key === "uiParameters") {
-        mappedField.type = "ui-udf-parameters";
-      }
-
-      if (mappedField.key === "datasetVersionPath") {
-        mappedField.type = "datasetversionselector";
-      }
-
       // Show the required marker for a field the schema requires 
conditionally,
       // e.g. Sklearn's Text Attribute once Count Vectorizer is on, or 
Aggregate's
       // attribute for every function but `count`.
@@ -1145,12 +1139,6 @@ export class OperatorPropertyEditFrameComponent 
implements OnInit, OnChanges, On
         };
       }
 
-      // if the title is python script (for Python UDF), then make this field 
a custom template 'codearea'
-      if (mapSource?.description?.toLowerCase() === "input your code here") {
-        if (mappedField.type) {
-          mappedField.type = "codearea";
-        }
-      }
       // if presetService is ready and operator property allows presets, setup 
formly field to display presets
       if (
         this.config.env.userPresetEnabled &&
@@ -1181,7 +1169,8 @@ export class OperatorPropertyEditFrameComponent 
implements OnInit, OnChanges, On
       // }
 
       if (this.currentOperatorSchema?.operatorType === "Projection" && 
mappedField.key === "attributes") {
-        mappedField.type = "repeat-section-dnd";
+        // type ("repeat-section-dnd") is set by customFormlyFieldType above; 
the reorder callback
+        // is the canvas's own and stays here.
         mappedField.props = {
           ...mappedField.props,
           reorder: () => this.onFormChanges(cloneDeep(this.formData)),
@@ -1374,7 +1363,12 @@ export class OperatorPropertyEditFrameComponent 
implements OnInit, OnChanges, On
     if (this.exposeChoosing && this.currentOperatorId && fields) {
       const operatorId = this.currentOperatorId;
       for (const topLevelField of fields) {
-        if (typeof topLevelField.key === "string") {
+        // A property whose control cannot be a form field (the code editor) 
is not offered for
+        // exposure -- its type was already resolved by customFormlyFieldType 
when the field was
+        // built, so the shared NON_FORM_FIELD_TYPES set decides it here.
+        const fieldType = topLevelField.type;
+        const isNonFormField = typeof fieldType === "string" && 
NON_FORM_FIELD_TYPES.has(fieldType);
+        if (typeof topLevelField.key === "string" && !isNonFormField) {
           const propertyKey = topLevelField.key;
           ExposePropertyWrapperComponent.decorate(
             topLevelField,
diff --git a/frontend/src/app/workspace/util/custom-formly-type.spec.ts 
b/frontend/src/app/workspace/util/custom-formly-type.spec.ts
new file mode 100644
index 0000000000..4417ca3f44
--- /dev/null
+++ b/frontend/src/app/workspace/util/custom-formly-type.spec.ts
@@ -0,0 +1,82 @@
+/**
+ * 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 { customFormlyFieldType, NON_FORM_FIELD_TYPES } from 
"./custom-formly-type";
+
+describe("NON_FORM_FIELD_TYPES", () => {
+  it("blocks only the code editor from being a form field, not the 
drag-reorder list", () => {
+    expect(NON_FORM_FIELD_TYPES.has("codearea")).toBe(true);
+    // a drag-reorder property is still a valid form field (it just renders 
without the drag)
+    expect(NON_FORM_FIELD_TYPES.has("repeat-section-dnd")).toBe(false);
+  });
+});
+
+describe("customFormlyFieldType", () => {
+  it("maps a fileName property to the autocomplete input", () => {
+    expect(customFormlyFieldType({ key: "fileName", operatorType: 
"CSVFileScan" })).toBe("inputautocomplete");
+  });
+
+  it("maps huggingFaceModel to the model picker regardless of operator", () => 
{
+    expect(customFormlyFieldType({ key: "huggingFaceModel", operatorType: 
"Anything" })).toBe("huggingface");
+  });
+
+  it("maps modelId to the model picker only on a HuggingFace operator", () => {
+    expect(customFormlyFieldType({ key: "modelId", operatorType: "HuggingFace" 
})).toBe("huggingface");
+    expect(customFormlyFieldType({ key: "modelId", operatorType: "PythonUDF" 
})).toBeUndefined();
+  });
+
+  it("maps HuggingFace image/audio inputs to their uploaders", () => {
+    expect(customFormlyFieldType({ key: "imageInput", operatorType: 
"HuggingFace" })).toBe("huggingface-image-upload");
+    expect(customFormlyFieldType({ key: "audioInput", operatorType: 
"HuggingFace" })).toBe("huggingface-audio-upload");
+    // Off a HuggingFace operator they are plain fields.
+    expect(customFormlyFieldType({ key: "imageInput", operatorType: 
"PythonUDF" })).toBeUndefined();
+    expect(customFormlyFieldType({ key: "audioInput", operatorType: 
"PythonUDF" })).toBeUndefined();
+  });
+
+  it("maps uiParameters and datasetVersionPath to their custom controls", () 
=> {
+    expect(customFormlyFieldType({ key: "uiParameters", operatorType: 
"PythonUDF" })).toBe("ui-udf-parameters");
+    expect(customFormlyFieldType({ key: "datasetVersionPath", operatorType: 
"CSVFileScan" })).toBe(
+      "datasetversionselector"
+    );
+  });
+
+  it("maps the code-editor property to the code box only when it already has 
an editable control", () => {
+    expect(
+      customFormlyFieldType({
+        key: "code",
+        operatorType: "PythonUDF",
+        description: "Input your code here",
+        currentType: "textarea",
+      })
+    ).toBe("codearea");
+    // The description matches but the schema left no editable control -> keep 
the default.
+    expect(
+      customFormlyFieldType({ key: "code", operatorType: "PythonUDF", 
description: "input your code here" })
+    ).toBeUndefined();
+  });
+
+  it("maps Projection's attributes to the drag-reorder list, only on 
Projection", () => {
+    expect(customFormlyFieldType({ key: "attributes", operatorType: 
"Projection" })).toBe("repeat-section-dnd");
+    expect(customFormlyFieldType({ key: "attributes", operatorType: "Filter" 
})).toBeUndefined();
+  });
+
+  it("returns undefined for an ordinary property, keeping formly's default 
control", () => {
+    expect(customFormlyFieldType({ key: "limit", operatorType: "Limit" 
})).toBeUndefined();
+  });
+});
diff --git a/frontend/src/app/workspace/util/custom-formly-type.ts 
b/frontend/src/app/workspace/util/custom-formly-type.ts
new file mode 100644
index 0000000000..6b6e666be9
--- /dev/null
+++ b/frontend/src/app/workspace/util/custom-formly-type.ts
@@ -0,0 +1,76 @@
+/**
+ * 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.
+ */
+
+/**
+ * Widget types that cannot be a form field at all, so the property is not 
offered for exposure on
+ * the Form View. Only the code editor: editing code is not "filling in a 
value", and a form reader
+ * should not be writing code. (A drag-reorder property such as Projection's 
columns stays exposable
+ * -- it just renders without the drag in the form -- so it is deliberately 
NOT in this set.)
+ */
+export const NON_FORM_FIELD_TYPES: ReadonlySet<string> = new Set(["codearea"]);
+
+/**
+ * The custom formly widget an operator-schema property renders as, decided 
from the property key
+ * and its operator. A single source of truth extracted from the operator 
property panel so that a
+ * later view (the Form View) can render the same control instead of letting a 
selectable/uploadable
+ * property silently degrade to a plain text box.
+ *
+ * Returns undefined to keep formly's default control 
(string/number/textarea/...). Only the widget
+ * TYPE lives here; each caller keeps its own field behaviour (the panel's 
task-driven hide rules,
+ * validators, and the Projection reorder callback).
+ */
+export function customFormlyFieldType(input: {
+  key: unknown;
+  operatorType: string | undefined;
+  description?: string;
+  /** formly's already-resolved type; the code box only replaces an editable 
control. */
+  currentType?: unknown;
+}): string | undefined {
+  const { key, operatorType, description, currentType } = input;
+
+  if (key === "fileName") {
+    return "inputautocomplete";
+  }
+  if (key === "huggingFaceModel") {
+    return "huggingface";
+  }
+  if (key === "modelId" && operatorType === "HuggingFace") {
+    return "huggingface";
+  }
+  if (key === "imageInput" && operatorType === "HuggingFace") {
+    return "huggingface-image-upload";
+  }
+  if (key === "audioInput" && operatorType === "HuggingFace") {
+    return "huggingface-audio-upload";
+  }
+  if (key === "uiParameters") {
+    return "ui-udf-parameters";
+  }
+  if (key === "datasetVersionPath") {
+    return "datasetversionselector";
+  }
+  // Python UDF script box: only when the schema already resolved to an 
editable control.
+  if (description?.toLowerCase() === "input your code here" && currentType) {
+    return "codearea";
+  }
+  if (operatorType === "Projection" && key === "attributes") {
+    return "repeat-section-dnd";
+  }
+  return undefined;
+}

Reply via email to