This is an automated email from the ASF dual-hosted git repository.
github-merge-queue[bot] pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/texera.git
The following commit(s) were added to refs/heads/main by this push:
new 27f21a9ba2 feat(workflow-form): share the operator property panel's
field-type rules (#8436)
27f21a9ba2 is described below
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;
+}