mengw15 commented on code in PR #8437:
URL: https://github.com/apache/texera/pull/8437#discussion_r3943459829


##########
frontend/src/app/workspace/component/workflow-form/workflow-form.component.ts:
##########
@@ -175,6 +240,149 @@ export class WorkflowFormComponent implements OnInit, 
OnDestroy {
     this.workflowActionService.disableWorkflowModification();
   }
 
+  // 
---------------------------------------------------------------------------
+  // Inputs: the exposed properties, rendered as their operators' own fields
+  // 
---------------------------------------------------------------------------
+
+  /** Whether the cursor is currently inside one of this page's inputs. */
+  private isTypingInTheForm(): boolean {
+    const active = document.activeElement as HTMLElement | null;
+    if (!active || !this.host.nativeElement.contains(active)) {
+      return false;
+    }
+    return ["INPUT", "TEXTAREA", "SELECT"].includes(active.tagName) || 
active.isContentEditable;
+  }
+
+  private readConfig(): void {
+    this.parameters = this.formBindingService.resolveFields();
+    this.buildForm();
+  }
+
+  /**
+   * Build the form from the operators' JSON schemas (FormlyJsonschema), 
keeping the one field per
+   * exposed property. Each input gets its own form keyed by binding id.
+   */
+  private buildForm(): void {
+    this.formsRebuilt.next();
+    this.rendered = this.visibleFields
+      .map(field => this.renderField(field))
+      .filter((r): r is RenderedField => r !== undefined);
+  }
+
+  private renderField(resolved: ResolvedField): RenderedField | undefined {
+    const { binding } = resolved;
+    const schema = this.operatorSchemaFor(binding.operatorID);
+    if (!schema) {
+      return undefined;
+    }
+    const operatorType = 
this.workflowActionService.getTexeraGraph().getOperator(binding.operatorID)?.operatorType;
+    const full = this.formlyJsonschema.toFieldConfig(cloneDeep(schema) as 
never, {
+      map: (mapped, source) => {
+        // Render the exact custom widget the operator property panel would 
(file/model/dataset
+        // pickers, image/audio uploaders, ...), shared via 
customFormlyFieldType so an exposed
+        // property shows its real control instead of degrading to a text box.
+        const customType = customFormlyFieldType({
+          key: mapped.key,
+          operatorType,
+          description: (source as { description?: string })?.description,
+          currentType: mapped.type,
+        });
+        // Canvas-only widgets (code editor, drag-reorder) do not work here; 
an older workflow may
+        // already carry one, so leave it to formly's default editable control 
rather than a widget
+        // that cannot function on a form.
+        if (customType && !CANVAS_ONLY_FORMLY_TYPES.has(customType)) {
+          mapped.type = customType;
+        }
+        return mapped;
+      },
+    });
+    const source = (full.fieldGroup ?? []).find(child => child.key === 
binding.propertyKey);
+    if (!source) {
+      return undefined;
+    }
+
+    const field = cloneDeep(source);
+    // The schema's own title ("Attributes", "Limit", "File") -- the reader's 
title when unnamed.
+    // Falls back to this, not the lower-camel key ("fileName"), which would 
read inconsistently.
+    const schemaLabel = (source.props?.label as string) || binding.propertyKey;
+    field.key = binding.id;
+    field.props = {
+      ...(field.props ?? {}),
+      label: binding.displayName || schemaLabel,
+      // The schema's own description is the operator author's note to whoever 
wired the operator
+      // up; it is not guidance to a form reader, and formly shows it once per 
scalar field. Drop it
+      // here so it does not appear unbidden under the input.
+      description: "",
+    };
+
+    const form = new FormGroup({});
+    const model: Record<string, unknown> = { [binding.id]: 
cloneDeep(resolved.value) };
+    if (this.canEdit) {
+      form.valueChanges
+        .pipe(debounceTime(FORM_DEBOUNCE_TIME_MS), 
takeUntil(this.formsRebuilt), untilDestroyed(this))
+        .subscribe(() => {
+          // Formly emits the schema's empty default while building the 
control, before any edit;
+          // writing that back silently wiped the operator's real value (both 
views edit one
+          // workflow). So only accept a dirtied form, or a value that differs 
from the operator's
+          // without being emptier (some controls set values without marking 
dirty).
+          const next = model[binding.id];
+          const current = 
this.formBindingService.readValue(binding.operatorID, binding.propertyKey);
+          const isEmpty = (v: unknown) => v === undefined || v === null || v 
=== "";
+          const unchanged = JSON.stringify(next ?? null) === 
JSON.stringify(current ?? null);
+          if (unchanged || (!form.dirty && isEmpty(next) && 
!isEmpty(current))) {
+            return;
+          }
+          // Write straight onto the operator (the same edit the canvas makes) 
and refresh this
+          // card's snapshot, which the template reads.
+          this.formBindingService.writeValue(binding, next);
+          this.parameters = this.formBindingService.resolveFields();
+          const refreshed = this.parameters.find(p => p.binding.id === 
binding.id);
+          const card = this.rendered.find(r => r.resolved.binding.id === 
binding.id);
+          if (refreshed && card) {
+            card.resolved = refreshed;
+          }
+          this.cdr.detectChanges();
+        });
+    } else {
+      // A read-only viewer sees the author's values and can run with them, 
but cannot change them:
+      // disable the control so it renders non-editable, and wire no 
write-back at all.
+      form.disable();
+    }
+
+    return { resolved, fields: [field], form, model };
+  }
+
+  private operatorSchemaFor(operatorID: string): object | undefined {
+    const graph = this.workflowActionService.getTexeraGraph();
+    if (!graph.hasOperator(operatorID)) {
+      return undefined;
+    }
+    try {
+      // Prefer the per-instance schema: it carries the upstream column names, 
so an attribute
+      // picker renders as a dropdown of real columns rather than a text box.
+      return this.dynamicSchemaService.getDynamicSchema(operatorID).jsonSchema;
+    } catch {
+      try {
+        return 
this.operatorMetadataService.getOperatorSchema(graph.getOperator(operatorID).operatorType).jsonSchema;
+      } catch {
+        return undefined;
+      }
+    }
+  }
+
+  /**
+   * The inputs a reader is offered. Broken bindings (the operator was 
deleted, or the property key
+   * no longer exists) are left out, since filling one in could not affect a 
run; the author's view
+   * of them, to repair them, is added by the authoring PR.
+   */
+  public get visibleFields(): ResolvedField[] {

Review Comment:
   You're right — I pinned FormFieldOverride's doc on the binding level, which 
has no `hidden`. With #8438 honouring it in the sub-field walk, nothing to 
filter here.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to