-
Notifications
You must be signed in to change notification settings - Fork 303
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
General: Add course exercise overview grouped by due date (timeframe …
…view) (#7363)
- Loading branch information
1 parent
6fa10af
commit eca9020
Showing
16 changed files
with
462 additions
and
95 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
38 changes: 38 additions & 0 deletions
38
.../webapp/app/overview/course-exercises/course-exercises-grouped-by-category.component.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
<div *ngIf="!filteredExercises?.length; else exercisesWithAppliedSearchAndFilters"> | ||
{{ 'artemisApp.courseOverview.exerciseList.noExerciseMatchesSearchAndFilters' | artemisTranslate }} | ||
</div> | ||
<ng-template #exercisesWithAppliedSearchAndFilters> | ||
<div class="exercise-row-container mb-3" *ngFor="let exerciseGroupKey of Object.keys(exerciseGroups)" (click)="toggleGroupCategoryCollapse(exerciseGroupKey)"> | ||
<ng-container *ngIf="exerciseGroups[exerciseGroupKey].exercises.length"> | ||
<div class="control-label d-flex align-items-center"> | ||
<div class="icon-container pe-3"> | ||
<fa-icon | ||
[icon]="faChevronRight" | ||
class="rotate-icon chevron-position" | ||
[class.rotated]="!exerciseGroups[exerciseGroupKey].isCollapsed" | ||
[class.text-primary]="exerciseGroupKey === 'current'" | ||
></fa-icon> | ||
</div> | ||
|
||
<ng-container [ngSwitch]="exerciseGroupKey"> | ||
<h3 *ngSwitchCase="'past'" class="mb-0">{{ 'artemisApp.courseOverview.exerciseList.past' | artemisTranslate }}</h3> | ||
<h3 *ngSwitchCase="'current'" class="text-primary mb-0">{{ 'artemisApp.courseOverview.exerciseList.current' | artemisTranslate }}</h3> | ||
<h3 *ngSwitchCase="'future'" class="mb-0">{{ 'artemisApp.courseOverview.exerciseList.future' | artemisTranslate }}</h3> | ||
<h3 *ngSwitchCase="'noDueDate'" class="mb-0">{{ 'artemisApp.courseOverview.exerciseList.noDueDate' | artemisTranslate }}</h3> | ||
</ng-container> | ||
</div> | ||
|
||
<div [ngbCollapse]="exerciseGroups[exerciseGroupKey].isCollapsed"> | ||
<jhi-course-exercise-row | ||
class="pb-1" | ||
[exercise]="exercise" | ||
[course]="course!" | ||
[hasGuidedTour]="exercise === exerciseForGuidedTour" | ||
*ngFor="let exercise of exerciseGroups[exerciseGroupKey].exercises" | ||
/> | ||
</div> | ||
|
||
<div class="collapsed"></div> | ||
</ng-container> | ||
</div> | ||
</ng-template> |
167 changes: 167 additions & 0 deletions
167
...in/webapp/app/overview/course-exercises/course-exercises-grouped-by-category.component.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
import { Component, Input, OnChanges } from '@angular/core'; | ||
import { Exercise } from 'app/entities/exercise.model'; | ||
import { Course } from 'app/entities/course.model'; | ||
import dayjs from 'dayjs/esm/'; | ||
import { faAngleDown, faAngleUp, faChevronRight } from '@fortawesome/free-solid-svg-icons'; | ||
import { cloneDeep } from 'lodash-es'; | ||
|
||
type ExerciseGroupCategory = 'past' | 'current' | 'future' | 'noDueDate'; | ||
|
||
/** | ||
* {@link ExerciseGroupCategory#past} is always collapsed by default | ||
*/ | ||
const DEFAULT_EXPAND_ORDER: ExerciseGroupCategory[] = ['current', 'future', 'noDueDate']; | ||
|
||
type ExerciseGroups = Record<ExerciseGroupCategory, { exercises: Exercise[]; isCollapsed: boolean }>; | ||
|
||
const DEFAULT_EXERCISE_GROUPS = { | ||
past: { exercises: [], isCollapsed: true }, | ||
current: { exercises: [], isCollapsed: false }, | ||
future: { exercises: [], isCollapsed: false }, | ||
noDueDate: { exercises: [], isCollapsed: true }, | ||
}; | ||
|
||
@Component({ | ||
selector: 'jhi-course-exercises-grouped-by-category', | ||
templateUrl: './course-exercises-grouped-by-category.component.html', | ||
styleUrls: ['../course-overview.scss'], | ||
}) | ||
export class CourseExercisesGroupedByCategoryComponent implements OnChanges { | ||
protected readonly Object = Object; | ||
|
||
@Input() filteredExercises?: Exercise[]; | ||
@Input() course?: Course; | ||
@Input() exerciseForGuidedTour?: Exercise; | ||
@Input() appliedSearchString?: string; | ||
|
||
exerciseGroups: ExerciseGroups; | ||
|
||
searchWasActive: boolean = false; | ||
exerciseGroupsBeforeSearch: ExerciseGroups = cloneDeep(DEFAULT_EXERCISE_GROUPS); | ||
|
||
faAngleUp = faAngleUp; | ||
faAngleDown = faAngleDown; | ||
faChevronRight = faChevronRight; | ||
|
||
ngOnChanges() { | ||
this.exerciseGroups = this.groupExercisesByDueDate(); | ||
} | ||
|
||
toggleGroupCategoryCollapse(exerciseGroupCategoryKey: string) { | ||
this.exerciseGroups[exerciseGroupCategoryKey].isCollapsed = !this.exerciseGroups[exerciseGroupCategoryKey].isCollapsed; | ||
} | ||
|
||
private groupExercisesByDueDate(): ExerciseGroups { | ||
const updatedExerciseGroups: ExerciseGroups = cloneDeep(DEFAULT_EXERCISE_GROUPS); | ||
|
||
if (!this.filteredExercises) { | ||
return updatedExerciseGroups; | ||
} | ||
|
||
for (const exercise of this.filteredExercises) { | ||
const exerciseGroup = this.getExerciseGroup(exercise); | ||
updatedExerciseGroups[exerciseGroup].exercises.push(exercise); | ||
} | ||
|
||
this.adjustExpandedOrCollapsedStateOfExerciseGroups(updatedExerciseGroups); | ||
|
||
return updatedExerciseGroups; | ||
} | ||
|
||
private expandAllExercisesAndSaveStateBeforeSearch(exerciseGroups: ExerciseGroups) { | ||
const isAConsecutiveSearchWithAllGroupsExpanded = this.searchWasActive; | ||
if (!isAConsecutiveSearchWithAllGroupsExpanded) { | ||
this.exerciseGroupsBeforeSearch = cloneDeep(this.exerciseGroups); | ||
this.searchWasActive = true; | ||
} | ||
|
||
Object.entries(exerciseGroups).forEach(([, exerciseGroup]) => { | ||
exerciseGroup.isCollapsed = false; | ||
}); | ||
} | ||
|
||
private restoreStateBeforeSearch(exerciseGroups: ExerciseGroups) { | ||
this.searchWasActive = false; | ||
|
||
Object.entries(exerciseGroups).forEach(([exerciseGroupKey, exerciseGroup]) => { | ||
exerciseGroup.isCollapsed = this.exerciseGroupsBeforeSearch[exerciseGroupKey].isCollapsed; | ||
}); | ||
} | ||
|
||
private keepCurrentCollapsedOrExpandedStateOfExerciseGroups(exerciseGroups: ExerciseGroups) { | ||
Object.entries(exerciseGroups).forEach(([exerciseGroupKey, exerciseGroup]) => { | ||
exerciseGroup.isCollapsed = this.exerciseGroups[exerciseGroupKey].isCollapsed; | ||
}); | ||
} | ||
|
||
/** | ||
* Expand at least one exercise group, considering that {@link ExerciseGroupCategory#past} shall never be expanded by default | ||
* | ||
* Expanded by the order {@link ExerciseGroupCategory#current}, {@link ExerciseGroupCategory#future}, {@link ExerciseGroupCategory#noDueDate} | ||
*/ | ||
private makeSureAtLeastOneExerciseGroupIsExpanded(exerciseGroups: ExerciseGroups) { | ||
const exerciseGroupsWithExercises = Object.entries(exerciseGroups).filter(([, exerciseGroup]) => exerciseGroup.exercises.length > 0); | ||
const expandedExerciseGroups = exerciseGroupsWithExercises.filter(([exerciseGroupKey, exerciseGroup]) => !exerciseGroup.isCollapsed && exerciseGroupKey !== 'past'); | ||
|
||
const atLeastOneExerciseIsExpanded = expandedExerciseGroups.length > 0; | ||
const expandableGroupsExist = !atLeastOneExerciseIsExpanded && exerciseGroupsWithExercises.length > 0; | ||
|
||
if (!expandableGroupsExist) { | ||
return; | ||
} | ||
|
||
for (const exerciseGroupKey of DEFAULT_EXPAND_ORDER) { | ||
const groupToExpand = exerciseGroupsWithExercises.find(([key]) => key === exerciseGroupKey); | ||
if (groupToExpand) { | ||
groupToExpand![1].isCollapsed = false; | ||
break; | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* 1. Expand all sections with matches on search | ||
* 2. Keep the expanded or collapsed state of the exercise groups when a filter is applied | ||
* 3. Make sure at least one displayed section is expanded by default | ||
* | ||
* @param exerciseGroups updated and grouped exercises that are to be displayed | ||
*/ | ||
private adjustExpandedOrCollapsedStateOfExerciseGroups(exerciseGroups: ExerciseGroups) { | ||
const isSearchingExercise = this.appliedSearchString; | ||
if (isSearchingExercise) { | ||
return this.expandAllExercisesAndSaveStateBeforeSearch(exerciseGroups); | ||
} | ||
|
||
if (this.searchWasActive) { | ||
return this.restoreStateBeforeSearch(exerciseGroups); | ||
} | ||
|
||
const filterIsApplied = this.exerciseGroups; | ||
if (filterIsApplied) { | ||
this.keepCurrentCollapsedOrExpandedStateOfExerciseGroups(exerciseGroups); | ||
} | ||
|
||
this.makeSureAtLeastOneExerciseGroupIsExpanded(exerciseGroups); | ||
} | ||
|
||
private getExerciseGroup(exercise: Exercise): ExerciseGroupCategory { | ||
if (!exercise.dueDate) { | ||
return 'noDueDate'; | ||
} | ||
|
||
const dueDate = dayjs(exercise.dueDate); | ||
const now = dayjs(); | ||
|
||
const dueDateIsInThePast = dueDate.isBefore(now); | ||
if (dueDateIsInThePast) { | ||
return 'past'; | ||
} | ||
|
||
const dueDateIsWithinNextWeek = dueDate.isBefore(now.add(1, 'week')); | ||
if (dueDateIsWithinNextWeek) { | ||
return 'current'; | ||
} | ||
|
||
return 'future'; | ||
} | ||
} |
53 changes: 53 additions & 0 deletions
53
...main/webapp/app/overview/course-exercises/course-exercises-grouped-by-week.component.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
<div class="exercise-row-container mb-3" *ngIf="nextRelevantExercise && isVisibleToStudents(nextRelevantExercise.exercise)"> | ||
<h3 class="text-primary"> | ||
<span *ngIf="nextRelevantExercise!.dueDate; else noDueDate"> | ||
{{ 'artemisApp.courseOverview.exerciseList.currentExerciseGroupHeader' | artemisTranslate: { date: nextRelevantExercise.dueDate | artemisDate } }} | ||
</span> | ||
<ng-template #noDueDate> | ||
{{ 'artemisApp.courseOverview.exerciseList.currentExerciseGroupHeaderWithoutDueDate' | artemisTranslate }} | ||
</ng-template> | ||
</h3> | ||
<jhi-course-exercise-row | ||
class="pb-1" | ||
id="next-course-exercise-row" | ||
[exercise]="nextRelevantExercise!.exercise" | ||
[course]="course" | ||
[hasGuidedTour]="nextRelevantExercise!.exercise === exerciseForGuidedTour" | ||
/> | ||
<div class="collapsed"></div> | ||
</div> | ||
|
||
<div class="guided-tour exercise-row-container mb-3" *ngFor="let weekKey of weeklyIndexKeys"> | ||
<div | ||
class="control-label" | ||
[ngClass]="{ 'text-primary': immutableWeeklyExercisesGrouped[weekKey] ? immutableWeeklyExercisesGrouped[weekKey].isCurrentWeek : false }" | ||
(click)="weeklyExercisesGrouped[weekKey].isCollapsed = !weeklyExercisesGrouped[weekKey].isCollapsed" | ||
> | ||
<fa-icon class="pe-3" [icon]="immutableWeeklyExercisesGrouped[weekKey].isCollapsed ? faAngleDown : faAngleUp"></fa-icon> | ||
<span *ngIf="immutableWeeklyExercisesGrouped[weekKey].start && immutableWeeklyExercisesGrouped[weekKey].end"> | ||
{{ immutableWeeklyExercisesGrouped[weekKey].start | artemisDate: 'short-date' }} - | ||
{{ immutableWeeklyExercisesGrouped[weekKey].end | artemisDate: 'short-date' }} | ||
</span> | ||
<span *ngIf="!immutableWeeklyExercisesGrouped[weekKey].start || !immutableWeeklyExercisesGrouped[weekKey].end"> | ||
{{ 'artemisApp.courseOverview.exerciseList.noDateAssociated' | artemisTranslate }} | ||
</span> | ||
<span | ||
class="ms-2" | ||
style="font-weight: 300" | ||
jhiTranslate="artemisApp.courseOverview.exerciseList.exerciseGroupHeader" | ||
[translateValues]="{ total: immutableWeeklyExercisesGrouped[weekKey].exercises.length }" | ||
> | ||
(Exercises: {{ immutableWeeklyExercisesGrouped[weekKey].exercises.length }}) | ||
</span> | ||
</div> | ||
<div *ngIf="!immutableWeeklyExercisesGrouped[weekKey].isCollapsed"> | ||
<jhi-course-exercise-row | ||
class="pb-1" | ||
[exercise]="exercise" | ||
[course]="course" | ||
[hasGuidedTour]="exercise === exerciseForGuidedTour" | ||
*ngFor="let exercise of immutableWeeklyExercisesGrouped[weekKey].exercises" | ||
/> | ||
</div> | ||
<div class="collapsed"></div> | ||
</div> |
42 changes: 42 additions & 0 deletions
42
src/main/webapp/app/overview/course-exercises/course-exercises-grouped-by-week.component.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
import { Component, Input, OnChanges, OnInit } from '@angular/core'; | ||
import { Exercise } from 'app/entities/exercise.model'; | ||
import { ExerciseFilter, ExerciseWithDueDate } from 'app/overview/course-exercises/course-exercises.component'; | ||
import { Course } from 'app/entities/course.model'; | ||
import { faAngleDown, faAngleUp } from '@fortawesome/free-solid-svg-icons'; | ||
import { QuizExercise } from 'app/entities/quiz/quiz-exercise.model'; | ||
import { getAsMutableObject } from 'app/shared/util/utils'; | ||
|
||
@Component({ | ||
selector: 'jhi-course-exercises-grouped-by-week', | ||
templateUrl: './course-exercises-grouped-by-week.component.html', | ||
styleUrls: ['../course-overview.scss'], | ||
}) | ||
export class CourseExercisesGroupedByWeekComponent implements OnInit, OnChanges { | ||
@Input() nextRelevantExercise?: ExerciseWithDueDate; | ||
@Input() course: Course; | ||
@Input() exerciseForGuidedTour?: Exercise; | ||
@Input() weeklyIndexKeys: string[]; | ||
@Input() immutableWeeklyExercisesGrouped: object; | ||
@Input() activeFilters: Set<ExerciseFilter>; | ||
|
||
weeklyExercisesGrouped: object; | ||
|
||
faAngleUp = faAngleUp; | ||
faAngleDown = faAngleDown; | ||
|
||
ngOnInit() { | ||
this.weeklyExercisesGrouped = getAsMutableObject(this.immutableWeeklyExercisesGrouped); | ||
} | ||
|
||
ngOnChanges() { | ||
this.weeklyExercisesGrouped = getAsMutableObject(this.immutableWeeklyExercisesGrouped); | ||
} | ||
|
||
/** | ||
* Checks whether an exercise is visible to students or not | ||
* @param exercise The exercise which should be checked | ||
*/ | ||
isVisibleToStudents(exercise: Exercise): boolean | undefined { | ||
return !this.activeFilters.has(ExerciseFilter.UNRELEASED) || (exercise as QuizExercise)?.visibleToStudents; | ||
} | ||
} |
Oops, something went wrong.