MOBILE-4348 module: Renew completion
parent
d790e2a752
commit
05786e94d3
|
@ -1582,6 +1582,7 @@
|
|||
"core.course.completion_setby:auto:todo": "course",
|
||||
"core.course.completion_setby:manual:done": "course",
|
||||
"core.course.completion_setby:manual:markdone": "course",
|
||||
"core.course.completionmenuitem": "completion",
|
||||
"core.course.completionrequirements": "course",
|
||||
"core.course.confirmdownload": "local_moodlemobileapp",
|
||||
"core.course.confirmdownloadunknownsize": "local_moodlemobileapp",
|
||||
|
@ -1620,6 +1621,7 @@
|
|||
"core.course.relativedatessubmissionduedatebefore": "course",
|
||||
"core.course.section": "moodle",
|
||||
"core.course.startdate": "moodle",
|
||||
"core.course.studentsmust": "completion",
|
||||
"core.course.thisweek": "format_weeks/currentsection",
|
||||
"core.course.todo": "completion",
|
||||
"core.course.tour_navigation_course_index_student_content": "tool_usertours",
|
||||
|
@ -1628,6 +1630,7 @@
|
|||
"core.course.viewcourse": "block_timeline",
|
||||
"core.course.warningmanualcompletionmodified": "local_moodlemobileapp",
|
||||
"core.course.warningofflinemanualcompletiondeleted": "local_moodlemobileapp",
|
||||
"core.course.youmust": "completion",
|
||||
"core.coursedetails": "moodle",
|
||||
"core.coursenogroups": "local_moodlemobileapp",
|
||||
"core.courses.addtofavourites": "block_myoverview",
|
||||
|
@ -2313,8 +2316,9 @@
|
|||
"core.scanqr": "local_moodlemobileapp",
|
||||
"core.scrollbackward": "local_moodlemobileapp",
|
||||
"core.scrollforward": "local_moodlemobileapp",
|
||||
"core.search.allcourses": "search",
|
||||
"core.search": "moodle",
|
||||
"core.search.allcategories": "local_moodlemobileapp",
|
||||
"core.search.allcourses": "search",
|
||||
"core.search.empty": "local_moodlemobileapp",
|
||||
"core.search.filtercategories": "local_moodlemobileapp",
|
||||
"core.search.filtercourses": "local_moodlemobileapp",
|
||||
|
@ -2323,7 +2327,6 @@
|
|||
"core.search.noresults": "local_moodlemobileapp",
|
||||
"core.search.noresultshelp": "local_moodlemobileapp",
|
||||
"core.search.resultby": "local_moodlemobileapp",
|
||||
"core.search": "moodle",
|
||||
"core.searching": "local_moodlemobileapp",
|
||||
"core.searchresults": "moodle",
|
||||
"core.sec": "moodle",
|
||||
|
|
|
@ -220,6 +220,15 @@ export class AddonModResourceModuleHandlerService extends CoreModuleHandlerBase
|
|||
return extra.join(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async manualCompletionAlwaysShown(module: CoreCourseModuleData): Promise<boolean> {
|
||||
const hideButton = await this.hideOpenButton(module);
|
||||
|
||||
return !hideButton;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
|
|
|
@ -131,25 +131,4 @@ ion-button {
|
|||
ion-icon {
|
||||
margin: var(--icon-margin);
|
||||
}
|
||||
|
||||
.select-icon {
|
||||
margin: var(--icon-margin);
|
||||
width: 19px;
|
||||
height: 19px;
|
||||
position: relative;
|
||||
|
||||
.select-icon-inner {
|
||||
left: 5px;
|
||||
top: 50%;
|
||||
margin-top: -2px;
|
||||
position: absolute;
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
color: currentcolor;
|
||||
pointer-events: none;
|
||||
border-top: 5px solid;
|
||||
border-right: 5px solid transparent;
|
||||
border-left: 5px solid transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,6 +30,7 @@ import { CoreCourseModuleNavigationComponent } from './module-navigation/module-
|
|||
import { CoreCourseModuleSummaryComponent } from './module-summary/module-summary';
|
||||
import { CoreCourseCourseIndexTourComponent } from './course-index-tour/course-index-tour';
|
||||
import { CoreRemindersComponentsModule } from '@features/reminders/components/components.module';
|
||||
import { CoreCourseModuleCompletionDetailsComponent } from './module-completion-details/module-completion-details';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
|
@ -46,6 +47,7 @@ import { CoreRemindersComponentsModule } from '@features/reminders/components/co
|
|||
CoreCourseUnsupportedModuleComponent,
|
||||
CoreCourseModuleNavigationComponent,
|
||||
CoreCourseModuleSummaryComponent,
|
||||
CoreCourseModuleCompletionDetailsComponent,
|
||||
],
|
||||
imports: [
|
||||
CoreBlockComponentsModule,
|
||||
|
@ -66,6 +68,7 @@ import { CoreRemindersComponentsModule } from '@features/reminders/components/co
|
|||
CoreCourseUnsupportedModuleComponent,
|
||||
CoreCourseModuleNavigationComponent,
|
||||
CoreCourseModuleSummaryComponent,
|
||||
CoreCourseModuleCompletionDetailsComponent,
|
||||
],
|
||||
})
|
||||
export class CoreCourseComponentsModule {}
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
<!-- Completion criterias. -->
|
||||
<div class="ion-padding">
|
||||
<!-- Dialog header. -->
|
||||
<h2 *ngIf="isTrackedUser">{{ 'core.course.youmust' | translate }}</h2>
|
||||
<h2 *ngIf="!isTrackedUser">{{ 'core.course.studentsmust' | translate }}</h2>
|
||||
|
||||
<ion-list role="list">
|
||||
<ng-container *ngFor="let rule of completionDetails">
|
||||
<!-- Show completion status and description to tracked users. -->
|
||||
|
||||
<ng-container *ngIf="isTrackedUser">
|
||||
<div *ngIf="rule.statusComplete" role="listitem" [attr.aria-label]="rule.accessibleDescription" class="text-success">
|
||||
<ion-icon name="fas-check" aria-hidden="true"></ion-icon>
|
||||
<span class="sr-only">{{ 'core.course.completion_automatic:done' | translate }}</span>
|
||||
{{ rule.rulevalue.description }}
|
||||
</div>
|
||||
|
||||
<div *ngIf="rule.statusCompleteFail" role="listitem" [attr.aria-label]="rule.accessibleDescription" class="text-danger">
|
||||
<ion-icon name="fas-xmark" aria-hidden="true"></ion-icon>
|
||||
<span class="sr-only">{{ 'core.course.completion_automatic:failed' | translate }}</span>
|
||||
{{ rule.rulevalue.description }}
|
||||
</div>
|
||||
|
||||
<div *ngIf="rule.statusIncomplete" role="listitem" [attr.aria-label]="rule.accessibleDescription">
|
||||
<ion-icon name="fas-circle" class="completion_dot" aria-hidden="true"></ion-icon>
|
||||
<span class="sr-only">{{ 'core.course.completion_automatic:todo' | translate }}</span>
|
||||
{{ rule.rulevalue.description }}
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<!-- Show only description (without status) to non-tracked users. -->
|
||||
<div *ngIf="!isTrackedUser" role="listitem" [attr.aria-label]="rule.accessibleDescription">
|
||||
<ion-icon name="fas-circle" class="completion_dot" aria-hidden="true"></ion-icon>
|
||||
{{ rule.rulevalue.description }}
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<!-- Show also manual completion description in the list to non-tracked users. -->
|
||||
<div *ngIf="isManual && !isTrackedUser" role="listitem" class="core-module-completion-todo">
|
||||
<ion-icon name="fas-circle" class="completion_dot" aria-hidden="true"></ion-icon>
|
||||
{{ 'core.course.completion_manual:markdone' | translate }}
|
||||
</div>
|
||||
</ion-list>
|
||||
</div>
|
|
@ -0,0 +1,22 @@
|
|||
:host {
|
||||
h2, div {
|
||||
font-size: 16px;
|
||||
}
|
||||
h2 {
|
||||
margin-top: 0px;
|
||||
margin-bottom: 8px;
|
||||
line-height: 27px;
|
||||
}
|
||||
ion-list {
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
ion-icon {
|
||||
width: 24px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
ion-icon.completion_dot {
|
||||
font-size: 4px;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed 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 { Component, Input, OnInit } from '@angular/core';
|
||||
|
||||
import {
|
||||
CoreCourseModuleCompletionStatus,
|
||||
CoreCourseModuleWSRuleDetails,
|
||||
} from '@features/course/services/course';
|
||||
import { CoreCourseModuleCompletionData } from '@features/course/services/course-helper';
|
||||
import { CoreUser } from '@features/user/services/user';
|
||||
import { Translate } from '@singletons';
|
||||
|
||||
/**
|
||||
* Component to show automatic completion details dialog.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'core-course-module-completion-details',
|
||||
templateUrl: 'module-completion-details.html',
|
||||
styleUrls: ['module-completion-details.scss'],
|
||||
})
|
||||
export class CoreCourseModuleCompletionDetailsComponent implements OnInit {
|
||||
|
||||
@Input() completion?: CoreCourseModuleCompletionData; // The completion status.
|
||||
|
||||
isTrackedUser = false;
|
||||
isManual = false;
|
||||
completionDetails: CompletionRule[] = [];
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async ngOnInit(): Promise<void> {
|
||||
if (!this.completion) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isManual = !this.completion.isautomatic;
|
||||
this.isTrackedUser = !!this.completion.istrackeduser;
|
||||
|
||||
if (!this.completion?.details) {
|
||||
return;
|
||||
}
|
||||
|
||||
const details = this.completion.details;
|
||||
|
||||
// Format rules.
|
||||
this.completionDetails = await Promise.all(details.map(async (rule: CompletionRule) => {
|
||||
rule.statusComplete = rule.rulevalue.status === CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE ||
|
||||
rule.rulevalue.status === CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE_PASS;
|
||||
rule.statusCompleteFail = rule.rulevalue.status === CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE_FAIL;
|
||||
rule.statusIncomplete = rule.rulevalue.status === CoreCourseModuleCompletionStatus.COMPLETION_INCOMPLETE;
|
||||
rule.accessibleDescription = null;
|
||||
|
||||
if (this.completion?.overrideby) {
|
||||
const fullName = await CoreUser.getUserFullNameWithDefault(this.completion.overrideby, this.completion.courseId);
|
||||
|
||||
const setByData = {
|
||||
$a: {
|
||||
condition: rule.rulevalue.description,
|
||||
setby: fullName,
|
||||
},
|
||||
};
|
||||
const overrideStatus = rule.statusComplete ? 'done' : 'todo';
|
||||
|
||||
rule.accessibleDescription = Translate.instant('core.course.completion_setby:auto:' + overrideStatus, setByData);
|
||||
}
|
||||
|
||||
return rule;
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type CompletionRule = CoreCourseModuleWSRuleDetails & {
|
||||
statusComplete?: boolean;
|
||||
statusCompleteFail?: boolean;
|
||||
statusIncomplete?: boolean;
|
||||
accessibleDescription?: string | null;
|
||||
};
|
|
@ -1,14 +1,13 @@
|
|||
:host {
|
||||
min-width: var(--a11y-min-target-size);
|
||||
min-height: var(--a11y-min-target-size);
|
||||
display: contents;
|
||||
--size: 30px;
|
||||
|
||||
img {
|
||||
padding: 5px;
|
||||
padding: 2px;
|
||||
width: var(--size);
|
||||
vertical-align: middle;
|
||||
max-width: none;
|
||||
margin: 7px;
|
||||
margin: 4px;
|
||||
}
|
||||
|
||||
ion-button {
|
||||
|
@ -16,5 +15,11 @@
|
|||
--padding-start: 0px;
|
||||
--padding-end: 0px;
|
||||
--padding-bottom: 0px;
|
||||
margin: 0;
|
||||
--a11y-target-min-size: 32px;
|
||||
width: var(--a11y-target-min-size);
|
||||
height: var(--a11y-target-min-size);
|
||||
min-width: var(--a11y-target-min-size);
|
||||
min-height: var(--a11y-target-min-size);;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -147,7 +147,10 @@ export class CoreCourseModuleCompletionLegacyComponent extends CoreCourseModuleC
|
|||
return;
|
||||
}
|
||||
|
||||
await CoreCourseHelper.changeManualCompletion(this.completion, event);
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
await CoreCourseHelper.changeManualCompletion(this.completion);
|
||||
|
||||
CoreEvents.trigger(CoreEvents.MANUAL_COMPLETION_CHANGED, { completion: this.completion });
|
||||
}
|
||||
|
|
|
@ -1,70 +1,45 @@
|
|||
<ng-container *ngIf="completion">
|
||||
<ng-container *ngIf="showCompletionConditions && completion.isautomatic">
|
||||
<div *ngIf="mode == 'full'" class="core-module-automatic-completion-conditions" role="list"
|
||||
[attr.aria-label]="'core.course.completionrequirements' | translate:{ $a: moduleName }">
|
||||
<ng-container *ngIf="showCompletionInfo && completion">
|
||||
<ng-container *ngIf="completion.istrackeduser">
|
||||
<ng-container *ngIf="completion.isautomatic">
|
||||
<ion-button class="completioninfo completion_incomplete ion-text-wrap chip" *ngIf="!completed" fill="outline"
|
||||
(click)="completionClicked($event)">
|
||||
{{ 'core.course.todo' | translate }}
|
||||
<div class="select-icon" role="presentation" aria-hidden="true">
|
||||
<div class="select-icon-inner"></div>
|
||||
</div>
|
||||
</ion-button>
|
||||
|
||||
<ng-container *ngIf="completion.istrackeduser">
|
||||
<ng-container *ngFor="let rule of details">
|
||||
<ion-chip *ngIf="rule.statuscomplete" color="success" role="listitem" [attr.aria-label]="rule.accessibleDescription"
|
||||
class="completioninfo completion_complete">
|
||||
<ion-icon name="fas-check" aria-hidden="true"></ion-icon>
|
||||
<ion-label>
|
||||
<strong>{{ 'core.course.completion_automatic:done' | translate }}</strong>
|
||||
{{ rule.rulevalue.description }}
|
||||
</ion-label>
|
||||
</ion-chip>
|
||||
<ion-button class="completioninfo completion_complete ion-text-wrap chip" color="success" (click)="completionClicked($event)"
|
||||
*ngIf="completed">
|
||||
<ion-icon name="fas-check" slot="start" aria-hidden="true"></ion-icon>
|
||||
{{'core.course.done' | translate }}
|
||||
<div class="select-icon" role="presentation" aria-hidden="true">
|
||||
<div class="select-icon-inner"></div>
|
||||
</div>
|
||||
</ion-button>
|
||||
</ng-container>
|
||||
|
||||
<ion-chip *ngIf="rule.statuscompletefail" color="danger" role="listitem" [attr.aria-label]="rule.accessibleDescription"
|
||||
class="completioninfo completion_fail">
|
||||
<ion-icon name="fas-xmark" aria-hidden="true"></ion-icon>
|
||||
<ion-label>
|
||||
<strong>{{ 'core.course.completion_automatic:failed' | translate }}</strong>
|
||||
{{ rule.rulevalue.description }}
|
||||
</ion-label>
|
||||
</ion-chip>
|
||||
|
||||
<ion-chip *ngIf="rule.statusincomplete" color="secondary" role="listitem" [attr.aria-label]="rule.accessibleDescription"
|
||||
class="completioninfo completion_incomplete">
|
||||
<ion-icon name="fas-pen-to-square" aria-hidden="true"></ion-icon>
|
||||
<ion-label>
|
||||
<strong>{{ 'core.course.completion_automatic:todo' | translate }}</strong>
|
||||
{{ rule.rulevalue.description }}
|
||||
</ion-label>
|
||||
</ion-chip>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="!completion.istrackeduser">
|
||||
<ion-chip *ngFor="let rule of details" role="listitem" class="core-module-completion-todo">
|
||||
<ion-icon name="fas-pen-to-square" aria-hidden="true"></ion-icon>
|
||||
<ion-label>
|
||||
<strong>{{ 'core.course.completion_automatic:todo' | translate }}</strong>
|
||||
{{ rule.rulevalue.description }}
|
||||
</ion-label>
|
||||
</ion-chip>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="mode == 'basic' && completion.istrackeduser">
|
||||
<ion-chip class="completioninfo completion_incomplete" *ngIf="completionStatus === 0" color="secondary">
|
||||
<ion-icon name="fas-pen-to-square" aria-hidden="true"></ion-icon>
|
||||
<ion-label>
|
||||
{{ 'core.course.todo' | translate }}
|
||||
</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip class="completioninfo completion_complete" *ngIf="completionStatus === 1 || completionStatus === 2" color="success">
|
||||
<ion-icon name="fas-check" aria-hidden="true"></ion-icon>
|
||||
<ion-label>{{'core.course.done' | translate }}</ion-label>
|
||||
</ion-chip>
|
||||
<ion-chip class="completioninfo completion_fail" *ngIf="completionStatus === 3" color="danger">
|
||||
<ion-icon name="fas-xmark" aria-hidden="true"></ion-icon>
|
||||
<ion-label>{{'core.course.failed' | translate }}</ion-label>
|
||||
</ion-chip>
|
||||
<ng-container *ngIf="!completion.isautomatic">
|
||||
<ion-button *ngIf="completed" color="success" [attr.aria-label]="accessibleDescription" (click)="completionClicked($event)"
|
||||
class="completioninfo completion_complete ion-text-wrap chip">
|
||||
<ion-icon name="fas-check" slot="start" aria-hidden="true"></ion-icon>
|
||||
{{ 'core.course.completion_manual:done' | translate }}
|
||||
<ion-icon *ngIf="completion.offline" name="fas-arrows-rotate"
|
||||
[attr.aria-label]="'core.course.manualcompletionnotsynced' | translate" slot="end"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-button *ngIf="!completed" fill="outline" [attr.aria-label]="accessibleDescription" (click)="completionClicked($event)"
|
||||
class="completioninfo completion_incomplete ion-text-wrap chip">
|
||||
{{ 'core.course.completion_manual:markdone' | translate }}
|
||||
<ion-icon *ngIf="completion.offline" name="fas-arrows-rotate"
|
||||
[attr.aria-label]="'core.course.manualcompletionnotsynced' | translate" slot="end"></ion-icon>
|
||||
</ion-button>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
|
||||
<core-course-module-manual-completion *ngIf="showManualCompletion" [completion]="completion" [moduleName]="moduleName"
|
||||
(completionChanged)="completionChanged.emit($event)" [mode]="mode">
|
||||
</core-course-module-manual-completion>
|
||||
<ion-button *ngIf="!completion.istrackeduser" fill="outline" class="ion-text-wrap chip" (click)="completionClicked($event)">
|
||||
{{ 'core.course.completionmenuitem' | translate }}
|
||||
<div class="select-icon" role="presentation" aria-hidden="true">
|
||||
<div class="select-icon-inner"></div>
|
||||
</div>
|
||||
</ion-button>
|
||||
</ng-container>
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
:host {
|
||||
display: block;
|
||||
margin: var(--margin, 4px);
|
||||
|
||||
ion-button {
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
ion-button.button-solid.ion-color-success::part(native){
|
||||
background: var(--ion-color-tint);
|
||||
color: var(--ion-color-shade);
|
||||
}
|
||||
|
||||
ion-button.button-outline::part(native){
|
||||
border-color: var(--gray-400);
|
||||
}
|
||||
}
|
|
@ -12,17 +12,19 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { Component, Input, OnChanges, OnDestroy, OnInit, SimpleChange } from '@angular/core';
|
||||
|
||||
import { CoreCourseModuleCompletionBaseComponent } from '@features/course/classes/module-completion';
|
||||
import {
|
||||
CoreCourseCompletionMode,
|
||||
CoreCourseModuleCompletionStatus,
|
||||
CoreCourseModuleCompletionTracking,
|
||||
CoreCourseModuleWSRuleDetails,
|
||||
} from '@features/course/services/course';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreCourseModuleCompletionDetailsComponent } from '../module-completion-details/module-completion-details';
|
||||
import { CoreCourseHelper } from '@features/course/services/course-helper';
|
||||
import { CoreUser } from '@features/user/services/user';
|
||||
import { Translate } from '@singletons';
|
||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||
|
||||
/**
|
||||
* Component to handle activity completion. It shows a checkbox with the current status, and allows manually changing
|
||||
|
@ -36,61 +38,132 @@ import { Translate } from '@singletons';
|
|||
@Component({
|
||||
selector: 'core-course-module-completion',
|
||||
templateUrl: 'core-course-module-completion.html',
|
||||
styleUrls: ['module-completion.scss'],
|
||||
})
|
||||
export class CoreCourseModuleCompletionComponent extends CoreCourseModuleCompletionBaseComponent {
|
||||
export class CoreCourseModuleCompletionComponent
|
||||
extends CoreCourseModuleCompletionBaseComponent
|
||||
implements OnInit, OnChanges, OnDestroy {
|
||||
|
||||
@Input() showCompletionConditions = false; // Whether to show activity completion conditions.
|
||||
@Input() showManualCompletion = false; // Whether to show manual completion.
|
||||
@Input() mode: CoreCourseCompletionMode = CoreCourseCompletionMode.FULL; // Show full completion status or a basic mode.
|
||||
|
||||
details?: CompletionRule[];
|
||||
completed = false;
|
||||
accessibleDescription: string | null = null;
|
||||
completionStatus?: CoreCourseModuleCompletionStatus;
|
||||
showCompletionInfo = false;
|
||||
protected completionObserver?: CoreEventObserver;
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
if (!this.completion) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hasConditions = !this.completion.isautomatic || (this.completion.details?.length || 0) > 0;
|
||||
this.showCompletionInfo = hasConditions && (this.showCompletionConditions || this.showManualCompletion);
|
||||
if (!this.showCompletionInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.completion.isautomatic && this.completion.istrackeduser) {
|
||||
this.completionObserver = CoreEvents.on(CoreEvents.MANUAL_COMPLETION_CHANGED, (data) => {
|
||||
if (!this.completion || this.completion.cmid != data.completion.cmid) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.completion = data.completion;
|
||||
this.calculateData();
|
||||
this.completionChanged.emit(this.completion);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
protected async calculateData(): Promise<void> {
|
||||
if (!this.completion?.details) {
|
||||
if (!this.completion || !this.completion.istrackeduser) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.completionStatus = !this.completion?.istrackeduser ||
|
||||
this.completion.tracking == CoreCourseModuleCompletionTracking.COMPLETION_TRACKING_NONE
|
||||
const completionStatus = this.completion.tracking == CoreCourseModuleCompletionTracking.COMPLETION_TRACKING_NONE
|
||||
? undefined
|
||||
: this.completion.state;
|
||||
|
||||
// Format rules.
|
||||
this.details = await Promise.all(this.completion.details.map(async (rule: CompletionRule) => {
|
||||
rule.statuscomplete = rule.rulevalue.status == CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE ||
|
||||
rule.rulevalue.status == CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE_PASS;
|
||||
rule.statuscompletefail = rule.rulevalue.status == CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE_FAIL;
|
||||
rule.statusincomplete = rule.rulevalue.status == CoreCourseModuleCompletionStatus.COMPLETION_INCOMPLETE;
|
||||
rule.accessibleDescription = null;
|
||||
this.completed = completionStatus !== CoreCourseModuleCompletionStatus.COMPLETION_INCOMPLETE &&
|
||||
completionStatus !== CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE_FAIL;
|
||||
|
||||
if (this.completion?.overrideby) {
|
||||
if (!this.completion.isautomatic) {
|
||||
// Set an accessible description for manual completions with overridden completion state.
|
||||
if (this.completion.overrideby) {
|
||||
const fullName = await CoreUser.getUserFullNameWithDefault(this.completion.overrideby, this.completion.courseId);
|
||||
|
||||
const setByData = {
|
||||
$a: {
|
||||
condition: rule.rulevalue.description,
|
||||
activityname: this.moduleName,
|
||||
setby: fullName,
|
||||
},
|
||||
};
|
||||
const overrideStatus = rule.statuscomplete ? 'done' : 'todo';
|
||||
const setByLangKey = this.completion.state ? 'completion_setby:manual:done' : 'completion_setby:manual:markdone';
|
||||
this.accessibleDescription = Translate.instant('core.course.' + setByLangKey, setByData);
|
||||
} else {
|
||||
const langKey = this.completion.state ? 'completion_manual:aria:done' : 'completion_manual:aria:markdone';
|
||||
this.accessibleDescription = Translate.instant('core.course.' + langKey, { $a: this.moduleName });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rule.accessibleDescription = Translate.instant('core.course.completion_setby:auto:' + overrideStatus, setByData);
|
||||
/**
|
||||
* Completion clicked.
|
||||
*
|
||||
* @param event The click event.
|
||||
*/
|
||||
async completionClicked(event: Event): Promise<void> {
|
||||
if (!this.completion || !this.showCompletionInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
if (this.completion.isautomatic || !this.completion.istrackeduser) {
|
||||
// Fake clicked element to correct position of the popover.
|
||||
let target: HTMLElement | null = event.target as HTMLElement;
|
||||
if (target && target.tagName !== 'ION-BUTTON') {
|
||||
target = target.parentElement;
|
||||
}
|
||||
|
||||
return rule;
|
||||
}));
|
||||
CoreDomUtils.openPopover({
|
||||
component: CoreCourseModuleCompletionDetailsComponent,
|
||||
componentProps: {
|
||||
completion: this.completion,
|
||||
},
|
||||
showBackdrop: true,
|
||||
event: { target } as Event,
|
||||
});
|
||||
} else {
|
||||
await CoreCourseHelper.changeManualCompletion(this.completion);
|
||||
|
||||
CoreEvents.trigger(CoreEvents.MANUAL_COMPLETION_CHANGED, { completion: this.completion });
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnChanges(changes: { [name: string]: SimpleChange }): void {
|
||||
if (changes.completion && this.completion && this.completion.istrackeduser) {
|
||||
this.calculateData();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.completionObserver?.off();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type CompletionRule = CoreCourseModuleWSRuleDetails & {
|
||||
statuscomplete?: boolean;
|
||||
statuscompletefail?: boolean;
|
||||
statusincomplete?: boolean;
|
||||
accessibleDescription?: string | null;
|
||||
};
|
||||
|
|
|
@ -60,30 +60,8 @@
|
|||
}
|
||||
}
|
||||
|
||||
core-course-module-completion ::ng-deep ion-button {
|
||||
min-height: 28px;
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
text-transform: none;
|
||||
font-weight: normal;
|
||||
|
||||
ion-icon {
|
||||
font-size: 16px;
|
||||
min-width: 16px;
|
||||
@include margin(0, 8px, 0, 0);
|
||||
|
||||
&[slot=start] {
|
||||
@include margin(null, 8px, null, 0);
|
||||
}
|
||||
|
||||
&[slot=end] {
|
||||
@include margin(null, 0, null, 8px);
|
||||
}
|
||||
}
|
||||
|
||||
ion-label {
|
||||
white-space: normal !important;
|
||||
}
|
||||
core-course-module-completion {
|
||||
--margin: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
<ng-container *ngIf="completion && !completion.isautomatic">
|
||||
<ng-container *ngIf="completion.istrackeduser">
|
||||
<ion-button *ngIf="completion.state" color="success" [attr.aria-label]="accessibleDescription" (click)="completionClicked($event)"
|
||||
class="ion-text-wrap" [class.chip]="mode == 'basic'">
|
||||
class="ion-text-wrap chip">
|
||||
<ion-icon name="fas-check" slot="start" aria-hidden="true"></ion-icon>
|
||||
{{ 'core.course.completion_manual:done' | translate }}
|
||||
<ion-icon *ngIf="completion?.offline" name="fas-arrows-rotate"
|
||||
[attr.aria-label]="'core.course.manualcompletionnotsynced' | translate" slot="end"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-button *ngIf="!completion.state" fill="outline" [attr.aria-label]="accessibleDescription" (click)="completionClicked($event)"
|
||||
class="ion-text-wrap" [class.chip]="mode == 'basic'">
|
||||
class="ion-text-wrap chip">
|
||||
{{ 'core.course.completion_manual:markdone' | translate }}
|
||||
<ion-icon *ngIf="completion?.offline" name="fas-arrows-rotate"
|
||||
[attr.aria-label]="'core.course.manualcompletionnotsynced' | translate" slot="end"></ion-icon>
|
||||
|
@ -16,7 +16,7 @@
|
|||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="!completion.istrackeduser">
|
||||
<ion-button disabled="true" fill="outline" class="ion-text-wrap" [class.chip]="mode == 'basic'">
|
||||
<ion-button disabled="true" fill="outline" class="ion-text-wrap chip">
|
||||
{{ 'core.course.completion_manual:markdone' | translate }}
|
||||
</ion-button>
|
||||
</ng-container>
|
||||
|
|
|
@ -13,7 +13,6 @@
|
|||
// limitations under the License.
|
||||
|
||||
import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChange } from '@angular/core';
|
||||
import { CoreCourseCompletionMode } from '@features/course/services/course';
|
||||
import { CoreCourseHelper, CoreCourseModuleCompletionData } from '@features/course/services/course-helper';
|
||||
import { CoreUser } from '@features/user/services/user';
|
||||
import { Translate } from '@singletons';
|
||||
|
@ -21,6 +20,8 @@ import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
|||
|
||||
/**
|
||||
* Component to display a button for manual completion.
|
||||
*
|
||||
* @deprecated since 4.3. Not used anymore.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'core-course-module-manual-completion',
|
||||
|
@ -30,7 +31,6 @@ export class CoreCourseModuleManualCompletionComponent implements OnInit, OnChan
|
|||
|
||||
@Input() completion?: CoreCourseModuleCompletionData; // The completion status.
|
||||
@Input() moduleName?: string; // The name of the module this completion affects.
|
||||
@Input() mode: CoreCourseCompletionMode = CoreCourseCompletionMode.FULL; // Show full completion status or a basic mode.
|
||||
@Output() completionChanged = new EventEmitter<CoreCourseModuleCompletionData>(); // Notify when completion changes.
|
||||
|
||||
accessibleDescription: string | null = null;
|
||||
|
@ -100,7 +100,7 @@ export class CoreCourseModuleManualCompletionComponent implements OnInit, OnChan
|
|||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
await CoreCourseHelper.changeManualCompletion(this.completion, event);
|
||||
await CoreCourseHelper.changeManualCompletion(this.completion);
|
||||
|
||||
CoreEvents.trigger(CoreEvents.MANUAL_COMPLETION_CHANGED, { completion: this.completion });
|
||||
}
|
||||
|
|
|
@ -23,13 +23,6 @@
|
|||
</p>
|
||||
|
||||
<div class="core-module-additional-info">
|
||||
<!-- Basic module completion. -->
|
||||
<core-course-module-completion *ngIf="module.completiondata && module.uservisible && !showLegacyCompletion"
|
||||
[completion]="module.completiondata" [moduleName]="module.name" [moduleId]="module.id"
|
||||
[showCompletionConditions]="showCompletionConditions" [showManualCompletion]="showManualCompletion"
|
||||
(completionChanged)="completionChanged.emit($event)" mode="basic">
|
||||
</core-course-module-completion>
|
||||
|
||||
<ion-chip *ngIf="module.handlerData.extraBadge" [color]="module.handlerData.extraBadgeColor"
|
||||
class="ion-text-wrap ion-text-start" [outline]="true">
|
||||
<ion-label><span [innerHTML]="module.handlerData.extraBadge"></span></ion-label>
|
||||
|
@ -72,10 +65,10 @@
|
|||
contextLevel="module" [contextInstanceId]="module.id" [courseId]="module.course">
|
||||
</core-format-text>
|
||||
|
||||
<!-- Module completion. Only auto conditions-->
|
||||
<core-course-module-completion *ngIf="autoCompletionTodo && module.uservisible && !showLegacyCompletion"
|
||||
[completion]="module.completiondata" [moduleName]="module.name" [moduleId]="module.id"
|
||||
[showCompletionConditions]="showCompletionConditions">
|
||||
<!-- Activity completion. -->
|
||||
<core-course-module-completion *ngIf="hasCompletion && !showLegacyCompletion" [completion]="module.completiondata"
|
||||
[moduleName]="module.name" [moduleId]="module.id" [showCompletionConditions]="showCompletionConditions"
|
||||
[showManualCompletion]="showManualCompletion" (completionChanged)="completionChanged.emit($event)">
|
||||
</core-course-module-completion>
|
||||
|
||||
<div class="core-module-dates-availabilityinfo"
|
||||
|
|
|
@ -56,10 +56,10 @@ export class CoreCourseModuleComponent implements OnInit, OnDestroy {
|
|||
|
||||
modNameTranslated = '';
|
||||
hasInfo = false;
|
||||
hasCompletion = false; // Whether activity has completion to be shown.
|
||||
showManualCompletion = false; // Whether to show manual completion when completion conditions are disabled.
|
||||
prefetchStatusIcon$ = new BehaviorSubject<string>(''); // Module prefetch status icon.
|
||||
prefetchStatusText$ = new BehaviorSubject<string>(''); // Module prefetch status text.
|
||||
autoCompletionTodo = false;
|
||||
moduleHasView = true;
|
||||
|
||||
protected prefetchHandler?: CoreCourseModulePrefetchHandler;
|
||||
|
@ -78,7 +78,7 @@ export class CoreCourseModuleComponent implements OnInit, OnDestroy {
|
|||
this.showLegacyCompletion = this.showLegacyCompletion ??
|
||||
CoreConstants.CONFIG.uselegacycompletion ??
|
||||
!site.isVersionGreaterEqualThan('3.11');
|
||||
this.checkShowManualCompletion();
|
||||
this.checkShowCompletion();
|
||||
|
||||
if (!this.module.handlerData) {
|
||||
return;
|
||||
|
@ -87,18 +87,10 @@ export class CoreCourseModuleComponent implements OnInit, OnDestroy {
|
|||
this.module.handlerData.a11yTitle = this.module.handlerData.a11yTitle ?? this.module.handlerData.title;
|
||||
this.moduleHasView = CoreCourse.moduleHasView(this.module);
|
||||
|
||||
const completionStatus = this.showCompletionConditions && this.module.completiondata?.isautomatic &&
|
||||
this.module.completiondata.tracking == CoreCourseModuleCompletionTracking.COMPLETION_TRACKING_AUTOMATIC
|
||||
? this.module.completiondata.state
|
||||
: undefined;
|
||||
|
||||
this.autoCompletionTodo = completionStatus == CoreCourseModuleCompletionStatus.COMPLETION_INCOMPLETE ||
|
||||
completionStatus == CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE_FAIL;
|
||||
|
||||
this.hasInfo = !!(
|
||||
this.module.description ||
|
||||
(this.showActivityDates && this.module.dates && this.module.dates.length) ||
|
||||
(this.autoCompletionTodo && !this.showLegacyCompletion) ||
|
||||
(this.hasCompletion && !this.showLegacyCompletion) ||
|
||||
(this.module.availabilityinfo)
|
||||
);
|
||||
|
||||
|
@ -160,9 +152,14 @@ export class CoreCourseModuleComponent implements OnInit, OnDestroy {
|
|||
/**
|
||||
* Check whether manual completion should be shown.
|
||||
*/
|
||||
protected async checkShowManualCompletion(): Promise<void> {
|
||||
protected async checkShowCompletion(): Promise<void> {
|
||||
this.showManualCompletion = this.showCompletionConditions ||
|
||||
await CoreCourseModuleDelegate.manualCompletionAlwaysShown(this.module);
|
||||
|
||||
this.hasCompletion = !!this.module.completiondata && this.module.uservisible &&
|
||||
(!this.module.completiondata.isautomatic || (this.module.completiondata.details?.length || 0) > 0) &&
|
||||
(this.showCompletionConditions || this.showManualCompletion);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
"completion_manual:aria:markdone": "Mark {{$a}} as done",
|
||||
"completion_manual:done": "Done",
|
||||
"completion_manual:markdone": "Mark as done",
|
||||
"completionmenuitem": "Completion",
|
||||
"completion_setby:auto:done": "Done: {{$a.condition}} (set by {{$a.setby}})",
|
||||
"completion_setby:auto:todo": "To do: {{$a.condition}} (set by {{$a.setby}})",
|
||||
"completion_setby:manual:done": "{{$a.activityname}} is marked by {{$a.setby}} as done. Press to undo.",
|
||||
|
@ -54,6 +55,7 @@
|
|||
"relativedatessubmissionduedatebefore": "{{$a.datediffstr}} before course start",
|
||||
"section": "Section",
|
||||
"startdate": "Course start date",
|
||||
"studentsmust": "Students must",
|
||||
"thisweek": "This week",
|
||||
"todo": "To do",
|
||||
"tour_navigation_course_index_student_content": "Browse through activities and track your progress.",
|
||||
|
@ -61,5 +63,6 @@
|
|||
"useactivityonbrowser": "You can still use it using your device's web browser.",
|
||||
"viewcourse": "View course",
|
||||
"warningmanualcompletionmodified": "The manual completion of an activity was modified on the site.",
|
||||
"youmust": "You must",
|
||||
"warningofflinemanualcompletiondeleted": "Some offline manual completion of course '{{name}}' has been deleted. {{error}}"
|
||||
}
|
||||
|
|
|
@ -2012,12 +2012,10 @@ export class CoreCourseHelperProvider {
|
|||
* Completion clicked.
|
||||
*
|
||||
* @param completion The completion.
|
||||
* @param event The click event.
|
||||
* @returns Promise resolved with the result.
|
||||
*/
|
||||
async changeManualCompletion(
|
||||
completion: CoreCourseModuleCompletionData,
|
||||
event?: Event,
|
||||
): Promise<CoreStatusWithWarningsWSResponse | void> {
|
||||
if (!completion) {
|
||||
return;
|
||||
|
@ -2028,9 +2026,6 @@ export class CoreCourseHelperProvider {
|
|||
return;
|
||||
}
|
||||
|
||||
event?.preventDefault();
|
||||
event?.stopPropagation();
|
||||
|
||||
const modal = await CoreDomUtils.showModalLoading();
|
||||
completion.state = completion.state === CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE
|
||||
? CoreCourseModuleCompletionStatus.COMPLETION_INCOMPLETE
|
||||
|
|
|
@ -81,6 +81,9 @@ export enum CoreCourseModuleCompletionStatus {
|
|||
COMPLETION_COMPLETE_FAIL = 3,
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 4.3 Not used anymore.
|
||||
*/
|
||||
export enum CoreCourseCompletionMode {
|
||||
FULL = 'full',
|
||||
BASIC = 'basic',
|
||||
|
|
|
@ -389,6 +389,27 @@ ion-button.button.button-outline {
|
|||
--border-radius: var(--core-input-radius);
|
||||
}
|
||||
|
||||
ion-button .select-icon {
|
||||
margin: var(--icon-margin);
|
||||
width: 19px;
|
||||
height: 19px;
|
||||
position: relative;
|
||||
|
||||
.select-icon-inner {
|
||||
left: 5px;
|
||||
top: 50%;
|
||||
margin-top: -2px;
|
||||
position: absolute;
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
color: currentcolor;
|
||||
pointer-events: none;
|
||||
border-top: 5px solid;
|
||||
border-right: 5px solid transparent;
|
||||
border-left: 5px solid transparent;
|
||||
}
|
||||
}
|
||||
|
||||
[role="button"],
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
|
@ -463,10 +484,9 @@ div.core-iframe-network-error {
|
|||
left: -15%;
|
||||
}
|
||||
|
||||
ion-alert.core-nohead {
|
||||
.alert-head {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
ion-alert.core-nohead .alert-head,
|
||||
ion-alert .alert-head:empty {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
@keyframes scaleFrom0 {
|
||||
|
@ -1130,7 +1150,7 @@ ion-badge {
|
|||
}
|
||||
|
||||
ion-chip,
|
||||
ion-button.chip {
|
||||
ion-button.button.chip {
|
||||
line-height: 1.1;
|
||||
font-size: 12px;
|
||||
min-height: 24px;
|
||||
|
@ -1149,7 +1169,11 @@ ion-button.chip {
|
|||
}
|
||||
}
|
||||
|
||||
ion-button.chip {
|
||||
ion-button.button.chip {
|
||||
--border-radius: var(--radius-md);
|
||||
min-height: 32px;
|
||||
font-size: 14px;
|
||||
|
||||
ion-icon[slot=start] {
|
||||
@include margin(0, 8px, 0, 0);
|
||||
}
|
||||
|
@ -1840,13 +1864,34 @@ video::-webkit-media-text-track-display {
|
|||
white-space: normal !important;
|
||||
}
|
||||
|
||||
ion-modal.core-modal-no-background {
|
||||
--background: transparent;
|
||||
--box-shadow: none !important;
|
||||
pointer-events: none;
|
||||
ion-modal {
|
||||
.modal-wrapper {
|
||||
--border-radius: var(--modal-radius);
|
||||
}
|
||||
|
||||
ion-backdrop {
|
||||
display: none;
|
||||
&.core-modal-lateral,
|
||||
&.core-modal-fullscreen {
|
||||
--modal-radius: 0px;
|
||||
}
|
||||
|
||||
&.core-modal-no-background {
|
||||
--background: transparent;
|
||||
--box-shadow: none !important;
|
||||
pointer-events: none;
|
||||
|
||||
ion-backdrop {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ion-popover {
|
||||
.popover-wrapper .popover-content {
|
||||
border-radius: var(--modal-radius);
|
||||
}
|
||||
&.md {
|
||||
margin-top: 2px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -72,6 +72,7 @@ html {
|
|||
|
||||
--list-item-max-width: 768px;
|
||||
|
||||
--modal-radius: var(--radius-md);
|
||||
--modal-lateral-max-width: 320px;
|
||||
--modal-lateral-margin: 56px;
|
||||
|
||||
|
|
Loading…
Reference in New Issue