MOBILE-4348 module: Renew completion

main
Pau Ferrer Ocaña 2023-09-27 12:20:12 +02:00
parent d790e2a752
commit 05786e94d3
22 changed files with 431 additions and 192 deletions

View File

@ -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",

View File

@ -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
*/

View File

@ -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;
}
}
}

View File

@ -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 {}

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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;
};

View File

@ -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);;
}
}

View File

@ -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 });
}

View File

@ -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>

View File

@ -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);
}
}

View File

@ -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;
};

View File

@ -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;
}
}

View File

@ -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>

View File

@ -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 });
}

View File

@ -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"

View File

@ -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);
}
/**

View File

@ -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}}"
}

View File

@ -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

View File

@ -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',

View File

@ -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;
}
}

View File

@ -72,6 +72,7 @@ html {
--list-item-max-width: 768px;
--modal-radius: var(--radius-md);
--modal-lateral-max-width: 320px;
--modal-lateral-margin: 56px;