Skip to content

Commit 7070047

Browse files
feat(Authoring Tool): Drag and drop lessons and steps (#2288)
Co-authored-by: Jonathan Lim-Breitbart <breity10@gmail.com>
1 parent 57b99a4 commit 7070047

15 files changed

Lines changed: 462 additions & 195 deletions
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<div class="flex flex-row justify-start items-center gap-1">
22
<node-icon [nodeId]="nodeId" size="18" />&nbsp;
33
@if (showPosition) {
4-
<span class="step-number">{{ getNodePosition(nodeId) }}: </span>
4+
<span class="step-number">{{ nodePosition }}: </span>
55
}
6-
<span class="step-title">{{ getNodeTitle(nodeId) }}</span>
6+
<span class="step-title">{{ nodeTitle }}</span>
77
</div>

src/assets/wise5/authoringTool/choose-node-location/node-icon-and-title/node-icon-and-title.component.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Component, Input } from '@angular/core';
22
import { TeacherProjectService } from '../../../services/teacherProjectService';
33
import { NodeIconComponent } from '../../../vle/node-icon/node-icon.component';
44
import { TeacherProjectTranslationService } from '../../../services/teacherProjectTranslationService';
5+
import { Subscription } from 'rxjs';
56

67
@Component({
78
imports: [NodeIconComponent],
@@ -11,18 +12,34 @@ import { TeacherProjectTranslationService } from '../../../services/teacherProje
1112
})
1213
export class NodeIconAndTitleComponent {
1314
@Input() protected nodeId: string;
15+
protected nodePosition: string;
16+
protected nodeTitle: string;
1417
@Input() protected showPosition: boolean;
18+
private subscriptions: Subscription;
1519

1620
constructor(
1721
private projectService: TeacherProjectService,
1822
private projectTranslationService: TeacherProjectTranslationService
1923
) {}
2024

21-
protected getNodePosition(nodeId: string): string {
25+
ngOnInit(): void {
26+
this.nodePosition = this.getNodePosition(this.nodeId);
27+
this.nodeTitle = this.getNodeTitle(this.nodeId);
28+
this.subscriptions = this.projectService.projectParsed$.subscribe(() => {
29+
this.nodePosition = this.getNodePosition(this.nodeId);
30+
this.nodeTitle = this.getNodeTitle(this.nodeId);
31+
});
32+
}
33+
34+
ngOnDestroy(): void {
35+
this.subscriptions.unsubscribe();
36+
}
37+
38+
private getNodePosition(nodeId: string): string {
2239
return this.projectService.getNodePositionById(nodeId);
2340
}
2441

25-
protected getNodeTitle(nodeId: string): string {
42+
private getNodeTitle(nodeId: string): string {
2643
return this.projectService.isDefaultLocale()
2744
? this.projectService.getNodeTitle(nodeId)
2845
: this.translateNodeTitle(nodeId);

src/assets/wise5/authoringTool/project-authoring-lesson/project-authoring-lesson.component.html

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,34 @@
33
class="lesson flex grow"
44
(opened)="toggleExpanded(true)"
55
(closed)="toggleExpanded(false)"
6+
cdkDrag
7+
[cdkDragData]="{ type: 'group', id: lesson.id }"
8+
(cdkDragStarted)="drag(true)"
9+
(cdkDragEnded)="drag(false)"
10+
[id]="lesson.id"
611
>
712
<mat-expansion-panel-header aria-label="Expand/collapse lesson" i18n-aria-label>
813
<mat-panel-title>
9-
<mat-checkbox
10-
color="primary"
11-
(change)="selectNode($event.checked)"
12-
(click)="$event.stopPropagation()"
13-
(keydown)="$event.stopPropagation()"
14-
[disabled]="nodeTypeSelected() === 'step'"
15-
aria-label="Select lesson"
16-
i18n-aria-label
17-
/>
14+
@if (batchEditMode) {
15+
<mat-checkbox
16+
color="primary"
17+
(change)="selectNode($event.checked)"
18+
(click)="$event.stopPropagation()"
19+
(keydown)="$event.stopPropagation()"
20+
[disabled]="nodeTypeSelected() === 'step'"
21+
aria-label="Select lesson"
22+
i18n-aria-label
23+
/>
24+
} @else {
25+
<mat-icon
26+
cdkDragHandle
27+
class="cursor-move me-2"
28+
(click)="$event.stopPropagation()"
29+
title="Drag to reorder"
30+
i18n-title
31+
>drag_indicator</mat-icon
32+
>
33+
}
1834
<node-icon-and-title [nodeId]="lesson.id" [showPosition]="showPosition" />
1935
</mat-panel-title>
2036
<mat-panel-description class="text flex justify-end items-center gap-1">
@@ -51,21 +67,33 @@
5167
</button>
5268
</mat-panel-description>
5369
</mat-expansion-panel-header>
54-
<div>
70+
<div
71+
class="ps-6 flex flex-col gap-1"
72+
cdkDropList
73+
[id]="lesson.id"
74+
[cdkDropListData]="{ groupId: lesson.id, nodes: lesson.ids, idToNode: idToNode }"
75+
cdkDropListLockAxis="y"
76+
[cdkDropListConnectedTo]="allGroupIds"
77+
[cdkDropListSortPredicate]="notAfterBranchingNode"
78+
(cdkDropListDropped)="dropNode($event)"
79+
>
5580
@for (childId of lesson.ids; track childId) {
5681
<div class="flex flex-row flex-wrap justify-start items-center">
5782
<project-authoring-step
83+
[batchEditMode]="batchEditMode"
5884
[step]="idToNode[childId]"
5985
(selectNodeEvent)="selectNodeEvent.emit($event)"
6086
[showPosition]="showPosition"
6187
[projectId]="projectId"
62-
class="flex grow"
88+
class="grow"
89+
(dragStarted)="drag(true)"
90+
(dragEnded)="drag(false)"
6391
/>
64-
<add-step-button [nodeId]="childId" />
92+
<add-step-button [nodeId]="childId" [class.hidden]="isDragging()" />
6593
</div>
6694
}
6795
@if (lesson.ids.length === 0) {
68-
<div class="no-steps-message flex justify-start items-center">
96+
<div class="px-2 flex justify-start items-center">
6997
<div i18n>This lesson has no steps</div>
7098
<button
7199
mat-icon-button
@@ -80,4 +108,5 @@
80108
</div>
81109
}
82110
</div>
111+
<div class="drag-placeholder" *cdkDragPlaceholder></div>
83112
</mat-expansion-panel>

src/assets/wise5/authoringTool/project-authoring-lesson/project-authoring-lesson.component.scss

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
@reference "tailwindcss";
2+
13
.lesson {
24
.mat-expansion-panel-header {
35
padding-inline-start: 8px;
@@ -19,8 +21,22 @@
1921
.full-width {
2022
width: 100%;
2123
}
24+
}
25+
26+
.drag-placeholder {
27+
@apply rounded border-2 border-dashed border-gray-300 bg-gray-100 flex grow;
28+
min-height: 48px;
29+
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
30+
}
31+
32+
.branch-path-step {
33+
@apply ps-6;
34+
}
35+
36+
.cdk-drag-preview {
37+
height: auto !important;
2238

23-
.no-steps-message {
24-
padding: 0 8px;
39+
.mat-expansion-panel-content-wrapper {
40+
display: none;
2541
}
2642
}

src/assets/wise5/authoringTool/project-authoring-lesson/project-authoring-lesson.component.spec.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import { CopyTranslationsService } from '../../services/copyTranslationsService'
1616
import { ConstraintService } from '../../services/constraintService';
1717
import { Node } from '../../common/Node';
1818
import { NodeIconAndTitleComponent } from '../choose-node-location/node-icon-and-title/node-icon-and-title.component';
19+
import { MoveNodesService } from '../../services/moveNodesService';
20+
import { of } from 'rxjs';
1921

2022
let component: ProjectAuthoringLessonComponent;
2123
let fixture: ComponentFixture<ProjectAuthoringLessonComponent>;
@@ -49,8 +51,12 @@ describe('ProjectAuthoringLessonComponent', () => {
4951
TeacherDataService,
5052
TeacherProjectTranslationService
5153
),
54+
MockProvider(MoveNodesService, {
55+
getIsDragging: () => signal<boolean>(false)
56+
}),
5257
MockProvider(TeacherProjectService, {
53-
getNodeTypeSelected: () => signal<NodeTypeSelected>(NodeTypeSelected.lesson)
58+
getNodeTypeSelected: () => signal<NodeTypeSelected>(NodeTypeSelected.lesson),
59+
projectParsed$: of()
5460
}),
5561
provideRouter([])
5662
]

src/assets/wise5/authoringTool/project-authoring-lesson/project-authoring-lesson.component.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
11
import { Component, EventEmitter, Input, Output, Signal, ViewEncapsulation } from '@angular/core';
2+
import {
3+
CdkDrag,
4+
CdkDragDrop,
5+
CdkDropList,
6+
DragDropModule,
7+
moveItemInArray
8+
} from '@angular/cdk/drag-drop';
29
import { MatExpansionModule } from '@angular/material/expansion';
310
import { MatCheckboxModule } from '@angular/material/checkbox';
411
import { MatIconModule } from '@angular/material/icon';
@@ -17,11 +24,13 @@ import { DeleteNodeService } from '../../services/deleteNodeService';
1724
import { ActivatedRoute, Router } from '@angular/router';
1825
import { DeleteTranslationsService } from '../../services/deleteTranslationsService';
1926
import { AddStepTarget } from '../../../../app/domain/addStepTarget';
27+
import { MoveNodesService } from '../../services/moveNodesService';
2028

2129
@Component({
2230
encapsulation: ViewEncapsulation.None,
2331
imports: [
2432
FormsModule,
33+
DragDropModule,
2534
MatExpansionModule,
2635
MatCheckboxModule,
2736
MatIconModule,
@@ -36,9 +45,12 @@ import { AddStepTarget } from '../../../../app/domain/addStepTarget';
3645
templateUrl: './project-authoring-lesson.component.html'
3746
})
3847
export class ProjectAuthoringLessonComponent {
48+
allGroupIds: string[];
49+
@Input() batchEditMode: boolean;
3950
@Input() expanded: boolean = true;
4051
@Output() onExpandedChanged: EventEmitter<ExpandEvent> = new EventEmitter<ExpandEvent>();
4152
protected idToNode: any = {};
53+
protected isDragging: Signal<boolean>;
4254
@Input() lesson: any;
4355
protected nodeTypeSelected: Signal<NodeTypeSelected>;
4456
@Input() projectId: number;
@@ -49,14 +61,17 @@ export class ProjectAuthoringLessonComponent {
4961
private dataService: TeacherDataService,
5062
private deleteNodeService: DeleteNodeService,
5163
private deleteTranslationsService: DeleteTranslationsService,
64+
private moveNodesService: MoveNodesService,
5265
private projectService: TeacherProjectService,
5366
private route: ActivatedRoute,
5467
private router: Router
5568
) {}
5669

5770
ngOnInit(): void {
71+
this.allGroupIds = this.projectService.getAllGroupIds();
5872
this.idToNode = this.projectService.idToNode;
5973
this.nodeTypeSelected = this.projectService.getNodeTypeSelected();
74+
this.isDragging = this.moveNodesService.getIsDragging();
6075
}
6176

6277
protected selectNode(checked: boolean): void {
@@ -100,4 +115,38 @@ export class ProjectAuthoringLessonComponent {
100115
this.projectService.saveProject();
101116
this.projectService.refreshProject();
102117
}
118+
119+
protected dropNode(event: CdkDragDrop<any>): void {
120+
const { container, currentIndex, item, previousContainer, previousIndex } = event;
121+
if (previousContainer === container) {
122+
moveItemInArray(container.data.nodes, previousIndex, currentIndex);
123+
} else {
124+
// do nothing. the UI will be updated by moveNodesAfter() and refreshProject() calls
125+
}
126+
if (currentIndex == 0) {
127+
this.moveNodesService.moveNodesInsideGroup([item.data.id], container.data.groupId);
128+
} else {
129+
this.moveNodesService.moveNodesAfter([item.data.id], container.data.nodes[currentIndex - 1]);
130+
}
131+
this.projectService.checkPotentialStartNodeIdChangeThenSaveProject().then(() => {
132+
this.projectService.refreshProject();
133+
});
134+
}
135+
136+
// allow a step to drop anywhere except the first step in a first path of a branch activity
137+
// otherwise, the step will be placed after a node that has multiple transitions, which is not allowed
138+
protected notAfterBranchingNode(
139+
index: number,
140+
item: CdkDrag<any>,
141+
drop: CdkDropList<any>
142+
): boolean {
143+
if (index === 0) return true;
144+
const nodesExceptItem = drop.data.nodes.filter((nodeId) => nodeId !== item.data.id);
145+
const nodeBefore = drop.data.idToNode[nodesExceptItem[index - 1]];
146+
return nodeBefore.transitionLogic.transitions.length <= 1;
147+
}
148+
149+
protected drag(isDragging: boolean): void {
150+
this.moveNodesService.setIsDragging(isDragging);
151+
}
103152
}

src/assets/wise5/authoringTool/project-authoring-step/project-authoring-step.component.html

Lines changed: 38 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,43 @@
11
<div
22
id="{{ step.id }}"
33
class="step flex justify-start items-center gap-2 grow"
4-
[ngClass]="{ 'branch-path-step': isNodeInAnyBranchPath(step.id) }"
54
[ngStyle]="{ 'background-color': getStepBackgroundColor(step.id) }"
65
(click)="setCurrentNode(step.id)"
76
(keyup.enter)="setCurrentNode(step.id)"
87
role="button"
98
tabindex="0"
9+
cdkDrag
10+
[cdkDragDisabled]="branchPoint"
11+
[cdkDragData]="{ type: 'step', id: step.id }"
12+
(cdkDragStarted)="drag(true)"
13+
(cdkDragEnded)="drag(false)"
1014
aria-label="Edit step"
1115
i18n-aria-label
1216
>
13-
<mat-checkbox
14-
color="primary"
15-
(change)="selectNode($event.checked)"
16-
(click)="$event.stopPropagation()"
17-
[disabled]="nodeTypeSelected() === 'lesson'"
18-
aria-label="Select step"
19-
i18n-aria-label
20-
/>
17+
@if (batchEditMode) {
18+
<mat-checkbox
19+
color="primary"
20+
(change)="selectNode($event.checked)"
21+
(click)="$event.stopPropagation()"
22+
[disabled]="nodeTypeSelected() === 'lesson'"
23+
aria-label="Select step"
24+
i18n-aria-label
25+
/>
26+
} @else if (!branchPoint) {
27+
<mat-icon
28+
cdkDragHandle
29+
class="cursor-move"
30+
(click)="$event.stopPropagation()"
31+
title="Drag to reorder"
32+
i18n-title
33+
>drag_indicator</mat-icon
34+
>
35+
}
2136
<strong>
2237
<node-icon-and-title [nodeId]="step.id" [showPosition]="showPosition" />
2338
</strong>
2439
<div class="flex justify-start items-center gap-2">
25-
@if (isBranchPoint(step.id)) {
40+
@if (branchPoint) {
2641
<button
2742
mat-icon-button
2843
(click)="goToEditBranch(step.id); $event.stopPropagation()"
@@ -64,18 +79,19 @@
6479
matTooltipPosition="above"
6580
i18n-matTooltip
6681
></div>
67-
<!-- <div class="dynamic-step-buttons"> -->
6882
<div class="flex justify-start items-center">
69-
<button
70-
class="step-action"
71-
mat-icon-button
72-
(click)="move(); $event.stopPropagation()"
73-
matTooltip="Move step"
74-
matTooltipPosition="above"
75-
i18n-matTooltip
76-
>
77-
<mat-icon>redo</mat-icon>
78-
</button>
83+
@if (!branchPoint) {
84+
<button
85+
class="step-action"
86+
mat-icon-button
87+
(click)="move(); $event.stopPropagation()"
88+
matTooltip="Move step"
89+
matTooltipPosition="above"
90+
i18n-matTooltip
91+
>
92+
<mat-icon>redo</mat-icon>
93+
</button>
94+
}
7995
<button
8096
class="step-action"
8197
mat-icon-button
@@ -97,5 +113,5 @@
97113
<mat-icon>delete</mat-icon>
98114
</button>
99115
</div>
100-
<!-- </div> -->
116+
<div class="drag-placeholder" *cdkDragPlaceholder></div>
101117
</div>

0 commit comments

Comments
 (0)