From fb9e59303143cda7fd4a30e84c8e7e78d54b0cb6 Mon Sep 17 00:00:00 2001 From: Xinyuan Lin Date: Wed, 5 Aug 2026 23:45:44 -0700 Subject: [PATCH 1/2] chore(frontend): remove the disabled CollabWrapperComponent CollabWrapperComponent cannot be instantiated. It is declared in app.module and registered as the formly wrapper "collab-wrapper", but nothing ever applies that wrapper name to a field: the only code that does is CollabWrapperComponent.setupFieldConfig, inside the component's own file, and its sole caller in operator-property-edit-frame is commented out. That caller was disabled with "TODO: we temporarily disable this due to Yjs update causing issues in Formly". The disabling predates the October 2025 core/gui relocation, so it has been off for well over a year, and every commit touching the component since has been mechanical upkeep -- an Angular 19 upgrade, the standalone-components refactor, and a dependabot y-quill bump keeping a dependency current for code that never runs. This is a proposal rather than an obvious cleanup: the TODO states an intent to restore, so if that is still live, say so and I will close this. The implementation is one `git revert` away either way, and the sibling PresetWrapperComponent.setupFieldConfig remains in use, so the pattern to restore it from is still in the tree. Removed: the component and its template/styles, the app.module import and declaration, the formly-config import and wrapper registration, and the commented-out caller block that referenced the deleted class. One knock-on. operator-property-edit-frame.component.spec.ts imported COLLAB_DEBOUNCE_TIME_MS purely as a tick() duration during fixture setup, unrelated to the debounce its assertions actually wait on (FORM_DEBOUNCE_TIME_MS). It is replaced by a locally named SETUP_FLUSH_MS with the same 10ms value, so behaviour is unchanged. quill, y-quill and quill-cursors are NOT removable: operator-property-edit-frame and port-property-edit-frame both still use them. No package.json change here. Verified: tsc --noEmit clean across the app, formly-config.spec (11) and operator-property-edit-frame.spec (195) green, yarn format:ci passing. --- frontend/src/app/app.module.ts | 2 - .../collab-wrapper.component.css | 37 --- .../collab-wrapper.component.html | 23 -- .../collab-wrapper.component.ts | 227 ------------------ .../src/app/common/formly/formly-config.ts | 6 +- ...ator-property-edit-frame.component.spec.ts | 10 +- .../operator-property-edit-frame.component.ts | 15 -- 7 files changed, 9 insertions(+), 311 deletions(-) delete mode 100644 frontend/src/app/common/formly/collab-wrapper/collab-wrapper/collab-wrapper.component.css delete mode 100644 frontend/src/app/common/formly/collab-wrapper/collab-wrapper/collab-wrapper.component.html delete mode 100644 frontend/src/app/common/formly/collab-wrapper/collab-wrapper/collab-wrapper.component.ts diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index fef2fd5aa9a..04479621324 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -113,7 +113,6 @@ import { DatasetFileSelectorComponent } from "./workspace/component/dataset-file import { DatasetVersionSelectorComponent } from "./workspace/component/dataset-version-selector/dataset-version-selector.component"; import { DatasetSelectionModalComponent } from "./workspace/component/dataset-selection-modal/dataset-selection-modal.component"; import { ReActStepDetailModalComponent } from "./workspace/component/agent/agent-panel/react-step-detail-modal/react-step-detail-modal.component"; -import { CollabWrapperComponent } from "./common/formly/collab-wrapper/collab-wrapper/collab-wrapper.component"; import { NzSwitchModule } from "ng-zorro-antd/switch"; import { NzRadioModule } from "ng-zorro-antd/radio"; import { AboutComponent } from "./hub/component/about/about.component"; @@ -341,7 +340,6 @@ registerLocaleData(en); DatasetVersionSelectorComponent, DatasetSelectionModalComponent, ReActStepDetailModalComponent, - CollabWrapperComponent, AboutComponent, UserWorkflowListItemComponent, UserProjectListItemComponent, diff --git a/frontend/src/app/common/formly/collab-wrapper/collab-wrapper/collab-wrapper.component.css b/frontend/src/app/common/formly/collab-wrapper/collab-wrapper/collab-wrapper.component.css deleted file mode 100644 index d0848680345..00000000000 --- a/frontend/src/app/common/formly/collab-wrapper/collab-wrapper/collab-wrapper.component.css +++ /dev/null @@ -1,37 +0,0 @@ -/** - * 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. - */ - -:host ::ng-deep .ql-clipboard { - max-height: 0; -} - -:host ::ng-deep .ql-editor { - outline: transparent; - overflow: visible; - position: relative; -} - -:host ::ng-deep .ql-editor > p { - margin-bottom: 0; - box-sizing: border-box; -} - -:host ::ng-deep .ql-container { - overflow: visible; -} diff --git a/frontend/src/app/common/formly/collab-wrapper/collab-wrapper/collab-wrapper.component.html b/frontend/src/app/common/formly/collab-wrapper/collab-wrapper/collab-wrapper.component.html deleted file mode 100644 index e3c9700fedb..00000000000 --- a/frontend/src/app/common/formly/collab-wrapper/collab-wrapper/collab-wrapper.component.html +++ /dev/null @@ -1,23 +0,0 @@ - - -
diff --git a/frontend/src/app/common/formly/collab-wrapper/collab-wrapper/collab-wrapper.component.ts b/frontend/src/app/common/formly/collab-wrapper/collab-wrapper/collab-wrapper.component.ts deleted file mode 100644 index ad6c11eabb4..00000000000 --- a/frontend/src/app/common/formly/collab-wrapper/collab-wrapper/collab-wrapper.component.ts +++ /dev/null @@ -1,227 +0,0 @@ -/** - * 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 { AfterContentInit, Component, ElementRef, ViewChild } from "@angular/core"; -import { FieldTypeConfig, FieldWrapper, FormlyFieldConfig } from "@ngx-formly/core"; -import { WorkflowActionService } from "../../../../workspace/service/workflow-graph/model/workflow-action.service"; -import { merge } from "lodash"; -import Quill, { Delta, Range } from "quill"; -import Clipboard from "quill/modules/clipboard"; -import * as Y from "yjs"; -import { QuillBinding } from "y-quill"; -import QuillCursors from "quill-cursors"; -import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; -import { NgStyle } from "@angular/common"; - -// Quill related definitions -export const COLLAB_DEBOUNCE_TIME_MS = 10; - -/** - * Custom clipboard module that removes rich text formats and newline characters - */ -class PlainClipboard extends Clipboard { - override onPaste(range: Range, { text = "" }: { text?: string; html?: string }) { - const plainText = text.replace(/\n/g, ""); - const delta = new Delta().retain(range.index).delete(range.length).insert(plainText); - const index = plainText.length + range.index; - const length = 0; - this.quill.updateContents(delta, Quill.sources.SILENT); - this.quill.setSelection(index, length, Quill.sources.SILENT); - this.quill.scrollSelectionIntoView(); - } -} - -Quill.register( - { - "modules/clipboard": PlainClipboard, - }, - true -); - -Quill.register("modules/cursors", QuillCursors); - -/** - * CollabWrapperComponent is a custom field wrapper that connects a string/textfield typed form field to a collaborative - * text editor based on Yjs and Quill. - */ -@UntilDestroy() -@Component({ - templateUrl: "./collab-wrapper.component.html", - styleUrls: ["./collab-wrapper.component.css"], - imports: [NgStyle], -}) -export class CollabWrapperComponent extends FieldWrapper implements AfterContentInit { - private quill?: Quill; - private currentOperatorId: string = ""; - private operatorType: string = ""; - private quillBinding?: QuillBinding; - private sharedText?: Y.Text; - @ViewChild("editor", { static: true }) divEditor?: ElementRef; - - constructor(private workflowActionService: WorkflowActionService) { - super(); - } - - ngAfterContentInit(): void { - this.setUpYTextEditor(); - this.formControl.valueChanges.pipe(untilDestroyed(this)).subscribe(value => { - if (this.sharedText !== undefined && value !== this.sharedText.toJSON()) { - this.setUpYTextEditor(); - } - }); - this.registerDisableEditorInteractivityHandler(); - } - - private setUpYTextEditor() { - setTimeout(() => { - if (this.field.key === undefined || this.field.props === undefined) { - throw Error( - `form collab-wrapper field ${this.field} doesn't contain necessary .key and .templateOptions.presetKey attributes` - ); - } else { - this.currentOperatorId = this.field.props.currentOperatorId; - this.operatorType = this.field.props.operatorType; - let parents = [this.field.key]; - let parent = this.field.parent; - while (parent?.key !== undefined) { - parents.push(parent.key); - parent = parent.parent; - } - let parentStructure: any = this.workflowActionService - .getTexeraGraph() - .getSharedOperatorPropertyType(this.currentOperatorId); - let structure: any = undefined; - let key: any; - this.workflowActionService.getTexeraGraph().bundleActions(() => { - while (parents.length > 0 && parentStructure !== undefined && parentStructure !== null) { - key = parents.pop(); - structure = parentStructure.get(key); - if (structure === undefined || structure === null) { - if (parents.length > 0) { - if (parentStructure instanceof Y.Array) { - const yArray = parentStructure as Y.Array; - if (yArray.length > parseInt(key)) { - yArray.delete(parseInt(key), 1); - yArray.insert(parseInt(key), [new Y.Map()]); - } else { - yArray.push([new Y.Map()]); - } - } else { - parentStructure.set(key as string, new Y.Map()); - } - } else { - if (parentStructure instanceof Y.Array) { - const yArray = parentStructure as Y.Array; - if (yArray.length > parseInt(key)) { - yArray.delete(parseInt(key), 1); - yArray.insert(parseInt(key), [new Y.Text("")]); - } else { - yArray.push([new Y.Text("")]); - } - } else { - parentStructure.set(key as string, new Y.Text()); - } - } - structure = parentStructure.get(key); - } - parentStructure = structure; - } - }); - this.sharedText = structure; - this.initializeQuillEditor(); - if (this.currentOperatorId && this.sharedText) { - this.quillBinding = new QuillBinding( - this.sharedText, - this.quill, - this.workflowActionService.getTexeraGraph().getSharedModelAwareness() - ); - } - } - }, COLLAB_DEBOUNCE_TIME_MS); - } - - private initializeQuillEditor() { - // Operator name editor - const element = this.divEditor as ElementRef; - this.quill = new Quill(element.nativeElement, { - modules: { - cursors: true, - toolbar: false, - history: { - // Local undo shouldn't undo changes - // from remote users - userOnly: true, - }, - // Disable newline on enter and instead quit editing - keyboard: - this.field.type === "textarea" - ? {} - : { - bindings: { - enter: { - key: 13, - handler: () => {}, - }, - shift_enter: { - key: 13, - shiftKey: true, - handler: () => {}, - }, - }, - }, - }, - formats: [], - placeholder: "Start collaborating...", - theme: "bubble", - }); - this.quill.enable(this.evaluateInteractivity()); - } - - private evaluateInteractivity(): boolean { - return this.formControl.enabled; - } - - private setInteractivity(interactive: boolean) { - if (interactive !== this.quill?.isEnabled()) this.quill?.enable(interactive); - } - - private registerDisableEditorInteractivityHandler(): void { - this.formControl.statusChanges.pipe(untilDestroyed(this)).subscribe(_ => { - this.setInteractivity(this.evaluateInteractivity()); - }); - } - - static setupFieldConfig( - mappedField: FormlyFieldConfig, - operatorType: string, - currentOperatorId: string, - includePresetWrapper: boolean = false - ) { - const fieldConfig: FormlyFieldConfig = { - wrappers: includePresetWrapper - ? ["form-field", "preset-wrapper", "collab-wrapper"] - : ["form-field", "collab-wrapper"], - props: { - operatorType: operatorType, - currentOperatorId: currentOperatorId, - }, - }; - merge(mappedField, fieldConfig); - } -} diff --git a/frontend/src/app/common/formly/formly-config.ts b/frontend/src/app/common/formly/formly-config.ts index c4fc54fd77f..fa6888ee325 100644 --- a/frontend/src/app/common/formly/formly-config.ts +++ b/frontend/src/app/common/formly/formly-config.ts @@ -25,7 +25,6 @@ import { FormlyFieldConfig } from "@ngx-formly/core"; import { CodeareaCustomTemplateComponent } from "../../workspace/component/codearea-custom-template/codearea-custom-template.component"; import { PresetWrapperComponent } from "./preset-wrapper/preset-wrapper.component"; import { DatasetFileSelectorComponent } from "../../workspace/component/dataset-file-selector/dataset-file-selector.component"; -import { CollabWrapperComponent } from "./collab-wrapper/collab-wrapper/collab-wrapper.component"; import { FormlyRepeatDndComponent } from "./repeat-dnd/repeat-dnd.component"; import { UiUdfParametersComponent } from "../../workspace/component/ui-udf-parameters/ui-udf-parameters.component"; import { DatasetVersionSelectorComponent } from "../../workspace/component/dataset-version-selector/dataset-version-selector.component"; @@ -89,10 +88,7 @@ export const TEXERA_FORMLY_CONFIG = { { name: "repeat-section-dnd", component: FormlyRepeatDndComponent }, { name: "ui-udf-parameters", component: UiUdfParametersComponent, wrappers: ["form-field"] }, ], - wrappers: [ - { name: "preset-wrapper", component: PresetWrapperComponent }, - { name: "collab-wrapper", component: CollabWrapperComponent }, - ], + wrappers: [{ name: "preset-wrapper", component: PresetWrapperComponent }], }; export function minItemsValidationMessage(err: any, field: FormlyFieldConfig) { 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 aba2811444c..fc2a5f7ce53 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 @@ -30,6 +30,13 @@ import { CustomJSONSchema7 } from "../../../types/custom-json-schema.interface"; import { OperatorMetadataService } from "../../../service/operator-metadata/operator-metadata.service"; import { StubOperatorMetadataService } from "../../../service/operator-metadata/stub-operator-metadata.service"; import { FORM_DEBOUNCE_TIME_MS } from "../../../service/execute-workflow/execute-workflow.service"; + +/** + * Drains the short timers scheduled while the fixture is being set up, before the form change + * under test. Unrelated to FORM_DEBOUNCE_TIME_MS, which is what the assertions below actually wait + * on; this previously borrowed COLLAB_DEBOUNCE_TIME_MS (also 10ms) from an unrelated component. + */ +const SETUP_FLUSH_MS = 10; import { DatePipe } from "@angular/common"; import { By } from "@angular/platform-browser"; import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; @@ -52,7 +59,6 @@ import { SimpleChange } from "@angular/core"; import { cloneDeep } from "lodash-es"; import Ajv from "ajv"; -import { COLLAB_DEBOUNCE_TIME_MS } from "../../../../common/formly/collab-wrapper/collab-wrapper/collab-wrapper.component"; import { FormlyNgZorroAntdModule } from "@ngx-formly/ng-zorro-antd"; import { ComputingUnitStatusService } from "../../../../common/service/computing-unit/computing-unit-status/computing-unit-status.service"; import { MockComputingUnitStatusService } from "../../../../common/service/computing-unit/computing-unit-status/mock-computing-unit-status.service"; @@ -182,7 +188,7 @@ describe("OperatorPropertyEditFrameComponent", () => { currentOperatorId: new SimpleChange(undefined, mockScanPredicate.operatorID, true), }); fixture.detectChanges(); - tick(COLLAB_DEBOUNCE_TIME_MS); + tick(SETUP_FLUSH_MS); // stimulate a form change by the user const formChangeValue = { tableName: "twitter_sample" }; 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 f4b356c4746..211cf52e485 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 @@ -1089,21 +1089,6 @@ export class OperatorPropertyEditFrameComponent implements OnInit, OnChanges, On ); } - // TODO: we temporarily disable this due to Yjs update causing issues in Formly. - - // if ( - // this.currentOperatorId !== undefined && - // ["string", "textarea"].includes(mappedField.type as string) && - // (mappedField.key as string) !== "password" - // ) { - // CollabWrapperComponent.setupFieldConfig( - // mappedField, - // this.workflowActionService.getTexeraGraph().getOperator(this.currentOperatorId).operatorType, - // this.currentOperatorId, - // mappedField.wrappers?.includes("preset-wrapper") - // ); - // } - if (this.currentOperatorSchema?.operatorType === "Projection" && mappedField.key === "attributes") { mappedField.type = "repeat-section-dnd"; mappedField.props = { From 60c641636c016ec3b23b9df20d1db1a1198c008d Mon Sep 17 00:00:00 2001 From: Xinyuan Lin Date: Thu, 6 Aug 2026 16:34:25 -0700 Subject: [PATCH 2/2] chore(frontend): move SETUP_FLUSH_MS below the import block The constant landed at the exact line the deleted COLLAB_DEBOUNCE_TIME_MS import used to occupy, which left it wedged between imports with thirty more import statements after it. Move it, with its comment, to just above the other module-level constant so the import block stays contiguous. Pure reorder: the only read is a tick() call inside a test body, far after the declaration, so there is no temporal-dead-zone concern and no behaviour change. Addresses the review comment on #7351. Co-Authored-By: Claude Opus 5 --- .../operator-property-edit-frame.component.spec.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 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 fc2a5f7ce53..b96023abb57 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 @@ -30,13 +30,6 @@ import { CustomJSONSchema7 } from "../../../types/custom-json-schema.interface"; import { OperatorMetadataService } from "../../../service/operator-metadata/operator-metadata.service"; import { StubOperatorMetadataService } from "../../../service/operator-metadata/stub-operator-metadata.service"; import { FORM_DEBOUNCE_TIME_MS } from "../../../service/execute-workflow/execute-workflow.service"; - -/** - * Drains the short timers scheduled while the fixture is being set up, before the form change - * under test. Unrelated to FORM_DEBOUNCE_TIME_MS, which is what the assertions below actually wait - * on; this previously borrowed COLLAB_DEBOUNCE_TIME_MS (also 10ms) from an unrelated component. - */ -const SETUP_FLUSH_MS = 10; import { DatePipe } from "@angular/common"; import { By } from "@angular/platform-browser"; import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; @@ -67,6 +60,13 @@ import { DynamicSchemaService } from "../../../service/dynamic-schema/dynamic-sc import { NotificationService } from "../../../../common/service/notification/notification.service"; import { WorkflowGraph } from "../../../service/workflow-graph/model/workflow-graph"; +/** + * Drains the short timers scheduled while the fixture is being set up, before the form change + * under test. Unrelated to FORM_DEBOUNCE_TIME_MS, which is what the assertions below actually wait + * on; this previously borrowed COLLAB_DEBOUNCE_TIME_MS (also 10ms) from an unrelated component. + */ +const SETUP_FLUSH_MS = 10; + const { marbles } = configure({ run: false }); describe("Aggregate attribute requirement", () => {