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..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 @@ -52,7 +52,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"; @@ -61,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", () => { @@ -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 = {