Skip to content

Commit

Permalink
Add Workflow Comments (#1417)
Browse files Browse the repository at this point in the history
Original PR #1293
-Updating Comments related stuff to the latest master configs and environment

## Introduction

This PR includes the basic features of the Workflow Commenting feature of Texera--a feature implemented to support communication between contributors of a workflow and comprehension of people reading a workflow.

Early Stage :
- Now can add `commentBox` objects onto the canvas
- a pop-up window will be displayed when double-clicking the comment icon
- delete button on the top right corner which allows you to delete the commentBox
- after opening the pop-up window, you can edit comments by typing in the input box.
- the input box stores newly added comments and previous comments in a list.
![Comments-Front-End](https://user-images.githubusercontent.com/72828385/156052516-c626c8c1-efb1-49c6-a2a1-9a9da3437f16.gif)

## Lifecycle of Comments
A comment (the little dialog icon on canvas) consists of two components: a **CommentBox** and **a list of Comments**. 
**CommentBox** is generated when the dialog icon pops up, it is a container object with information of an id as identifier and a point representing its position on the canvas, and an empty list of **Comments**.
**Comment** is generated and added to the list in **CommentBox** when someone types a comment and hits the send button. it contains information related to its creator, its content, and its creation time.
Refer to `workflow-common.interface.ts` for more details.
When the **Delete** button on the top-right corner of the icon is clicked, the commentBox gets deleted as well as all its corresponding comments.

## Storage of Comments
Currently, CommentBoxes are components of a workflow (parallel with Links and Operators), it is persisted into the backend database as an attribute in the JSON object with every information embedded in -- position, ID, and all the comments inside. @Yicong-Huang, @zuozhiw and I have been discussing whether we should seperate comments from commentBoxes since  the current method lacks the ability to handle detailed requests related to specific comments.

## 2022/02/26 Update
- Updated to work with the share-editing feature.
- Optimized front-end CommentBox UI for user-friendly purposes.

Co-authored-by: Yicong Huang <[email protected]>
  • Loading branch information
Jiyang-Wu and Yicong-Huang authored Mar 2, 2022
1 parent c2f1dd3 commit 0edbbda
Show file tree
Hide file tree
Showing 21 changed files with 559 additions and 20 deletions.
5 changes: 5 additions & 0 deletions core/new-gui/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,8 @@ import { NgbdModalRemoveProjectWorkflowComponent } from "./dashboard/component/f
import { NgbdModalAddProjectFileComponent } from "./dashboard/component/feature-container/user-project-list/user-project-section/ngbd-modal-add-project-file/ngbd-modal-add-project-file.component";
import { NgbdModalRemoveProjectFileComponent } from "./dashboard/component/feature-container/user-project-list/user-project-section/ngbd-modal-remove-project-file/ngbd-modal-remove-project-file.component";
import { PresetWrapperComponent } from "./common/formly/preset-wrapper/preset-wrapper.component";
import { NzModalCommentBoxComponent } from "./workspace/component/workflow-editor/comment-box-modal/nz-modal-comment-box.component";
import { NzCommentModule } from "ng-zorro-antd/comment";
import { NgbdModalWorkflowExecutionsComponent } from "./dashboard/component/feature-container/saved-workflow-section/ngbd-modal-workflow-executions/ngbd-modal-workflow-executions.component";

registerLocaleData(en);
Expand Down Expand Up @@ -178,6 +180,7 @@ registerLocaleData(en);
NgbdModalRemoveProjectWorkflowComponent,
NgbdModalAddProjectFileComponent,
NgbdModalRemoveProjectFileComponent,
NzModalCommentBoxComponent,
],
imports: [
BrowserModule,
Expand Down Expand Up @@ -243,6 +246,7 @@ registerLocaleData(en);
NzTabsModule,
NzTreeViewModule,
NzPaginationModule,
NzCommentModule,
],
entryComponents: [
NgbdModalAddProjectWorkflowComponent,
Expand All @@ -258,6 +262,7 @@ registerLocaleData(en);
RowModalComponent,
NgbdModalFileAddComponent,
NgbdModalWorkflowShareAccessComponent,
NzModalCommentBoxComponent,
NgbdModalWorkflowExecutionsComponent,
],
providers: [
Expand Down
9 changes: 8 additions & 1 deletion core/new-gui/src/app/common/type/workflow.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { WorkflowMetadata } from "../../dashboard/type/workflow-metadata.interface";
import { PlainGroup } from "../../workspace/service/workflow-graph/model/operator-group";
import { Breakpoint, OperatorLink, OperatorPredicate, Point } from "../../workspace/types/workflow-common.interface";
import {
Breakpoint,
OperatorLink,
OperatorPredicate,
Point,
CommentBox,
} from "../../workspace/types/workflow-common.interface";

/**
* WorkflowContent is used to store the information of the workflow
Expand All @@ -21,6 +27,7 @@ export interface WorkflowContent
links: OperatorLink[];
groups: PlainGroup[];
breakpoints: Record<string, Breakpoint>;
commentBoxes: CommentBox[];
}> {}

export type Workflow = { content: WorkflowContent } & WorkflowMetadata;
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@
<button (click)="onClickRestoreZoomOffsetDefault()" nz-button title="reset zoom">
<i nz-icon nzTheme="outline" nzType="fullscreen"></i>
</button>
<button (click)="onClickAddCommentBox()" nz-button title="add a comment">
<i nz-icon nzType="comment" nzTheme="outline"></i>
</button>
<button
[disabled]="
!workflowResultExportService.exportExecutionResultEnabled ||
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { merge } from "rxjs";
import { WorkflowResultExportService } from "../../service/workflow-result-export/workflow-result-export.service";
import { debounceTime } from "rxjs/operators";
import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
import { WorkflowUtilService } from "../../service/workflow-graph/util/workflow-util.service";
import { isSink } from "../../service/workflow-graph/model/workflow-graph";
import { WorkflowVersionService } from "../../../dashboard/service/workflow-version/workflow-version.service";
import { WorkflowCollabService } from "../../service/workflow-collab/workflow-collab.service";
Expand Down Expand Up @@ -89,7 +90,8 @@ export class NavigationComponent {
private workflowCacheService: WorkflowCacheService,
private datePipe: DatePipe,
public workflowResultExportService: WorkflowResultExportService,
public workflowCollabService: WorkflowCollabService
public workflowCollabService: WorkflowCollabService,
public workflowUtilService: WorkflowUtilService
) {
this.executionState = executeWorkflowService.getExecutionState().state;
// return the run button after the execution is finished, either
Expand Down Expand Up @@ -207,6 +209,10 @@ export class NavigationComponent {
}
}

public onClickAddCommentBox(): void {
this.workflowActionService.addCommentBox(this.workflowUtilService.getNewCommentBox());
}

public handleKill(): void {
this.executeWorkflowService.killWorkflow();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ export class PropertyEditorComponent implements OnInit {
this.workflowActionService.getJointGraphWrapper().getJointGroupUnhighlightStream(),
this.workflowActionService.getJointGraphWrapper().getLinkHighlightStream(),
this.workflowActionService.getJointGraphWrapper().getLinkUnhighlightStream(),
this.workflowActionService.getJointGraphWrapper().getJointCommentBoxHighlightStream(),
this.workflowActionService.getJointGraphWrapper().getJointCommentBoxUnhighlightStream(),
this.workflowVersionService.workflowVersionsDisplayObservable()
)
.pipe(untilDestroyed(this))
Expand All @@ -79,6 +81,9 @@ export class PropertyEditorComponent implements OnInit {
.getCurrentHighlightedOperatorIDs();
const highlightedGroups = this.workflowActionService.getJointGraphWrapper().getCurrentHighlightedGroupIDs();
const highlightLinks = this.workflowActionService.getJointGraphWrapper().getCurrentHighlightedLinkIDs();
const highlightCommentBoxes = this.workflowActionService
.getJointGraphWrapper()
.getCurrentHighlightedCommentBoxIDs();

if (isDisplayWorkflowVersions) {
this.switchFrameComponent({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<div class="modal-body">
<nz-list [nzItemLayout]="'horizontal'">
<nz-list-item *ngFor="let item of commentBox.comments">
<nz-comment [nzAuthor]="item.creatorName" [nzDatetime]="toRelative(item.creationTime)">
<!-- TODO: add user avatar-->
<!-- <nz-avatar nz-comment-avatar nzIcon="user" [nzSrc]="item.avatar"></nz-avatar>-->
<nz-comment-content>
<p>{{ item.content }}</p>
</nz-comment-content>
</nz-comment>
</nz-list-item>
</nz-list>
</div>

<div class="modal-footer">
<!-- TODO: add user avatar-->
<!-- <nz-avatar nz-comment-avatar nzIcon="user" [nzSrc]="user.avatar"></nz-avatar>-->
<nz-input-group nzSearch [nzAddOnAfter]="suffixIconButton">
<textarea
type="text"
placeholder="add new comment"
[(ngModel)]="inputValue"
nz-input
[nzAutosize]="{ minRows: 1, maxRows: 6}"
></textarea>
</nz-input-group>
<ng-template #suffixIconButton>
<button
nz-button
nzType="primary"
[nzLoading]="submitting"
[disabled]="!user || !inputValue"
(click)="onClickAddComment()"
>
<i nz-icon nzType="send"></i>
</button>
</ng-template>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
.modal-body {
min-height: 20vh;
max-height: 60vh;
overflow-y: auto;
width: 100%;

p {
word-break: break-all;
white-space: normal;
}
}

.modal-dialog {
overflow-y: initial !important;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { Component, HostListener, Inject, Input, LOCALE_ID } from "@angular/core";
import { NzModalRef } from "ng-zorro-antd/modal";
import { CommentBox } from "src/app/workspace/types/workflow-common.interface";
import { WorkflowActionService } from "src/app/workspace/service/workflow-graph/model/workflow-action.service";
import { UserService } from "src/app/common/service/user/user.service";
import { NotificationService } from "../../../../common/service/notification/notification.service";
import { User } from "src/app/common/type/user";
import { untilDestroyed } from "@ngneat/until-destroy";
import { UntilDestroy } from "@ngneat/until-destroy";
import { formatDate } from "@angular/common";

@UntilDestroy()
@Component({
selector: "texera-nz-modal-comment-box",
templateUrl: "./nz-modal-comment-box.component.html",
styleUrls: ["./nz-modal-comment-box.component.scss"],
})
export class NzModalCommentBoxComponent {
@Input() commentBox!: CommentBox;
public user?: User;

constructor(
@Inject(LOCALE_ID) public locale: string,
public workflowActionService: WorkflowActionService,
public userService: UserService,
public modal: NzModalRef<any, number>,
public notificationService: NotificationService
) {
this.userService
.userChanged()
.pipe(untilDestroyed(this))
.subscribe(user => (this.user = user));
}

inputValue = "";
submitting = false;

public onClickAddComment(): void {
this.submitting = true;
this.addComment(this.inputValue);
this.inputValue = "";
this.submitting = false;
}

public addComment(content: string): void {
if (!this.user) {
return;
}
// A compromise: we create the timestamp in the frontend since the entire comment is managed together, in JSON format
const creationTime: string = new Date().toISOString();
const creatorName = this.user.name;
const creatorID = this.user.uid;
this.workflowActionService.addComment(
{ content, creatorName, creatorID, creationTime },
this.commentBox.commentBoxID
);
}

toRelative(datetime: string): string {
return formatDate(new Date(datetime), "MM/dd/yyyy, hh:mm:ss a z", this.locale);
}

@HostListener("window:keydown", ["$event"])
onKeyDown(event: KeyboardEvent) {
if ((event.metaKey || event.ctrlKey) && event.key == "Enter") {
this.onClickAddComment();
event.preventDefault();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,18 @@ import { WorkflowUtilService } from "../../service/workflow-graph/util/workflow-
import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing";
import { ValidationWorkflowService } from "../../service/validation/validation-workflow.service";
import { WorkflowEditorComponent } from "./workflow-editor.component";
import { NzModalCommentBoxComponent } from "./comment-box-modal/nz-modal-comment-box.component";
import { OperatorMetadataService } from "../../service/operator-metadata/operator-metadata.service";
import { StubOperatorMetadataService } from "../../service/operator-metadata/stub-operator-metadata.service";
import { JointUIService } from "../../service/joint-ui/joint-ui.service";
import { NzModalModule, NzModalService } from "ng-zorro-antd/modal";
import { Overlay } from "@angular/cdk/overlay";
import * as jQuery from "jquery";
import * as joint from "jointjs";
import { ResultPanelToggleService } from "../../service/result-panel-toggle/result-panel-toggle.service";
import { marbles } from "rxjs-marbles";
import {
mockCommentBox,
mockPoint,
mockResultPredicate,
mockScanPredicate,
Expand All @@ -24,6 +28,7 @@ import { WorkflowStatusService } from "../../service/workflow-status/workflow-st
import { ExecuteWorkflowService } from "../../service/execute-workflow/execute-workflow.service";
import { HttpClientTestingModule } from "@angular/common/http/testing";
import { OperatorLink, OperatorPredicate } from "../../types/workflow-common.interface";
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
import { tap } from "rxjs/operators";

describe("WorkflowEditorComponent", () => {
Expand All @@ -41,7 +46,7 @@ describe("WorkflowEditorComponent", () => {
waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [WorkflowEditorComponent],
imports: [HttpClientTestingModule],
imports: [HttpClientTestingModule, NzModalModule],
providers: [
JointUIService,
WorkflowUtilService,
Expand All @@ -50,6 +55,7 @@ describe("WorkflowEditorComponent", () => {
ResultPanelToggleService,
ValidationWorkflowService,
WorkflowActionService,
Overlay,
{
provide: OperatorMetadataService,
useClass: StubOperatorMetadataService,
Expand Down Expand Up @@ -132,12 +138,13 @@ describe("WorkflowEditorComponent", () => {
let validationWorkflowService: ValidationWorkflowService;
let dragDropService: DragDropService;
let jointUIService: JointUIService;
let nzModalService: NzModalService;

beforeEach(
waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [WorkflowEditorComponent],
imports: [HttpClientTestingModule],
declarations: [WorkflowEditorComponent, NzModalCommentBoxComponent],
imports: [HttpClientTestingModule, NzModalModule, NoopAnimationsModule],
providers: [
JointUIService,
WorkflowUtilService,
Expand All @@ -146,6 +153,7 @@ describe("WorkflowEditorComponent", () => {
ResultPanelToggleService,
ValidationWorkflowService,
DragDropService,
NzModalService,
{
provide: OperatorMetadataService,
useClass: StubOperatorMetadataService,
Expand All @@ -165,6 +173,7 @@ describe("WorkflowEditorComponent", () => {
dragDropService = TestBed.inject(DragDropService);
// detect changes to run ngAfterViewInit and bind Model
jointUIService = TestBed.inject(JointUIService);
nzModalService = TestBed.inject(NzModalService);
fixture.detectChanges();
});

Expand Down Expand Up @@ -197,6 +206,44 @@ describe("WorkflowEditorComponent", () => {
expect(jointGraphWrapper.getCurrentHighlightedOperatorIDs()).toEqual([mockScanPredicate.operatorID]);
});

it("should highlight the commentBox when user double clicks on a commentBox", () => {
const jointGraphWrapper = workflowActionService.getJointGraphWrapper();
const highlightCommentBoxFunctionSpy = spyOn(jointGraphWrapper, "highlightCommentBoxes").and.callThrough();
workflowActionService.addCommentBox(mockCommentBox);
jointGraphWrapper.unhighlightCommentBoxes(mockCommentBox.commentBoxID);
const jointCellView = component.getJointPaper().findViewByModel(mockCommentBox.commentBoxID);
jointCellView.$el.trigger("dblclick");
fixture.detectChanges();
expect(jointGraphWrapper.getCurrentHighlightedCommentBoxIDs()).toEqual([mockCommentBox.commentBoxID]);
});

it("should open commentBox as NzModal", () => {
// const modalRef:NzModalRef = nzModalService.create({
// nzTitle: "CommentBox",
// nzContent: NzModalCommentBoxComponent,
// nzComponentParams: {
// commentBox: mockCommentBox,
// },
// nzAutofocus: null,
// nzFooter: [
// {
// label: "OK",
// onClick: () => {
// modalRef.destroy();
// },
// type: "primary",
// },
// ],
// });
spyOn(nzModalService, "create");
const jointGraphWrapper = workflowActionService.getJointGraphWrapper();
workflowActionService.addCommentBox(mockCommentBox);
jointGraphWrapper.highlightCommentBoxes(mockCommentBox.commentBoxID);
expect(nzModalService.create).toHaveBeenCalled();
fixture.detectChanges();
// modalRef.destroy();
});

it("should unhighlight all highlighted operators when user mouse clicks on the blank space", () => {
const jointGraphWrapper = workflowActionService.getJointGraphWrapper();

Expand Down
Loading

0 comments on commit 0edbbda

Please sign in to comment.