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-5676-9fa612401790f866d1e58a94f70744c996a2f696 in repository https://gitbox.apache.org/repos/asf/texera.git
commit ba63cedf0185dc1cd92fd4eec5f175a660639c02 Author: Ranjani Veena Belavadi <[email protected]> AuthorDate: Fri Jul 31 13:15:50 2026 -0700 fix(frontend): allow null/undefined to pass enum validation for optional operator properties (#5676) ### What changes were proposed in this PR? Fix optional enum fields in operator properties. When a dropdown is cleared, the value becomes null/undefined instead of being removed, which caused validation to incorrectly fail for optional fields. The fix has two parts: 1. The validator now allows null/undefined to pass for optional fields (c.value == null || mapSource.enum?.includes(c.value)) so no false error is shown in the UI 2. onFormChanges removes the key entirely for optional fields with null/undefined values before the data is saved, so the backend never receives a bad value Before: clearing an optional dropdown left { x: 10, y: null } in the model, which failed validation and sent bad data to the backend. After: clearing an optional dropdown results in { x: 10 } with the key removed entirely — no error shown, clean data sent to backend. <img width="1915" height="1050" alt="Animation" src="https://github.com/user-attachments/assets/3e52fbd5-a25d-4f5a-b9c2-4eeec2c16f87" /> ### Any related issues, documentation, discussions? Fixes #3524 ### How was this PR tested? Manually tested on UI: recorded GIF showing optional Pattern field in Bar Chart operator being cleared with no validation error and workflow executing successfully. Added 3 unit tests to operator-property-edit-frame.component.spec.ts covering: - null values are removed for optional fields - null values are kept for required fields (so the error still shows) - normal values are always kept regardless of required status All existing tests continue to pass. ### Was this PR authored or co-authored using generative AI tooling? Co-authored using Claude (Anthropic). The fix was reviewed and verified by the author. --- .../operator-property-edit-frame.component.spec.ts | 58 ++++++++++++++++++++++ .../operator-property-edit-frame.component.ts | 8 ++- 2 files changed, 64 insertions(+), 2 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 9bb72cea23..aba2811444 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 @@ -2088,6 +2088,64 @@ describe("OperatorPropertyEditFrameComponent", () => { }); }); + describe("onFormChanges null handling", () => { + it("should strip null values for optional fields", () => { + component.currentOperatorSchema = { + ...mockScanSourceSchema, + jsonSchema: { ...mockScanSourceSchema.jsonSchema, required: ["tableName"] }, + }; + + let emittedEvent: Record<string, unknown> | undefined; + component.sourceFormChangeEventStream.subscribe(event => (emittedEvent = event)); + + component.onFormChanges({ tableName: "table1", optionalField: null }); + + expect(emittedEvent).toEqual({ tableName: "table1" }); + }); + + it("should keep null values for required fields", () => { + component.currentOperatorSchema = { + ...mockScanSourceSchema, + jsonSchema: { ...mockScanSourceSchema.jsonSchema, required: ["tableName"] }, + }; + + let emittedEvent: Record<string, unknown> | undefined; + component.sourceFormChangeEventStream.subscribe(event => (emittedEvent = event)); + + component.onFormChanges({ tableName: null, optionalField: "value" }); + + expect(emittedEvent).toEqual({ tableName: null, optionalField: "value" }); + }); + + it("should keep non-null values regardless of required status", () => { + component.currentOperatorSchema = { + ...mockScanSourceSchema, + jsonSchema: { ...mockScanSourceSchema.jsonSchema, required: ["tableName"] }, + }; + + let emittedEvent: Record<string, unknown> | undefined; + component.sourceFormChangeEventStream.subscribe(event => (emittedEvent = event)); + + component.onFormChanges({ tableName: "table1", optionalField: "set" }); + + expect(emittedEvent).toEqual({ tableName: "table1", optionalField: "set" }); + }); + + it("should strip undefined values for optional fields", () => { + component.currentOperatorSchema = { + ...mockScanSourceSchema, + jsonSchema: { ...mockScanSourceSchema.jsonSchema, required: ["tableName"] }, + }; + + let emittedEvent: Record<string, unknown> | undefined; + component.sourceFormChangeEventStream.subscribe(event => (emittedEvent = event)); + + component.onFormChanges({ tableName: "table1", optionalField: undefined }); + + expect(emittedEvent).toEqual({ tableName: "table1" }); + }); + }); + describe("modify-operator-logic gating", () => { it("allowModifyOperatorLogic re-enables editing", () => { fixture.detectChanges(); 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 128f674645..f4b356c474 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 @@ -544,7 +544,11 @@ export class OperatorPropertyEditFrameComponent implements OnInit, OnChanges, On * @param event */ onFormChanges(event: Record<string, unknown>): void { - this.sourceFormChangeEventStream.next(event); + const requiredFields = this.currentOperatorSchema?.jsonSchema?.required ?? []; + const cleanedEvent = Object.fromEntries( + Object.entries(event).filter(([key, value]) => value != null || requiredFields.includes(key)) + ); + this.sourceFormChangeEventStream.next(cleanedEvent); } /** @@ -1118,7 +1122,7 @@ export class OperatorPropertyEditFrameComponent implements OnInit, OnChanges, On if (isDefined(mapSource.enum)) { mappedField.validators.inEnum = { - expression: (c: AbstractControl) => mapSource.enum?.includes(c.value ?? ""), + expression: (c: AbstractControl) => c.value == null || mapSource.enum?.includes(c.value), message: (error: any, field: FormlyFieldConfig) => `"${field.formControl?.value}" is no longer a valid option`, };
