commit
a812ee3625
|
@ -27,7 +27,7 @@ import {
|
|||
import { AddonCalendarOffline } from './calendar-offline';
|
||||
import { AddonCalendarHelper } from './calendar-helper';
|
||||
import { makeSingleton, Translate } from '@singletons';
|
||||
import { CoreSync } from '@services/sync';
|
||||
import { CoreSync, CoreSyncResult } from '@services/sync';
|
||||
import { CoreNetworkError } from '@classes/errors/network-error';
|
||||
import moment from 'moment-timezone';
|
||||
|
||||
|
@ -301,13 +301,11 @@ export class AddonCalendarSyncProvider extends CoreSyncBaseProvider<AddonCalenda
|
|||
|
||||
export const AddonCalendarSync = makeSingleton(AddonCalendarSyncProvider);
|
||||
|
||||
export type AddonCalendarSyncEvents = {
|
||||
warnings: string[];
|
||||
export type AddonCalendarSyncEvents = CoreSyncResult & {
|
||||
events: AddonCalendarEvent[];
|
||||
offlineIdMap: Record<number, number>; // Map offline ID with online ID for created events.
|
||||
deleted: number[];
|
||||
toinvalidate: AddonCalendarSyncInvalidateEvent[];
|
||||
updated: boolean;
|
||||
source?: string; // Added on pages.
|
||||
moment?: moment.Moment; // Added on day page.
|
||||
};
|
||||
|
|
|
@ -270,9 +270,10 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView
|
|||
}
|
||||
|
||||
if (this.userId) {
|
||||
const userId = this.userId;
|
||||
// Get the member info. Invalidate first to make sure we get the latest status.
|
||||
promises.push(AddonMessages.invalidateMemberInfo(this.userId).then(async () => {
|
||||
this.otherMember = await AddonMessages.getMemberInfo(this.userId!);
|
||||
this.otherMember = await AddonMessages.getMemberInfo(userId);
|
||||
|
||||
if (!exists && this.otherMember) {
|
||||
this.conversationImage = this.otherMember.profileimageurl;
|
||||
|
@ -288,6 +289,8 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView
|
|||
|
||||
} else {
|
||||
if (this.userId) {
|
||||
const userId = this.userId;
|
||||
|
||||
// Fake the user member info.
|
||||
promises.push(CoreUser.getProfile(this.userId).then(async (user) => {
|
||||
this.otherMember = {
|
||||
|
@ -305,8 +308,8 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView
|
|||
canmessage: true,
|
||||
requirescontact: false,
|
||||
};
|
||||
this.otherMember.isblocked = await AddonMessages.isBlocked(this.userId!);
|
||||
this.otherMember.iscontact = await AddonMessages.isContact(this.userId!);
|
||||
this.otherMember.isblocked = await AddonMessages.isBlocked(userId);
|
||||
this.otherMember.iscontact = await AddonMessages.isContact(userId);
|
||||
this.blockIcon = this.otherMember.isblocked ? 'fas-user-check' : 'fas-user-lock';
|
||||
|
||||
return;
|
||||
|
|
|
@ -80,17 +80,17 @@ export class AddonMessagesDiscussions35Page implements OnInit, OnDestroy {
|
|||
AddonMessagesProvider.NEW_MESSAGE_EVENT,
|
||||
(data) => {
|
||||
if (data.userId && this.discussions) {
|
||||
const discussion = this.discussions.find((disc) => disc.message!.user == data.userId);
|
||||
const discussion = this.discussions.find((disc) => disc.message?.user === data.userId);
|
||||
|
||||
if (discussion === undefined) {
|
||||
this.loaded = false;
|
||||
this.refreshData().finally(() => {
|
||||
this.loaded = true;
|
||||
});
|
||||
} else {
|
||||
// An existing discussion has a new message, update the last message.
|
||||
discussion.message!.message = data.message;
|
||||
discussion.message!.timecreated = data.timecreated;
|
||||
} else if (discussion.message) {
|
||||
// An existing discussion has a new message, update the last message.
|
||||
discussion.message.message = data.message;
|
||||
discussion.message.timecreated = data.timecreated;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -102,10 +102,10 @@ export class AddonMessagesDiscussions35Page implements OnInit, OnDestroy {
|
|||
AddonMessagesProvider.READ_CHANGED_EVENT,
|
||||
(data) => {
|
||||
if (data.userId && this.discussions) {
|
||||
const discussion = this.discussions.find((disc) => disc.message!.user == data.userId);
|
||||
const discussion = this.discussions.find((disc) => disc.message?.user === data.userId);
|
||||
|
||||
if (discussion !== undefined) {
|
||||
// A discussion has been read reset counter.
|
||||
// A discussion has been read reset counter.
|
||||
discussion.unread = false;
|
||||
|
||||
// Conversations changed, invalidate them and refresh unread counts.
|
||||
|
@ -138,7 +138,7 @@ export class AddonMessagesDiscussions35Page implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
/**
|
||||
* Component loaded.
|
||||
* @inheritdoc
|
||||
*/
|
||||
async ngOnInit(): Promise<void> {
|
||||
this.route.queryParams.subscribe(async (params) => {
|
||||
|
@ -150,9 +150,9 @@ export class AddonMessagesDiscussions35Page implements OnInit, OnDestroy {
|
|||
|
||||
await this.fetchData();
|
||||
|
||||
if (!this.discussionUserId && this.discussions.length > 0 && CoreScreen.isTablet) {
|
||||
if (!this.discussionUserId && this.discussions.length > 0 && CoreScreen.isTablet && this.discussions[0].message) {
|
||||
// Take first and load it.
|
||||
await this.gotoDiscussion(this.discussions[0].message!.user);
|
||||
await this.gotoDiscussion(this.discussions[0].message.user);
|
||||
}
|
||||
|
||||
// Treat deep link now that the conversation route has been loaded if needed.
|
||||
|
@ -287,7 +287,7 @@ export class AddonMessagesDiscussions35Page implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
/**
|
||||
* Component destroyed.
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.newMessagesObserver?.off();
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
|
||||
import { Component, Optional, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
||||
import { Params } from '@angular/router';
|
||||
import { CoreError } from '@classes/errors/error';
|
||||
import { CoreSite } from '@classes/site';
|
||||
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
|
||||
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
|
||||
|
@ -314,11 +315,7 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo
|
|||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
protected hasSyncSucceed(result?: AddonModAssignSyncResult): boolean {
|
||||
if (!result) {
|
||||
return false;
|
||||
}
|
||||
|
||||
protected hasSyncSucceed(result: AddonModAssignSyncResult): boolean {
|
||||
if (result.updated) {
|
||||
this.submissionComponent?.invalidateAndRefresh(false);
|
||||
}
|
||||
|
@ -384,9 +381,9 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo
|
|||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
protected async sync(): Promise<AddonModAssignSyncResult | void> {
|
||||
protected async sync(): Promise<AddonModAssignSyncResult> {
|
||||
if (!this.assign) {
|
||||
return;
|
||||
throw new CoreError('Cannot sync without a assign.');
|
||||
}
|
||||
|
||||
return AddonModAssignSync.syncAssign(this.assign.id);
|
||||
|
|
|
@ -32,7 +32,7 @@ import {
|
|||
AddonModAssignSubmissionsDBRecordFormatted,
|
||||
AddonModAssignSubmissionsGradingDBRecordFormatted,
|
||||
} from './assign-offline';
|
||||
import { CoreSync } from '@services/sync';
|
||||
import { CoreSync, CoreSyncResult } from '@services/sync';
|
||||
import { CoreCourseLogHelper } from '@features/course/services/log-helper';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { CoreNetwork } from '@services/network';
|
||||
|
@ -530,9 +530,7 @@ export const AddonModAssignSync = makeSingleton(AddonModAssignSyncProvider);
|
|||
/**
|
||||
* Data returned by a assign sync.
|
||||
*/
|
||||
export type AddonModAssignSyncResult = {
|
||||
warnings: string[]; // List of warnings.
|
||||
updated: boolean; // Whether some data was sent to the server or offline data was updated.
|
||||
export type AddonModAssignSyncResult = CoreSyncResult & {
|
||||
courseId?: number; // Course the assign belongs to (if known).
|
||||
gradesBlocked: number[]; // Whether some grade couldn't be synced because it was blocked. UserId fields of the blocked grade.
|
||||
};
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
// limitations under the License.
|
||||
|
||||
import { Component, Optional, OnInit } from '@angular/core';
|
||||
import { CoreError } from '@classes/errors/error';
|
||||
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
|
||||
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
|
||||
import { IonContent } from '@ionic/angular';
|
||||
|
@ -454,22 +455,14 @@ export class AddonModChoiceIndexComponent extends CoreCourseModuleMainActivityCo
|
|||
}
|
||||
|
||||
/**
|
||||
* Performs the sync of the activity.
|
||||
*
|
||||
* @returns Promise resolved when done.
|
||||
* @inheritdoc
|
||||
*/
|
||||
protected sync(): Promise<AddonModChoiceSyncResult> {
|
||||
return AddonModChoiceSync.syncChoice(this.choice!.id, this.userId);
|
||||
}
|
||||
if (!this.choice) {
|
||||
throw new CoreError('Cannot sync without a choice.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if sync has succeed from result sync data.
|
||||
*
|
||||
* @param result Data returned on the sync function.
|
||||
* @returns Whether it succeed or not.
|
||||
*/
|
||||
protected hasSyncSucceed(result: AddonModChoiceSyncResult): boolean {
|
||||
return result.updated;
|
||||
return AddonModChoiceSync.syncChoice(this.choice.id, this.userId);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { CoreNetworkError } from '@classes/errors/network-error';
|
||||
import { CoreCourseActivitySyncBaseProvider } from '@features/course/classes/activity-sync';
|
||||
import { CoreSyncResult } from '@services/sync';
|
||||
import { CoreCourse } from '@features/course/services/course';
|
||||
import { CoreCourseLogHelper } from '@features/course/services/log-helper';
|
||||
import { CoreNetwork } from '@services/network';
|
||||
|
@ -217,10 +218,7 @@ export const AddonModChoiceSync = makeSingleton(AddonModChoiceSyncProvider);
|
|||
/**
|
||||
* Data returned by a choice sync.
|
||||
*/
|
||||
export type AddonModChoiceSyncResult = {
|
||||
warnings: string[]; // List of warnings.
|
||||
updated: boolean; // Whether some data was sent to the server or offline data was updated.
|
||||
};
|
||||
export type AddonModChoiceSyncResult = CoreSyncResult;
|
||||
|
||||
/**
|
||||
* Data passed to AUTO_SYNCED event.
|
||||
|
|
|
@ -77,15 +77,14 @@ export class AddonModChoiceProvider {
|
|||
choiceId: number,
|
||||
name: string,
|
||||
courseId: number,
|
||||
responses?: number[],
|
||||
responses: number[] = [],
|
||||
siteId?: string,
|
||||
): Promise<boolean> {
|
||||
siteId = siteId || CoreSites.getCurrentSiteId();
|
||||
responses = responses || [];
|
||||
|
||||
// Convenience function to store a message to be synchronized later.
|
||||
const storeOffline = async (): Promise<boolean> => {
|
||||
await AddonModChoiceOffline.saveResponse(choiceId, name, courseId, responses!, true, siteId);
|
||||
await AddonModChoiceOffline.saveResponse(choiceId, name, courseId, responses, true, siteId);
|
||||
|
||||
return false;
|
||||
};
|
||||
|
|
|
@ -519,24 +519,12 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp
|
|||
}
|
||||
|
||||
/**
|
||||
* Performs the sync of the activity.
|
||||
*
|
||||
* @returns Promise resolved when done.
|
||||
* @inheritdoc
|
||||
*/
|
||||
protected sync(): Promise<AddonModDataSyncResult> {
|
||||
return AddonModDataPrefetchHandler.sync(this.module, this.courseId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if sync has succeed from result sync data.
|
||||
*
|
||||
* @param result Data returned on the sync function.
|
||||
* @returns If suceed or not.
|
||||
*/
|
||||
protected hasSyncSucceed(result: AddonModDataSyncResult): boolean {
|
||||
return result.updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
|
|
|
@ -24,7 +24,7 @@ import { CoreRatingSync } from '@features/rating/services/rating-sync';
|
|||
import { CoreNetwork } from '@services/network';
|
||||
import { CoreFileEntry } from '@services/file-helper';
|
||||
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
|
||||
import { CoreSync } from '@services/sync';
|
||||
import { CoreSync, CoreSyncResult } from '@services/sync';
|
||||
import { CoreTextUtils } from '@services/utils/text';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { Translate, makeSingleton } from '@singletons';
|
||||
|
@ -477,10 +477,7 @@ export type AddonModDataSyncEntryResult = {
|
|||
/**
|
||||
* Data returned by a database sync.
|
||||
*/
|
||||
export type AddonModDataSyncResult = {
|
||||
warnings: string[]; // List of warnings.
|
||||
updated: boolean; // Whether some data was sent to the server or offline data was updated.
|
||||
};
|
||||
export type AddonModDataSyncResult = CoreSyncResult;
|
||||
|
||||
export type AddonModDataAutoSyncData = {
|
||||
dataId: number;
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
// limitations under the License.
|
||||
|
||||
import { Component, Input, Optional, ViewChild, OnInit, OnDestroy } from '@angular/core';
|
||||
import { CoreError } from '@classes/errors/error';
|
||||
import { CoreTabsComponent } from '@components/tabs/tabs';
|
||||
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
|
||||
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
|
||||
|
@ -477,14 +478,11 @@ export class AddonModFeedbackIndexComponent extends CoreCourseModuleMainActivity
|
|||
* @inheritdoc
|
||||
*/
|
||||
protected sync(): Promise<AddonModFeedbackSyncResult> {
|
||||
return AddonModFeedbackSync.syncFeedback(this.feedback!.id);
|
||||
}
|
||||
if (!this.feedback) {
|
||||
throw new CoreError('Cannot sync without a feedback.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
protected hasSyncSucceed(result: AddonModFeedbackSyncResult): boolean {
|
||||
return result.updated;
|
||||
return AddonModFeedbackSync.syncFeedback(this.feedback.id);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -20,7 +20,7 @@ import { CoreCourse, CoreCourseAnyModuleData } from '@features/course/services/c
|
|||
import { CoreCourseLogHelper } from '@features/course/services/log-helper';
|
||||
import { CoreNetwork } from '@services/network';
|
||||
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
|
||||
import { CoreSync } from '@services/sync';
|
||||
import { CoreSync, CoreSyncResult } from '@services/sync';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { makeSingleton, Translate } from '@singletons';
|
||||
import { CoreEvents } from '@singletons/events';
|
||||
|
@ -292,10 +292,7 @@ export const AddonModFeedbackSync = makeSingleton(AddonModFeedbackSyncProvider);
|
|||
/**
|
||||
* Data returned by a feedback sync.
|
||||
*/
|
||||
export type AddonModFeedbackSyncResult = {
|
||||
warnings: string[]; // List of warnings.
|
||||
updated: boolean; // Whether some data was sent to the server or offline data was updated.
|
||||
};
|
||||
export type AddonModFeedbackSyncResult = CoreSyncResult;
|
||||
|
||||
/**
|
||||
* Data passed to AUTO_SYNCED event.
|
||||
|
|
|
@ -500,24 +500,12 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
|||
}
|
||||
|
||||
/**
|
||||
* Performs the sync of the activity.
|
||||
*
|
||||
* @returns Promise resolved when done.
|
||||
* @inheritdoc
|
||||
*/
|
||||
protected sync(): Promise<AddonModForumSyncResult> {
|
||||
return AddonModForumPrefetchHandler.sync(this.module, this.courseId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if sync has succeed from result sync data.
|
||||
*
|
||||
* @param result Data returned on the sync function.
|
||||
* @returns Whether it succeed or not.
|
||||
*/
|
||||
protected hasSyncSucceed(result: AddonModForumSyncResult): boolean {
|
||||
return result.updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares sync event data with current data to check if refresh content is needed.
|
||||
*
|
||||
|
|
|
@ -62,7 +62,7 @@ export class AddonModForumHelperProvider {
|
|||
message: string,
|
||||
attachments?: CoreFileEntry[],
|
||||
options?: AddonModForumDiscussionOptions,
|
||||
groupIds?: number[],
|
||||
groupIds: number[] = [],
|
||||
timeCreated?: number,
|
||||
siteId?: string,
|
||||
): Promise<number[] | null> {
|
||||
|
@ -76,7 +76,7 @@ export class AddonModForumHelperProvider {
|
|||
// Convenience function to store a message to be synchronized later.
|
||||
const storeOffline = async (): Promise<void> => {
|
||||
// Multiple groups, the discussion is being posted to all groups.
|
||||
const groupId = groupIds!.length > 1 ? AddonModForumProvider.ALL_GROUPS : groupIds![0];
|
||||
const groupId = groupIds.length > 1 ? AddonModForumProvider.ALL_GROUPS : groupIds[0];
|
||||
|
||||
if (offlineAttachments && options) {
|
||||
options.attachmentsid = offlineAttachments;
|
||||
|
@ -182,7 +182,7 @@ export class AddonModForumHelperProvider {
|
|||
* @param siteId Site ID. If not defined, current site.
|
||||
* @returns Promise resolved with the object converted to Online.
|
||||
*/
|
||||
convertOfflineReplyToOnline(offlineReply: AddonModForumOfflineReply, siteId?: string): Promise<AddonModForumPost> {
|
||||
async convertOfflineReplyToOnline(offlineReply: AddonModForumOfflineReply, siteId?: string): Promise<AddonModForumPost> {
|
||||
const reply: AddonModForumPost = {
|
||||
id: -offlineReply.timecreated,
|
||||
discussionid: offlineReply.discussionid,
|
||||
|
@ -236,11 +236,11 @@ export class AddonModForumHelperProvider {
|
|||
),
|
||||
);
|
||||
|
||||
return Promise.all(promises).then(() => {
|
||||
reply.attachment = reply.attachments!.length > 0 ? 1 : 0;
|
||||
await Promise.all(promises);
|
||||
|
||||
return reply;
|
||||
});
|
||||
reply.attachment = reply.attachments?.length ? 1 : 0;
|
||||
|
||||
return reply;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -21,7 +21,7 @@ import { CoreRatingSync } from '@features/rating/services/rating-sync';
|
|||
import { CoreNetwork } from '@services/network';
|
||||
import { CoreGroups } from '@services/groups';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreSync } from '@services/sync';
|
||||
import { CoreSync, CoreSyncResult } from '@services/sync';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { makeSingleton, Translate } from '@singletons';
|
||||
import { CoreEvents } from '@singletons/events';
|
||||
|
@ -221,7 +221,7 @@ export class AddonModForumSyncProvider extends CoreCourseActivitySyncBaseProvide
|
|||
};
|
||||
|
||||
// Sync offline logs.
|
||||
const syncDiscussions = async (): Promise<{ warnings: string[]; updated: boolean }> => {
|
||||
const syncDiscussions = async (): Promise<AddonModForumSyncResult> => {
|
||||
await CoreUtils.ignoreErrors(
|
||||
CoreCourseLogHelper.syncActivity(AddonModForumProvider.COMPONENT, forumId, siteId),
|
||||
);
|
||||
|
@ -643,10 +643,7 @@ export const AddonModForumSync = makeSingleton(AddonModForumSyncProvider);
|
|||
/**
|
||||
* Result of forum sync.
|
||||
*/
|
||||
export type AddonModForumSyncResult = {
|
||||
updated: boolean;
|
||||
warnings: string[];
|
||||
};
|
||||
export type AddonModForumSyncResult = CoreSyncResult;
|
||||
|
||||
/**
|
||||
* Data passed to AUTO_SYNCED event.
|
||||
|
|
|
@ -22,7 +22,7 @@ import { CoreCourse, CoreCourseAnyModuleData, CoreCourseCommonModWSOptions } fro
|
|||
import { CoreUser } from '@features/user/services/user';
|
||||
import { CoreGroups, CoreGroupsProvider } from '@services/groups';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { AddonModForumSync } from '../forum-sync';
|
||||
import { AddonModForumSync, AddonModForumSyncResult } from '../forum-sync';
|
||||
import { makeSingleton } from '@singletons';
|
||||
import { CoreCourses } from '@features/courses/services/courses';
|
||||
|
||||
|
@ -341,11 +341,3 @@ export class AddonModForumPrefetchHandlerService extends CoreCourseActivityPrefe
|
|||
}
|
||||
|
||||
export const AddonModForumPrefetchHandler = makeSingleton(AddonModForumPrefetchHandlerService);
|
||||
|
||||
/**
|
||||
* Data returned by a forum sync.
|
||||
*/
|
||||
export type AddonModForumSyncResult = {
|
||||
warnings: string[]; // List of warnings.
|
||||
updated: boolean; // Whether some data was sent to the server or offline data was updated.
|
||||
};
|
||||
|
|
|
@ -215,24 +215,12 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
|
|||
}
|
||||
|
||||
/**
|
||||
* Performs the sync of the activity.
|
||||
*
|
||||
* @returns Promise resolved when done.
|
||||
* @inheritdoc
|
||||
*/
|
||||
protected sync(): Promise<AddonModGlossarySyncResult> {
|
||||
return AddonModGlossaryPrefetchHandler.sync(this.module, this.courseId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if sync has succeed from result sync data.
|
||||
*
|
||||
* @param result Data returned on the sync function.
|
||||
* @returns Whether it succeed or not.
|
||||
*/
|
||||
protected hasSyncSucceed(result: AddonModGlossarySyncResult): boolean {
|
||||
return result.updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares sync event data with current data to check if refresh content is needed.
|
||||
*
|
||||
|
|
|
@ -21,7 +21,7 @@ import { CoreCourseLogHelper } from '@features/course/services/log-helper';
|
|||
import { CoreRatingSync } from '@features/rating/services/rating-sync';
|
||||
import { CoreNetwork } from '@services/network';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreSync } from '@services/sync';
|
||||
import { CoreSync, CoreSyncResult } from '@services/sync';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { makeSingleton, Translate } from '@singletons';
|
||||
import { CoreEvents } from '@singletons/events';
|
||||
|
@ -344,10 +344,7 @@ export const AddonModGlossarySync = makeSingleton(AddonModGlossarySyncProvider);
|
|||
/**
|
||||
* Data returned by a glossary sync.
|
||||
*/
|
||||
export type AddonModGlossarySyncResult = {
|
||||
warnings: string[]; // List of warnings.
|
||||
updated: boolean; // Whether some data was sent to the server or offline data was updated.
|
||||
};
|
||||
export type AddonModGlossarySyncResult = CoreSyncResult;
|
||||
|
||||
/**
|
||||
* Data passed to AUTO_SYNCED event.
|
||||
|
|
|
@ -16,6 +16,7 @@ import { Injectable } from '@angular/core';
|
|||
|
||||
import { CoreNetworkError } from '@classes/errors/network-error';
|
||||
import { CoreCourseActivitySyncBaseProvider } from '@features/course/classes/activity-sync';
|
||||
import { CoreSyncResult } from '@services/sync';
|
||||
import { CoreCourseLogHelper } from '@features/course/services/log-helper';
|
||||
import { CoreXAPIOffline } from '@features/xapi/services/offline';
|
||||
import { CoreXAPI } from '@features/xapi/services/xapi';
|
||||
|
@ -197,10 +198,7 @@ export const AddonModH5PActivitySync = makeSingleton(AddonModH5PActivitySyncProv
|
|||
/**
|
||||
* Sync result.
|
||||
*/
|
||||
export type AddonModH5PActivitySyncResult = {
|
||||
updated: boolean;
|
||||
warnings: string[];
|
||||
};
|
||||
export type AddonModH5PActivitySyncResult = CoreSyncResult;
|
||||
|
||||
/**
|
||||
* Data passed to AUTO_SYNC event.
|
||||
|
|
|
@ -47,6 +47,7 @@ import {
|
|||
} from '../../services/lesson-sync';
|
||||
import { AddonModLessonModuleHandlerService } from '../../services/handlers/module';
|
||||
import { CoreTime } from '@singletons/time';
|
||||
import { CoreError } from '@classes/errors/error';
|
||||
|
||||
/**
|
||||
* Component that displays a lesson entry page.
|
||||
|
@ -270,10 +271,7 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo
|
|||
}
|
||||
|
||||
/**
|
||||
* Checks if sync has succeed from result sync data.
|
||||
*
|
||||
* @param result Data returned on the sync function.
|
||||
* @returns If suceed or not.
|
||||
* @inheritdoc
|
||||
*/
|
||||
protected hasSyncSucceed(result: AddonModLessonSyncResult): boolean {
|
||||
if (result.updated || this.dataSent) {
|
||||
|
@ -637,12 +635,14 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo
|
|||
}
|
||||
|
||||
/**
|
||||
* Performs the sync of the activity.
|
||||
*
|
||||
* @returns Promise resolved when done.
|
||||
* @inheritdoc
|
||||
*/
|
||||
protected async sync(): Promise<AddonModLessonSyncResult> {
|
||||
const result = await AddonModLessonSync.syncLesson(this.lesson!.id, true);
|
||||
if (!this.lesson) {
|
||||
throw new CoreError('Cannot sync without a lesson.');
|
||||
}
|
||||
|
||||
const result = await AddonModLessonSync.syncLesson(this.lesson.id, true);
|
||||
|
||||
if (!result.updated && this.dataSent && this.isPrefetched()) {
|
||||
// The user sent data to server, but not in the sync process. Check if we need to fetch data.
|
||||
|
|
|
@ -21,7 +21,7 @@ import { CoreCourse } from '@features/course/services/course';
|
|||
import { CoreCourseLogHelper } from '@features/course/services/log-helper';
|
||||
import { CoreNetwork } from '@services/network';
|
||||
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
|
||||
import { CoreSync } from '@services/sync';
|
||||
import { CoreSync, CoreSyncResult } from '@services/sync';
|
||||
import { CoreTimeUtils } from '@services/utils/time';
|
||||
import { CoreUrlUtils } from '@services/utils/url';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
|
@ -495,9 +495,7 @@ export const AddonModLessonSync = makeSingleton(AddonModLessonSyncProvider);
|
|||
/**
|
||||
* Data returned by a lesson sync.
|
||||
*/
|
||||
export type AddonModLessonSyncResult = {
|
||||
warnings: string[]; // List of warnings.
|
||||
updated: boolean; // Whether some data was sent to the server or offline data was updated.
|
||||
export type AddonModLessonSyncResult = CoreSyncResult & {
|
||||
courseId?: number; // Course the lesson belongs to (if known).
|
||||
};
|
||||
|
||||
|
|
|
@ -418,10 +418,7 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
|
|||
}
|
||||
|
||||
/**
|
||||
* Checks if sync has succeed from result sync data.
|
||||
*
|
||||
* @param result Data returned on the sync function.
|
||||
* @returns If suceed or not.
|
||||
* @inheritdoc
|
||||
*/
|
||||
protected hasSyncSucceed(result: AddonModQuizSyncResult): boolean {
|
||||
if (result.attemptFinished) {
|
||||
|
@ -553,9 +550,7 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
|
|||
}
|
||||
|
||||
/**
|
||||
* Performs the sync of the activity.
|
||||
*
|
||||
* @returns Promise resolved when done.
|
||||
* @inheritdoc
|
||||
*/
|
||||
protected async sync(): Promise<AddonModQuizSyncResult> {
|
||||
if (!this.candidateQuiz) {
|
||||
|
|
|
@ -23,7 +23,7 @@ import { CoreQuestion, CoreQuestionQuestionParsed } from '@features/question/ser
|
|||
import { CoreQuestionDelegate } from '@features/question/services/question-delegate';
|
||||
import { CoreNetwork } from '@services/network';
|
||||
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
|
||||
import { CoreSync } from '@services/sync';
|
||||
import { CoreSync, CoreSyncResult } from '@services/sync';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { makeSingleton, Translate } from '@singletons';
|
||||
import { CoreEvents } from '@singletons/events';
|
||||
|
@ -482,10 +482,8 @@ export const AddonModQuizSync = makeSingleton(AddonModQuizSyncProvider);
|
|||
/**
|
||||
* Data returned by a quiz sync.
|
||||
*/
|
||||
export type AddonModQuizSyncResult = {
|
||||
warnings: string[]; // List of warnings.
|
||||
export type AddonModQuizSyncResult = CoreSyncResult & {
|
||||
attemptFinished: boolean; // Whether an attempt was finished in the site due to the sync.
|
||||
updated: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -99,7 +99,7 @@ export class AddonModQuizProvider {
|
|||
* @returns Grade to display.
|
||||
*/
|
||||
formatGrade(grade?: number | null, decimals?: number): string {
|
||||
if (grade === undefined || grade == -1 || grade === null || isNaN(grade)) {
|
||||
if (grade === undefined || grade === -1 || grade === null || isNaN(grade)) {
|
||||
return Translate.instant('addon.mod_quiz.notyetgraded');
|
||||
}
|
||||
|
||||
|
@ -1800,7 +1800,7 @@ export class AddonModQuizProvider {
|
|||
): string | undefined {
|
||||
let grade: number | undefined;
|
||||
|
||||
const rawGradeNum = typeof rawGrade == 'string' ? parseFloat(rawGrade) : rawGrade;
|
||||
const rawGradeNum = typeof rawGrade === 'string' ? parseFloat(rawGrade) : rawGrade;
|
||||
if (rawGradeNum !== undefined && rawGradeNum !== null && !isNaN(rawGradeNum)) {
|
||||
if (quiz.sumgrades && quiz.sumgrades >= 0.000005) {
|
||||
grade = rawGradeNum * (quiz.grade ?? 0) / quiz.sumgrades;
|
||||
|
|
|
@ -143,7 +143,7 @@ export class AddonModResourceIndexComponent extends CoreCourseModuleMainResource
|
|||
this.displayDescription = false;
|
||||
|
||||
this.warning = downloadResult.failed
|
||||
? this.getErrorDownloadingSomeFilesMessage(downloadResult.error!)
|
||||
? this.getErrorDownloadingSomeFilesMessage(downloadResult.error ?? '')
|
||||
: '';
|
||||
|
||||
return;
|
||||
|
|
|
@ -61,8 +61,8 @@ export class AddonModResourcePrefetchHandlerService extends CoreCourseResourcePr
|
|||
async downloadOrPrefetch(module: CoreCourseModuleData, courseId: number, prefetch?: boolean): Promise<void> {
|
||||
let dirPath: string | undefined;
|
||||
|
||||
if (AddonModResourceHelper.isDisplayedInIframe(module)) {
|
||||
dirPath = await CoreFilepool.getPackageDirPathByUrl(CoreSites.getCurrentSiteId(), module.url!);
|
||||
if (AddonModResourceHelper.isDisplayedInIframe(module) && module.url !== undefined) {
|
||||
dirPath = await CoreFilepool.getPackageDirPathByUrl(CoreSites.getCurrentSiteId(), module.url);
|
||||
}
|
||||
|
||||
const promises: Promise<unknown>[] = [];
|
||||
|
|
|
@ -62,7 +62,7 @@ export class AddonModResourceHelperProvider {
|
|||
* @returns Promise resolved with the iframe src.
|
||||
*/
|
||||
async getIframeSrc(module: CoreCourseModuleData): Promise<string> {
|
||||
if (!module.contents?.length) {
|
||||
if (!module.contents?.length || module.url === undefined) {
|
||||
throw new CoreError('No contents available in module');
|
||||
}
|
||||
|
||||
|
@ -74,7 +74,7 @@ export class AddonModResourceHelperProvider {
|
|||
}
|
||||
|
||||
try {
|
||||
const dirPath = await CoreFilepool.getPackageDirUrlByUrl(CoreSites.getCurrentSiteId(), module.url!);
|
||||
const dirPath = await CoreFilepool.getPackageDirUrlByUrl(CoreSites.getCurrentSiteId(), module.url);
|
||||
|
||||
// This URL is going to be injected in an iframe, we need trustAsResourceUrl to make it work in a browser.
|
||||
return CorePath.concatenatePaths(dirPath, mainFilePath);
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
|
||||
import { CoreConstants } from '@/core/constants';
|
||||
import { Component, Input, OnInit, Optional } from '@angular/core';
|
||||
import { CoreError } from '@classes/errors/error';
|
||||
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
|
||||
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
|
||||
import { CoreCourse } from '@features/course/services/course';
|
||||
|
@ -59,7 +60,10 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom
|
|||
moduleName = 'scorm';
|
||||
|
||||
scorm?: AddonModScormScorm; // The SCORM object.
|
||||
currentOrganization: Partial<AddonModScormOrganization> = {}; // Selected organization.
|
||||
currentOrganization: Partial<AddonModScormOrganization> & { identifier: string} = {
|
||||
identifier: '',
|
||||
}; // Selected organization.
|
||||
|
||||
startNewAttempt = false;
|
||||
errorMessage?: string; // Error message.
|
||||
syncTime?: string; // Last sync time.
|
||||
|
@ -70,7 +74,7 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom
|
|||
percentage?: string; // Download/unzip percentage.
|
||||
showPercentage = false; // Whether to show the percentage.
|
||||
progressMessage?: string; // Message about download/unzip.
|
||||
organizations?: AddonModScormOrganization[]; // List of organizations.
|
||||
organizations: AddonModScormOrganization[] = []; // List of organizations.
|
||||
loadingToc = false; // Whether the TOC is being loaded.
|
||||
toc?: AddonModScormTOCScoWithIcon[]; // Table of contents (structure).
|
||||
accessInfo?: AddonModScormGetScormAccessInformationWSResponse; // Access information.
|
||||
|
@ -128,7 +132,7 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom
|
|||
|
||||
try {
|
||||
await AddonModScormPrefetchHandler.download(this.module, this.courseId, undefined, (data) => {
|
||||
if (!data) {
|
||||
if (!data || !this.scorm) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -137,8 +141,8 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom
|
|||
|
||||
if (data.downloading) {
|
||||
// Downloading package.
|
||||
if (this.scorm!.packagesize && data.progress) {
|
||||
const percentageNumber = Number(data.progress.loaded / this.scorm!.packagesize) * 100;
|
||||
if (this.scorm.packagesize && data.progress) {
|
||||
const percentageNumber = Number(data.progress.loaded / this.scorm.packagesize) * 100;
|
||||
this.percentage = percentageNumber.toFixed(1);
|
||||
this.showPercentage = percentageNumber >= 0 && percentageNumber <= 100;
|
||||
}
|
||||
|
@ -198,7 +202,7 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom
|
|||
(
|
||||
this.accessInfo.canskipview && !this.accessInfo.canviewreport &&
|
||||
(this.scorm.skipview ?? 0) >= AddonModScormProvider.SKIPVIEW_FIRST &&
|
||||
(this.scorm.skipview == AddonModScormProvider.SKIPVIEW_ALWAYS || this.lastAttempt == 0)
|
||||
(this.scorm.skipview === AddonModScormProvider.SKIPVIEW_ALWAYS || this.lastAttempt === 0)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -221,7 +225,7 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom
|
|||
this.lastAttempt = attempt.num;
|
||||
this.lastIsOffline = attempt.offline;
|
||||
|
||||
if (this.lastAttempt != this.attempts.lastAttempt.num) {
|
||||
if (this.lastAttempt !== this.attempts.lastAttempt.num) {
|
||||
this.attemptToContinue = this.lastAttempt;
|
||||
} else {
|
||||
this.attemptToContinue = undefined;
|
||||
|
@ -237,7 +241,7 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom
|
|||
this.gradeMethodReadable = AddonModScorm.getScormGradeMethod(scorm);
|
||||
this.attemptsLeft = AddonModScorm.countAttemptsLeft(scorm, this.attempts.lastAttempt.num);
|
||||
|
||||
if (scorm.forcenewattempt == AddonModScormProvider.SCORM_FORCEATTEMPT_ALWAYS ||
|
||||
if (scorm.forcenewattempt === AddonModScormProvider.SCORM_FORCEATTEMPT_ALWAYS ||
|
||||
(scorm.forcenewattempt && !this.incomplete)) {
|
||||
this.startNewAttempt = true;
|
||||
}
|
||||
|
@ -272,13 +276,9 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom
|
|||
protected async fetchStructure(scorm: AddonModScormScorm): Promise<void> {
|
||||
this.organizations = await AddonModScorm.getOrganizations(scorm.id, { cmId: this.module.id });
|
||||
|
||||
if (!this.currentOrganization.identifier) {
|
||||
if (this.currentOrganization.identifier === '' && this.organizations[0]?.identifier) {
|
||||
// Load first organization (if any).
|
||||
if (this.organizations.length) {
|
||||
this.currentOrganization.identifier = this.organizations[0].identifier;
|
||||
} else {
|
||||
this.currentOrganization.identifier = '';
|
||||
}
|
||||
this.currentOrganization.identifier = this.organizations[0].identifier;
|
||||
}
|
||||
|
||||
return this.loadOrganizationToc(scorm, this.currentOrganization.identifier);
|
||||
|
@ -297,7 +297,11 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom
|
|||
offline: boolean,
|
||||
attempts: Record<number, AddonModScormAttemptGrade>,
|
||||
): Promise<void> {
|
||||
const grade = await AddonModScorm.getAttemptGrade(this.scorm!, attempt, offline);
|
||||
if (!this.scorm) {
|
||||
return;
|
||||
}
|
||||
|
||||
const grade = await AddonModScorm.getAttemptGrade(this.scorm, attempt, offline);
|
||||
|
||||
attempts[attempt] = {
|
||||
num: attempt,
|
||||
|
@ -361,10 +365,7 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom
|
|||
}
|
||||
|
||||
/**
|
||||
* Checks if sync has succeed from result sync data.
|
||||
*
|
||||
* @param result Data returned on the sync function.
|
||||
* @returns If suceed or not.
|
||||
* @inheritdoc
|
||||
*/
|
||||
protected hasSyncSucceed(result: AddonModScormSyncResult): boolean {
|
||||
if (result.updated || this.dataSent) {
|
||||
|
@ -374,7 +375,7 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom
|
|||
|
||||
this.dataSent = false;
|
||||
|
||||
return true;
|
||||
return result.updated;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -438,8 +439,12 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom
|
|||
* Load a organization's TOC.
|
||||
*/
|
||||
async loadOrganization(): Promise<void> {
|
||||
if (!this.scorm) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.loadOrganizationToc(this.scorm!, this.currentOrganization.identifier!);
|
||||
await this.loadOrganizationToc(this.scorm, this.currentOrganization.identifier);
|
||||
} catch (error) {
|
||||
CoreDomUtils.showErrorModalDefault(error, this.fetchContentDefaultError, true);
|
||||
}
|
||||
|
@ -453,7 +458,7 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom
|
|||
* @returns Promise resolved when done.
|
||||
*/
|
||||
protected async loadOrganizationToc(scorm: AddonModScormScorm, organizationId: string): Promise<void> {
|
||||
if (!scorm.displaycoursestructure) {
|
||||
if (!scorm.displaycoursestructure || this.lastAttempt === undefined) {
|
||||
// TOC is not displayed, no need to load it.
|
||||
return;
|
||||
}
|
||||
|
@ -461,18 +466,17 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom
|
|||
this.loadingToc = true;
|
||||
|
||||
try {
|
||||
this.toc = await AddonModScormHelper.getToc(scorm.id, this.lastAttempt!, this.incomplete, {
|
||||
this.toc = await AddonModScormHelper.getToc(scorm.id, this.lastAttempt, this.incomplete, {
|
||||
organization: organizationId,
|
||||
offline: this.lastIsOffline,
|
||||
cmId: this.module.id,
|
||||
});
|
||||
|
||||
// Search organization title.
|
||||
this.organizations!.forEach((org) => {
|
||||
if (org.identifier == organizationId) {
|
||||
this.currentOrganization.title = org.title;
|
||||
}
|
||||
});
|
||||
const organization = this.organizations.find((org) => org.identifier === organizationId);
|
||||
if (organization) {
|
||||
this.currentOrganization.title = organization.title;
|
||||
}
|
||||
} finally {
|
||||
this.loadingToc = false;
|
||||
}
|
||||
|
@ -486,20 +490,18 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom
|
|||
* @param scoId SCO that needs to be loaded when the SCORM is opened. If not defined, load first SCO.
|
||||
*/
|
||||
async open(event?: Event, preview: boolean = false, scoId?: number): Promise<void> {
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
event?.preventDefault();
|
||||
event?.stopPropagation();
|
||||
|
||||
if (this.downloading) {
|
||||
if (this.downloading || !this.scorm) {
|
||||
// Scope is being downloaded, abort.
|
||||
return;
|
||||
}
|
||||
|
||||
const isOutdated = this.currentStatus == CoreConstants.OUTDATED;
|
||||
const scorm = this.scorm!;
|
||||
const isOutdated = this.currentStatus === CoreConstants.OUTDATED;
|
||||
const scorm = this.scorm;
|
||||
|
||||
if (!isOutdated && this.currentStatus != CoreConstants.NOT_DOWNLOADED) {
|
||||
if (!isOutdated && this.currentStatus !== CoreConstants.NOT_DOWNLOADED) {
|
||||
// Already downloaded, open it.
|
||||
this.openScorm(scoId, preview);
|
||||
|
||||
|
@ -552,7 +554,7 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom
|
|||
this.dataSentObserver?.off();
|
||||
|
||||
this.dataSentObserver = CoreEvents.on(AddonModScormProvider.DATA_SENT_EVENT, (data) => {
|
||||
if (data.scormId === this.scorm!.id) {
|
||||
if (data.scormId === this.scorm?.id) {
|
||||
this.dataSent = true;
|
||||
|
||||
if (this.module.completiondata && CoreCourse.isIncompleteAutomaticCompletion(this.module.completiondata)) {
|
||||
|
@ -581,14 +583,14 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom
|
|||
*/
|
||||
protected async showStatus(status: string): Promise<void> {
|
||||
|
||||
if (status == CoreConstants.OUTDATED && this.scorm) {
|
||||
if (status === CoreConstants.OUTDATED && this.scorm) {
|
||||
// Only show the outdated message if the file should be downloaded.
|
||||
const download = await AddonModScorm.shouldDownloadMainFile(this.scorm, true);
|
||||
|
||||
this.statusMessage = download ? 'addon.mod_scorm.scormstatusoutdated' : '';
|
||||
} else if (status == CoreConstants.NOT_DOWNLOADED) {
|
||||
} else if (status === CoreConstants.NOT_DOWNLOADED) {
|
||||
this.statusMessage = 'addon.mod_scorm.scormstatusnotdownloaded';
|
||||
} else if (status == CoreConstants.DOWNLOADING) {
|
||||
} else if (status === CoreConstants.DOWNLOADING) {
|
||||
if (!this.downloading) {
|
||||
// It's being downloaded right now but the view isn't tracking it. "Restore" the download.
|
||||
this.downloadScormPackage();
|
||||
|
@ -605,14 +607,18 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom
|
|||
* @returns Promise resolved when done.
|
||||
*/
|
||||
protected async sync(retries = 0): Promise<AddonModScormSyncResult> {
|
||||
if (CoreSync.isBlocked(AddonModScormProvider.COMPONENT, this.scorm!.id) && retries < 5) {
|
||||
if (!this.scorm) {
|
||||
throw new CoreError('Cannot sync without a scorm.');
|
||||
}
|
||||
|
||||
if (CoreSync.isBlocked(AddonModScormProvider.COMPONENT, this.scorm.id) && retries < 5) {
|
||||
// Sync is currently blocked, this can happen when SCORM player is left. Retry in a bit.
|
||||
await CoreUtils.wait(400);
|
||||
|
||||
return this.sync(retries + 1);
|
||||
}
|
||||
|
||||
const result = await AddonModScormSync.syncScorm(this.scorm!);
|
||||
const result = await AddonModScormSync.syncScorm(this.scorm);
|
||||
|
||||
if (!result.updated && this.dataSent) {
|
||||
// The user sent data to server, but not in the sync process. Check if we need to fetch data.
|
||||
|
|
|
@ -18,7 +18,7 @@ import { CoreCourseActivitySyncBaseProvider } from '@features/course/classes/act
|
|||
import { CoreCourse } from '@features/course/services/course';
|
||||
import { CoreCourseLogHelper } from '@features/course/services/log-helper';
|
||||
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
|
||||
import { CoreSync } from '@services/sync';
|
||||
import { CoreSync, CoreSyncResult } from '@services/sync';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { makeSingleton, Translate } from '@singletons';
|
||||
import { CoreEvents } from '@singletons/events';
|
||||
|
@ -841,18 +841,14 @@ export const AddonModScormSync = makeSingleton(AddonModScormSyncProvider);
|
|||
/**
|
||||
* Data returned by a SCORM sync.
|
||||
*/
|
||||
export type AddonModScormSyncResult = {
|
||||
warnings: string[]; // List of warnings.
|
||||
export type AddonModScormSyncResult = CoreSyncResult & {
|
||||
attemptFinished: boolean; // Whether an attempt was finished in the site due to the sync,
|
||||
updated: boolean; // Whether some data was sent to the site.
|
||||
};
|
||||
|
||||
/**
|
||||
* Auto sync event data.
|
||||
*/
|
||||
export type AddonModScormAutoSyncEventData = {
|
||||
export type AddonModScormAutoSyncEventData = CoreSyncResult & {
|
||||
scormId: number;
|
||||
attemptFinished: boolean;
|
||||
warnings: string[];
|
||||
updated: boolean;
|
||||
};
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
// limitations under the License.
|
||||
|
||||
import { Component, OnInit, Optional } from '@angular/core';
|
||||
import { CoreError } from '@classes/errors/error';
|
||||
import { CoreIonLoadingElement } from '@classes/ion-loading';
|
||||
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
|
||||
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
|
||||
|
@ -117,8 +118,8 @@ export class AddonModSurveyIndexComponent extends CoreCourseModuleMainActivityCo
|
|||
|
||||
if (sync) {
|
||||
// Try to synchronize the survey.
|
||||
const answersSent = await this.syncActivity(showErrors);
|
||||
if (answersSent) {
|
||||
const updated = await this.syncActivity(showErrors);
|
||||
if (updated) {
|
||||
// Answers were sent, update the survey.
|
||||
this.survey = await AddonModSurvey.getSurvey(this.courseId, this.module.id);
|
||||
}
|
||||
|
@ -130,17 +131,18 @@ export class AddonModSurveyIndexComponent extends CoreCourseModuleMainActivityCo
|
|||
: await AddonModSurveyOffline.hasAnswers(this.survey.id);
|
||||
|
||||
if (!this.survey.surveydone && !this.hasOffline) {
|
||||
await this.fetchQuestions();
|
||||
await this.fetchQuestions(this.survey.id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to get survey questions.
|
||||
*
|
||||
* @param surveyId Survey Id.
|
||||
* @returns Promise resolved when done.
|
||||
*/
|
||||
protected async fetchQuestions(): Promise<void> {
|
||||
const questions = await AddonModSurvey.getQuestions(this.survey!.id, { cmId: this.module.id });
|
||||
protected async fetchQuestions(surveyId: number): Promise<void> {
|
||||
const questions = await AddonModSurvey.getQuestions(surveyId, { cmId: this.module.id });
|
||||
|
||||
this.questions = AddonModSurveyHelper.formatQuestions(questions);
|
||||
|
||||
|
@ -183,6 +185,10 @@ export class AddonModSurveyIndexComponent extends CoreCourseModuleMainActivityCo
|
|||
* Save options selected.
|
||||
*/
|
||||
async submit(): Promise<void> {
|
||||
if (!this.survey) {
|
||||
return;
|
||||
}
|
||||
|
||||
let modal: CoreIonLoadingElement | undefined;
|
||||
|
||||
try {
|
||||
|
@ -198,7 +204,7 @@ export class AddonModSurveyIndexComponent extends CoreCourseModuleMainActivityCo
|
|||
});
|
||||
}
|
||||
|
||||
const online = await AddonModSurvey.submitAnswers(this.survey!.id, this.survey!.name, this.courseId, answers);
|
||||
const online = await AddonModSurvey.submitAnswers(this.survey.id, this.survey.name, this.courseId, answers);
|
||||
|
||||
CoreEvents.trigger(CoreEvents.ACTIVITY_DATA_SENT, { module: this.moduleName });
|
||||
|
||||
|
@ -231,22 +237,14 @@ export class AddonModSurveyIndexComponent extends CoreCourseModuleMainActivityCo
|
|||
}
|
||||
|
||||
/**
|
||||
* Performs the sync of the activity.
|
||||
*
|
||||
* @returns Promise resolved when done.
|
||||
* @inheritdoc
|
||||
*/
|
||||
protected sync(): Promise<AddonModSurveySyncResult> {
|
||||
return AddonModSurveySync.syncSurvey(this.survey!.id, this.currentUserId);
|
||||
}
|
||||
protected async sync(): Promise<AddonModSurveySyncResult> {
|
||||
if (!this.survey) {
|
||||
throw new CoreError('Cannot sync without a survey.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if sync has succeed from result sync data.
|
||||
*
|
||||
* @param result Data returned on the sync function.
|
||||
* @returns If suceed or not.
|
||||
*/
|
||||
protected hasSyncSucceed(result: AddonModSurveySyncResult): boolean {
|
||||
return result.answersSent;
|
||||
return AddonModSurveySync.syncSurvey(this.survey.id, this.currentUserId);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { CoreNetworkError } from '@classes/errors/network-error';
|
||||
import { CoreCourseActivitySyncBaseProvider } from '@features/course/classes/activity-sync';
|
||||
import { CoreSyncResult } from '@services/sync';
|
||||
import { CoreCourse } from '@features/course/services/course';
|
||||
import { CoreCourseLogHelper } from '@features/course/services/log-helper';
|
||||
import { CoreNetwork } from '@services/network';
|
||||
|
@ -80,7 +81,7 @@ export class AddonModSurveySyncProvider extends CoreCourseActivitySyncBaseProvid
|
|||
? this.syncSurvey(entry.surveyid, entry.userid, siteId)
|
||||
: this.syncSurveyIfNeeded(entry.surveyid, entry.userid, siteId));
|
||||
|
||||
if (result && result.answersSent) {
|
||||
if (result && result.updated) {
|
||||
// Sync successful, send event.
|
||||
CoreEvents.trigger(AddonModSurveySyncProvider.AUTO_SYNCED, {
|
||||
surveyId: entry.surveyid,
|
||||
|
@ -150,7 +151,7 @@ export class AddonModSurveySyncProvider extends CoreCourseActivitySyncBaseProvid
|
|||
protected async performSyncSurvey(surveyId: number, userId: number, siteId: string): Promise<AddonModSurveySyncResult> {
|
||||
const result: AddonModSurveySyncResult = {
|
||||
warnings: [],
|
||||
answersSent: false,
|
||||
updated: false,
|
||||
};
|
||||
|
||||
// Sync offline logs.
|
||||
|
@ -179,7 +180,7 @@ export class AddonModSurveySyncProvider extends CoreCourseActivitySyncBaseProvid
|
|||
try {
|
||||
await AddonModSurvey.submitAnswersOnline(surveyId, data.answers, siteId);
|
||||
|
||||
result.answersSent = true;
|
||||
result.updated = true;
|
||||
|
||||
// Answers sent, delete them.
|
||||
await AddonModSurveyOffline.deleteSurveyAnswers(surveyId, siteId, userId);
|
||||
|
@ -190,7 +191,7 @@ export class AddonModSurveySyncProvider extends CoreCourseActivitySyncBaseProvid
|
|||
}
|
||||
|
||||
// The WebService has thrown an error, this means that answers cannot be submitted. Delete them.
|
||||
result.answersSent = true;
|
||||
result.updated = true;
|
||||
|
||||
await AddonModSurveyOffline.deleteSurveyAnswers(surveyId, siteId, userId);
|
||||
|
||||
|
@ -236,9 +237,7 @@ declare module '@singletons/events' {
|
|||
/**
|
||||
* Data returned by a assign sync.
|
||||
*/
|
||||
export type AddonModSurveySyncResult = {
|
||||
warnings: string[]; // List of warnings.
|
||||
answersSent: boolean; // Whether some data was sent to the server or offline data was updated.
|
||||
export type AddonModSurveySyncResult = CoreSyncResult & {
|
||||
courseId?: number; // Course the survey belongs to (if known).
|
||||
};
|
||||
|
||||
|
|
|
@ -189,7 +189,11 @@ export class AddonModUrlIndexComponent extends CoreCourseModuleMainResourceCompo
|
|||
*/
|
||||
go(): void {
|
||||
this.logView();
|
||||
AddonModUrlHelper.open(this.url!);
|
||||
if (!this.url) {
|
||||
return;
|
||||
}
|
||||
|
||||
AddonModUrlHelper.open(this.url);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -707,12 +707,9 @@ export class AddonModWikiIndexComponent extends CoreCourseModuleMainActivityComp
|
|||
}
|
||||
|
||||
/**
|
||||
* Checks if sync has succeed from result sync data.
|
||||
*
|
||||
* @param result Data returned on the sync function.
|
||||
* @returns If suceed or not.
|
||||
* @inheritdoc
|
||||
*/
|
||||
protected hasSyncSucceed(result: AddonModWikiSyncWikiResult | undefined): boolean {
|
||||
protected hasSyncSucceed(result: AddonModWikiSyncWikiResult): boolean {
|
||||
if (!result) {
|
||||
return false;
|
||||
}
|
||||
|
@ -843,13 +840,11 @@ export class AddonModWikiIndexComponent extends CoreCourseModuleMainActivityComp
|
|||
}
|
||||
|
||||
/**
|
||||
* Performs the sync of the activity.
|
||||
*
|
||||
* @returns Promise resolved when done.
|
||||
* @inheritdoc
|
||||
*/
|
||||
protected async sync(): Promise<AddonModWikiSyncWikiResult | undefined> {
|
||||
protected async sync(): Promise<AddonModWikiSyncWikiResult> {
|
||||
if (!this.wiki) {
|
||||
return;
|
||||
throw new CoreError('Cannot sync without a wiki.');
|
||||
}
|
||||
|
||||
return AddonModWikiSync.syncWiki(this.wiki.id, this.courseId, this.wiki.coursemodule);
|
||||
|
|
|
@ -45,8 +45,8 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy, CanLeave {
|
|||
cmId?: number; // Course module ID.
|
||||
courseId?: number; // Course the wiki belongs to.
|
||||
title?: string; // Title to display.
|
||||
pageForm?: FormGroup; // The form group.
|
||||
contentControl?: FormControl; // The FormControl for the page content.
|
||||
pageForm: FormGroup; // The form group.
|
||||
contentControl: FormControl; // The FormControl for the page content.
|
||||
canEditTitle = false; // Whether title can be edited.
|
||||
loaded = false; // Whether the data has been loaded.
|
||||
component = AddonModWikiProvider.COMPONENT; // Component to link the files to.
|
||||
|
@ -71,7 +71,10 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy, CanLeave {
|
|||
|
||||
constructor(
|
||||
protected formBuilder: FormBuilder,
|
||||
) { }
|
||||
) {
|
||||
this.contentControl = this.formBuilder.control('');
|
||||
this.pageForm = this.formBuilder.group({});
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
|
@ -96,10 +99,7 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy, CanLeave {
|
|||
this.blockId = AddonModWikiSync.getSubwikiBlockId(this.subwikiId, this.wikiId, this.userId, this.groupId);
|
||||
|
||||
// Create the form group and its controls.
|
||||
this.contentControl = this.formBuilder.control('');
|
||||
this.pageForm = this.formBuilder.group({
|
||||
title: pageTitle,
|
||||
});
|
||||
this.pageForm.addControl('title', this.formBuilder.control(pageTitle));
|
||||
this.pageForm.addControl('text', this.contentControl);
|
||||
|
||||
// Block the wiki so it cannot be synced.
|
||||
|
@ -121,7 +121,7 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy, CanLeave {
|
|||
if (success && !this.isDestroyed) {
|
||||
// Block the subwiki now that we have blockId for sure.
|
||||
const newBlockId = AddonModWikiSync.getSubwikiBlockId(this.subwikiId, this.wikiId, this.userId, this.groupId);
|
||||
if (newBlockId != this.blockId) {
|
||||
if (newBlockId !== this.blockId) {
|
||||
CoreSync.unblockOperation(this.component, this.blockId);
|
||||
this.blockId = newBlockId;
|
||||
CoreSync.blockOperation(this.component, this.blockId);
|
||||
|
@ -143,7 +143,7 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy, CanLeave {
|
|||
|
||||
try {
|
||||
// Wait for sync to be over (if any).
|
||||
const syncResult = await AddonModWikiSync.waitForSync(this.blockId!);
|
||||
const syncResult = this.blockId ? await AddonModWikiSync.waitForSync(this.blockId) : undefined;
|
||||
|
||||
if (this.pageId) {
|
||||
// Editing a page that already exists.
|
||||
|
@ -154,7 +154,7 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy, CanLeave {
|
|||
// Get page contents to obtain title and editing permission
|
||||
const pageContents = await AddonModWiki.getPageContents(this.pageId, { cmId: this.cmId });
|
||||
|
||||
this.pageForm!.controls.title.setValue(pageContents.title); // Set the title in the form group.
|
||||
this.pageForm.controls.title.setValue(pageContents.title); // Set the title in the form group.
|
||||
this.wikiId = pageContents.wikiid;
|
||||
this.subwikiId = pageContents.subwikiid;
|
||||
this.title = Translate.instant('addon.mod_wiki.editingpage', { $a: pageContents.title });
|
||||
|
@ -177,7 +177,7 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy, CanLeave {
|
|||
// Get the original page contents, treating file URLs if needed.
|
||||
const content = CoreTextUtils.replacePluginfileUrls(editContents.content || '', this.subwikiFiles);
|
||||
|
||||
this.contentControl!.setValue(content);
|
||||
this.contentControl.setValue(content);
|
||||
this.originalContent = content;
|
||||
this.version = editContents.version;
|
||||
|
||||
|
@ -188,7 +188,7 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy, CanLeave {
|
|||
}, AddonModWikiProvider.RENEW_LOCK_TIME);
|
||||
}
|
||||
} else {
|
||||
const pageTitle = this.pageForm!.controls.title.value;
|
||||
const pageTitle = this.pageForm.controls.title.value;
|
||||
this.editing = false;
|
||||
canEdit = !!this.blockId; // If no blockId, the user cannot edit the page.
|
||||
|
||||
|
@ -222,7 +222,7 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy, CanLeave {
|
|||
|
||||
if (page) {
|
||||
// Load offline content.
|
||||
this.contentControl!.setValue(page.cachedcontent);
|
||||
this.contentControl.setValue(page.cachedcontent);
|
||||
this.originalContent = page.cachedcontent;
|
||||
this.editOffline = true;
|
||||
} else {
|
||||
|
@ -286,13 +286,17 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy, CanLeave {
|
|||
* @param title Page title.
|
||||
*/
|
||||
protected goToPage(title: string): void {
|
||||
if (!this.wikiId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Not the firstpage.
|
||||
AddonModWiki.setEditedPageData({
|
||||
cmId: this.cmId,
|
||||
courseId: this.courseId,
|
||||
pageId: this.pageId,
|
||||
pageTitle: title,
|
||||
wikiId: this.wikiId!,
|
||||
wikiId: this.wikiId,
|
||||
subwikiId: this.subwikiId,
|
||||
userId: this.userId,
|
||||
groupId: this.groupId,
|
||||
|
@ -307,7 +311,7 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy, CanLeave {
|
|||
* @returns Whether data has changed.
|
||||
*/
|
||||
protected hasDataChanged(): boolean {
|
||||
const values = this.pageForm!.value;
|
||||
const values = this.pageForm.value;
|
||||
|
||||
return !(this.originalContent == values.text || (!this.editing && !values.text && !values.title));
|
||||
}
|
||||
|
@ -348,7 +352,7 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy, CanLeave {
|
|||
* @returns Promise resolved when done.
|
||||
*/
|
||||
async save(): Promise<void> {
|
||||
const values = this.pageForm!.value;
|
||||
const values = this.pageForm.value;
|
||||
const title = values.title;
|
||||
let text = values.text;
|
||||
|
||||
|
@ -358,14 +362,14 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy, CanLeave {
|
|||
text = CoreTextUtils.formatHtmlLines(text);
|
||||
|
||||
try {
|
||||
if (this.editing) {
|
||||
if (this.editing && this.pageId) {
|
||||
// Edit existing page.
|
||||
await AddonModWiki.editPage(this.pageId!, text, this.section);
|
||||
await AddonModWiki.editPage(this.pageId, text, this.section);
|
||||
|
||||
CoreForms.triggerFormSubmittedEvent(this.formElement, true, CoreSites.getCurrentSiteId());
|
||||
|
||||
// Invalidate page since it changed.
|
||||
await AddonModWiki.invalidatePage(this.pageId!);
|
||||
await AddonModWiki.invalidatePage(this.pageId);
|
||||
|
||||
return this.goToPage(title);
|
||||
}
|
||||
|
@ -451,7 +455,11 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy, CanLeave {
|
|||
* Renew lock and control versions.
|
||||
*/
|
||||
protected async renewLock(): Promise<void> {
|
||||
const response = await AddonModWiki.getPageForEditing(this.pageId!, this.section, true);
|
||||
if (!this.pageId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await AddonModWiki.getPageForEditing(this.pageId, this.section, true);
|
||||
|
||||
if (response.version && this.version != response.version) {
|
||||
this.wrongVersionLock = true;
|
||||
|
|
|
@ -103,6 +103,10 @@ export class AddonModWikiCreateLinkHandlerService extends CoreContentLinksHandle
|
|||
|
||||
try {
|
||||
const route = CoreNavigator.getCurrentRoute({ pageComponent: AddonModWikiIndexPage });
|
||||
if (!route) {
|
||||
// Current view isn't wiki index.
|
||||
return;
|
||||
}
|
||||
const subwikiId = parseInt(params.swid, 10);
|
||||
const wikiId = parseInt(params.wid, 10);
|
||||
let path = AddonModWikiModuleHandlerService.PAGE_NAME;
|
||||
|
@ -112,7 +116,7 @@ export class AddonModWikiCreateLinkHandlerService extends CoreContentLinksHandle
|
|||
|
||||
if (isSameWiki) {
|
||||
// User is seeing the wiki, we can get the module from the wiki params.
|
||||
path = path + `/${route!.snapshot.params.courseId}/${route!.snapshot.params.cmId}/edit`;
|
||||
path = path + `/${route.snapshot.params.courseId}/${route.snapshot.params.cmId}/edit`;
|
||||
} else if (wikiId) {
|
||||
// The URL specifies which wiki it belongs to. Get the module.
|
||||
const module = await CoreCourse.getModuleBasicInfoByInstance(
|
||||
|
|
|
@ -19,7 +19,7 @@ import { CoreCourseLogHelper } from '@features/course/services/log-helper';
|
|||
import { CoreNetwork } from '@services/network';
|
||||
import { CoreGroups } from '@services/groups';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreSync } from '@services/sync';
|
||||
import { CoreSync, CoreSyncResult } from '@services/sync';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { makeSingleton, Translate } from '@singletons';
|
||||
import { CoreEvents } from '@singletons/events';
|
||||
|
@ -345,9 +345,7 @@ export const AddonModWikiSync = makeSingleton(AddonModWikiSyncProvider);
|
|||
/**
|
||||
* Data returned by a subwiki sync.
|
||||
*/
|
||||
export type AddonModWikiSyncSubwikiResult = {
|
||||
warnings: string[]; // List of warnings.
|
||||
updated: boolean; // Whether data was updated in the site.
|
||||
export type AddonModWikiSyncSubwikiResult = CoreSyncResult & {
|
||||
created: AddonModWikiCreatedPage[]; // List of created pages.
|
||||
discarded: AddonModWikiDiscardedPage[]; // List of discarded pages.
|
||||
};
|
||||
|
@ -355,9 +353,7 @@ export type AddonModWikiSyncSubwikiResult = {
|
|||
/**
|
||||
* Data returned by a wiki sync.
|
||||
*/
|
||||
export type AddonModWikiSyncWikiResult = {
|
||||
warnings: string[]; // List of warnings.
|
||||
updated: boolean; // Whether data was updated in the site.
|
||||
export type AddonModWikiSyncWikiResult = CoreSyncResult & {
|
||||
subwikis: {
|
||||
[subwikiId: number]: { // List of subwikis.
|
||||
created: AddonModWikiCreatedPage[];
|
||||
|
|
|
@ -162,6 +162,10 @@ export class AddonModWorkshopAssessmentStrategyComponent implements OnInit, OnDe
|
|||
cmId: this.workshop.coursemodule,
|
||||
});
|
||||
|
||||
if (!this.data.assessment.form) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.edit) {
|
||||
try {
|
||||
const offlineAssessment = await AddonModWorkshopOffline.getAssessment(this.workshop.id, this.assessmentId);
|
||||
|
@ -176,7 +180,7 @@ export class AddonModWorkshopAssessmentStrategyComponent implements OnInit, OnDe
|
|||
}
|
||||
|
||||
// Override assessment plugins values.
|
||||
this.data.assessment.form!.current = AddonModWorkshop.parseFields(
|
||||
this.data.assessment.form.current = AddonModWorkshop.parseFields(
|
||||
CoreUtils.objectToArrayOfObjects(offlineData, 'name', 'value'),
|
||||
);
|
||||
|
||||
|
@ -221,7 +225,7 @@ export class AddonModWorkshopAssessmentStrategyComponent implements OnInit, OnDe
|
|||
try {
|
||||
this.data.selectedValues = await AddonWorkshopAssessmentStrategyDelegate.getOriginalValues(
|
||||
this.strategy,
|
||||
this.data.assessment.form!,
|
||||
this.data.assessment.form,
|
||||
this.workshop.id,
|
||||
);
|
||||
} finally {
|
||||
|
@ -245,7 +249,7 @@ export class AddonModWorkshopAssessmentStrategyComponent implements OnInit, OnDe
|
|||
* @returns True if data has changed.
|
||||
*/
|
||||
hasDataChanged(): boolean {
|
||||
if (!this.assessmentStrategyLoaded) {
|
||||
if (!this.assessmentStrategyLoaded || !this.workshop.strategy) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -269,7 +273,7 @@ export class AddonModWorkshopAssessmentStrategyComponent implements OnInit, OnDe
|
|||
}
|
||||
|
||||
return AddonWorkshopAssessmentStrategyDelegate.hasDataChanged(
|
||||
this.workshop.strategy!,
|
||||
this.workshop.strategy,
|
||||
this.originalData.selectedValues,
|
||||
this.data.selectedValues,
|
||||
);
|
||||
|
@ -281,6 +285,10 @@ export class AddonModWorkshopAssessmentStrategyComponent implements OnInit, OnDe
|
|||
* @returns Promise resolved when done, rejected if assessment could not be saved.
|
||||
*/
|
||||
async saveAssessment(): Promise<void> {
|
||||
if (!this.data.assessment?.form) {
|
||||
return;
|
||||
}
|
||||
|
||||
const files = CoreFileSession.getFiles(
|
||||
AddonModWorkshopProvider.COMPONENT,
|
||||
this.workshop.id + '_' + this.assessmentId,
|
||||
|
@ -328,7 +336,7 @@ export class AddonModWorkshopAssessmentStrategyComponent implements OnInit, OnDe
|
|||
this.workshop,
|
||||
this.data.selectedValues,
|
||||
text,
|
||||
this.data.assessment!.form!,
|
||||
this.data.assessment.form,
|
||||
attachmentsId,
|
||||
);
|
||||
} catch (errors) {
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
|
||||
import { Component, Input, OnDestroy, OnInit, Optional } from '@angular/core';
|
||||
import { Params } from '@angular/router';
|
||||
import { CoreError } from '@classes/errors/error';
|
||||
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
|
||||
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
|
||||
import { IonContent } from '@ionic/angular';
|
||||
|
@ -264,7 +265,12 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity
|
|||
* @returns Resolved when done.
|
||||
*/
|
||||
async gotoSubmissionsPage(page: number): Promise<void> {
|
||||
const report = await AddonModWorkshop.getGradesReport(this.workshop!.id, {
|
||||
if (!this.workshop) {
|
||||
return;
|
||||
}
|
||||
const workshop = this.workshop;
|
||||
|
||||
const report = await AddonModWorkshop.getGradesReport(workshop.id, {
|
||||
groupId: this.group,
|
||||
page,
|
||||
cmId: this.module.id,
|
||||
|
@ -284,7 +290,7 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity
|
|||
await Promise.all(grades.map(async (grade) => {
|
||||
const submission: AddonModWorkshopSubmissionDataWithOfflineData = {
|
||||
id: grade.submissionid,
|
||||
workshopid: this.workshop!.id,
|
||||
workshopid: workshop.id,
|
||||
example: false,
|
||||
authorid: grade.userid,
|
||||
timecreated: grade.submissionmodified,
|
||||
|
@ -303,7 +309,7 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity
|
|||
reviewerof: this.parseReviewer(grade.reviewerof),
|
||||
};
|
||||
|
||||
if (this.workshop!.phase == AddonModWorkshopPhase.PHASE_ASSESSMENT) {
|
||||
if (workshop.phase == AddonModWorkshopPhase.PHASE_ASSESSMENT) {
|
||||
submission.reviewedbydone = grade.reviewedby?.reduce((a, b) => a + (b.grade ? 1 : 0), 0) || 0;
|
||||
submission.reviewerofdone = grade.reviewerof?.reduce((a, b) => a + (b.grade ? 1 : 0), 0) || 0;
|
||||
submission.reviewedbycount = grade.reviewedby?.length || 0;
|
||||
|
@ -313,7 +319,7 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity
|
|||
const offlineData = await AddonModWorkshopHelper.applyOfflineData(submission, this.offlineSubmissions);
|
||||
|
||||
if (offlineData !== undefined) {
|
||||
this.grades!.push(offlineData);
|
||||
this.grades.push(offlineData);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
@ -358,8 +364,12 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity
|
|||
* Go to submit page.
|
||||
*/
|
||||
gotoSubmit(): void {
|
||||
if (this.canSubmit && ((this.access!.creatingsubmissionallowed && !this.submission) ||
|
||||
(this.access!.modifyingsubmissionallowed && this.submission))) {
|
||||
if (!this.canSubmit || !this.access) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((this.access.creatingsubmissionallowed && !this.submission) ||
|
||||
(this.access.modifyingsubmissionallowed && this.submission)) {
|
||||
const params: Params = {
|
||||
module: this.module,
|
||||
access: this.access,
|
||||
|
@ -378,20 +388,22 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity
|
|||
* View Phase info.
|
||||
*/
|
||||
async viewPhaseInfo(): Promise<void> {
|
||||
if (this.phases) {
|
||||
const modalData = await CoreDomUtils.openModal<boolean>({
|
||||
component: AddonModWorkshopPhaseInfoComponent,
|
||||
componentProps: {
|
||||
phases: CoreUtils.objectToArray(this.phases),
|
||||
workshopPhase: this.workshop!.phase,
|
||||
externalUrl: this.module.url,
|
||||
showSubmit: this.showSubmit,
|
||||
},
|
||||
});
|
||||
if (!this.phases || !this.workshop) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (modalData === true) {
|
||||
this.gotoSubmit();
|
||||
}
|
||||
const modalData = await CoreDomUtils.openModal<boolean>({
|
||||
component: AddonModWorkshopPhaseInfoComponent,
|
||||
componentProps: {
|
||||
phases: CoreUtils.objectToArray(this.phases),
|
||||
workshopPhase: this.workshop.phase,
|
||||
externalUrl: this.module.url,
|
||||
showSubmit: this.showSubmit,
|
||||
},
|
||||
});
|
||||
|
||||
if (modalData === true) {
|
||||
this.gotoSubmit();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -413,26 +425,32 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity
|
|||
* @returns Promise resolved when done.
|
||||
*/
|
||||
protected async setPhaseInfo(): Promise<void> {
|
||||
if (!this.phases || !this.workshop || !this.access) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.submission = undefined;
|
||||
this.canAssess = false;
|
||||
this.assessments = [];
|
||||
this.userGrades = undefined;
|
||||
this.publishedSubmissions = [];
|
||||
|
||||
const workshop = this.workshop;
|
||||
|
||||
this.canSubmit = AddonModWorkshopHelper.canSubmit(
|
||||
this.workshop!,
|
||||
this.access!,
|
||||
this.phases![AddonModWorkshopPhase.PHASE_SUBMISSION].tasks,
|
||||
this.workshop,
|
||||
this.access,
|
||||
this.phases[AddonModWorkshopPhase.PHASE_SUBMISSION].tasks,
|
||||
);
|
||||
|
||||
this.showSubmit = this.canSubmit &&
|
||||
((this.access!.creatingsubmissionallowed && !this.submission) ||
|
||||
(this.access!.modifyingsubmissionallowed && !!this.submission));
|
||||
((this.access.creatingsubmissionallowed && !this.submission) ||
|
||||
(this.access.modifyingsubmissionallowed && !!this.submission));
|
||||
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
if (this.canSubmit) {
|
||||
promises.push(AddonModWorkshopHelper.getUserSubmission(this.workshop!.id, { cmId: this.module.id })
|
||||
promises.push(AddonModWorkshopHelper.getUserSubmission(this.workshop.id, { cmId: this.module.id })
|
||||
.then(async (submission) => {
|
||||
this.submission = await AddonModWorkshopHelper.applyOfflineData(submission, this.offlineSubmissions);
|
||||
|
||||
|
@ -440,27 +458,27 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity
|
|||
}));
|
||||
}
|
||||
|
||||
if (this.access!.canviewallsubmissions && this.workshop!.phase >= AddonModWorkshopPhase.PHASE_SUBMISSION) {
|
||||
if (this.access.canviewallsubmissions && this.workshop.phase >= AddonModWorkshopPhase.PHASE_SUBMISSION) {
|
||||
promises.push(this.gotoSubmissionsPage(this.page));
|
||||
}
|
||||
|
||||
let assessPromise = Promise.resolve();
|
||||
|
||||
if (this.workshop!.phase >= AddonModWorkshopPhase.PHASE_ASSESSMENT) {
|
||||
this.canAssess = AddonModWorkshopHelper.canAssess(this.workshop!, this.access!);
|
||||
if (this.workshop.phase >= AddonModWorkshopPhase.PHASE_ASSESSMENT) {
|
||||
this.canAssess = AddonModWorkshopHelper.canAssess(this.workshop, this.access);
|
||||
|
||||
if (this.canAssess) {
|
||||
assessPromise = AddonModWorkshopHelper.getReviewerAssessments(this.workshop!.id, {
|
||||
assessPromise = AddonModWorkshopHelper.getReviewerAssessments(this.workshop.id, {
|
||||
cmId: this.module.id,
|
||||
}).then(async (assessments) => {
|
||||
await Promise.all(assessments.map(async (assessment) => {
|
||||
assessment.strategy = this.workshop!.strategy;
|
||||
assessment.strategy = workshop.strategy;
|
||||
if (!this.hasOffline) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const offlineAssessment = await AddonModWorkshopOffline.getAssessment(this.workshop!.id, assessment.id);
|
||||
const offlineAssessment = await AddonModWorkshopOffline.getAssessment(workshop.id, assessment.id);
|
||||
|
||||
assessment.offline = true;
|
||||
assessment.timemodified = Math.floor(offlineAssessment.timemodified / 1000);
|
||||
|
@ -477,27 +495,23 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity
|
|||
}
|
||||
}
|
||||
|
||||
if (this.workshop!.phase == AddonModWorkshopPhase.PHASE_CLOSED) {
|
||||
promises.push(AddonModWorkshop.getGrades(this.workshop!.id, { cmId: this.module.id }).then((grades) => {
|
||||
if (this.workshop.phase === AddonModWorkshopPhase.PHASE_CLOSED) {
|
||||
promises.push(AddonModWorkshop.getGrades(this.workshop.id, { cmId: this.module.id }).then((grades) => {
|
||||
this.userGrades = grades.submissionlongstrgrade || grades.assessmentlongstrgrade ? grades : undefined;
|
||||
|
||||
return;
|
||||
}));
|
||||
|
||||
if (this.access!.canviewpublishedsubmissions) {
|
||||
if (this.access.canviewpublishedsubmissions) {
|
||||
promises.push(assessPromise.then(async () => {
|
||||
const submissions: AddonModWorkshopSubmissionDataWithOfflineData[] =
|
||||
await AddonModWorkshop.getSubmissions(this.workshop!.id, { cmId: this.module.id });
|
||||
await AddonModWorkshop.getSubmissions(workshop.id, { cmId: this.module.id });
|
||||
|
||||
this.publishedSubmissions = submissions.filter((submission) => {
|
||||
if (submission.published) {
|
||||
submission.reviewedby = [];
|
||||
|
||||
this.assessments.forEach((assessment) => {
|
||||
if (assessment.submissionid == submission.id) {
|
||||
submission.reviewedby!.push(AddonModWorkshopHelper.realGradeValue(this.workshop!, assessment));
|
||||
}
|
||||
});
|
||||
submission.reviewedby =
|
||||
this.assessments.filter((assessment) => assessment.submissionid === submission.id)
|
||||
.map((assessment => AddonModWorkshopHelper.realGradeValue(workshop, assessment)));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -514,22 +528,14 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity
|
|||
}
|
||||
|
||||
/**
|
||||
* Performs the sync of the activity.
|
||||
*
|
||||
* @returns Promise resolved when done.
|
||||
* @inheritdoc
|
||||
*/
|
||||
protected sync(): Promise<AddonModWorkshopSyncResult> {
|
||||
return AddonModWorkshopSync.syncWorkshop(this.workshop!.id);
|
||||
}
|
||||
if (!this.workshop) {
|
||||
throw new CoreError('Cannot sync without a workshop.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if sync has succeed from result sync data.
|
||||
*
|
||||
* @param result Data returned on the sync function.
|
||||
* @returns If suceed or not.
|
||||
*/
|
||||
protected hasSyncSucceed(result: AddonModWorkshopSyncResult): boolean {
|
||||
return result.updated;
|
||||
return AddonModWorkshopSync.syncWorkshop(this.workshop.id);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -385,11 +385,14 @@ export class AddonModWorkshopEditSubmissionPage implements OnInit, OnDestroy, Ca
|
|||
);
|
||||
newSubmissionId = false;
|
||||
} else {
|
||||
if (!submissionId) {
|
||||
throw new CoreError('Submission cannot be updated without a submissionId');
|
||||
}
|
||||
// Try to send it to server.
|
||||
// Don't allow offline if there are attachments since they were uploaded fine.
|
||||
newSubmissionId = await AddonModWorkshop.updateSubmission(
|
||||
this.workshopId,
|
||||
submissionId!,
|
||||
submissionId,
|
||||
this.courseId,
|
||||
inputData.title,
|
||||
inputData.content,
|
||||
|
|
|
@ -254,7 +254,7 @@ export class AddonModWorkshopSubmissionPage implements OnInit, OnDestroy, CanLea
|
|||
|
||||
return;
|
||||
}));
|
||||
} else if (this.currentUserId == this.userId && this.assessmentId) {
|
||||
} else if (this.currentUserId === this.userId && this.assessmentId) {
|
||||
// Get new data, different that came from stateParams.
|
||||
promises.push(AddonModWorkshop.getAssessment(this.workshopId, this.assessmentId, {
|
||||
cmId: this.module.id,
|
||||
|
@ -268,7 +268,7 @@ export class AddonModWorkshopSubmissionPage implements OnInit, OnDestroy, CanLea
|
|||
|
||||
return;
|
||||
}));
|
||||
} else if (this.workshop.phase == AddonModWorkshopPhase.PHASE_CLOSED && this.userId == this.currentUserId) {
|
||||
} else if (this.workshop.phase === AddonModWorkshopPhase.PHASE_CLOSED && this.userId === this.currentUserId) {
|
||||
const assessments = await AddonModWorkshop.getSubmissionAssessments(this.workshopId, this.submissionId, {
|
||||
cmId: this.module.id,
|
||||
});
|
||||
|
@ -276,7 +276,7 @@ export class AddonModWorkshopSubmissionPage implements OnInit, OnDestroy, CanLea
|
|||
this.submissionInfo.reviewedby = assessments.map((assessment) => this.parseAssessment(assessment));
|
||||
}
|
||||
|
||||
if (this.canAddFeedback || this.workshop.phase == AddonModWorkshopPhase.PHASE_CLOSED) {
|
||||
if (this.canAddFeedback || this.workshop.phase === AddonModWorkshopPhase.PHASE_CLOSED) {
|
||||
this.evaluate = {
|
||||
published: this.submission.published,
|
||||
text: this.submission.feedbackauthor || '',
|
||||
|
@ -284,54 +284,13 @@ export class AddonModWorkshopSubmissionPage implements OnInit, OnDestroy, CanLea
|
|||
}
|
||||
|
||||
if (this.canAddFeedback) {
|
||||
|
||||
if (!this.isDestroyed) {
|
||||
// Block the workshop.
|
||||
CoreSync.blockOperation(this.component, this.workshopId);
|
||||
}
|
||||
|
||||
const defaultGrade = Translate.instant('addon.mod_workshop.notoverridden');
|
||||
|
||||
promises.push(CoreGradesHelper.makeGradesMenu(this.workshop.grade || 0, undefined, defaultGrade, -1)
|
||||
.then(async (grades) => {
|
||||
this.evaluationGrades = grades;
|
||||
|
||||
this.evaluate!.grade = {
|
||||
label: CoreGradesHelper.getGradeLabelFromValue(grades, this.submissionInfo.gradeover) ||
|
||||
defaultGrade,
|
||||
value: this.submissionInfo.gradeover || -1,
|
||||
};
|
||||
|
||||
try {
|
||||
const offlineSubmission =
|
||||
await AddonModWorkshopOffline.getEvaluateSubmission(this.workshopId, this.submissionId);
|
||||
|
||||
this.hasOffline = true;
|
||||
this.evaluate!.published = offlineSubmission.published;
|
||||
this.evaluate!.text = offlineSubmission.feedbacktext;
|
||||
this.evaluate!.grade = {
|
||||
label: CoreGradesHelper.getGradeLabelFromValue(
|
||||
grades,
|
||||
parseInt(offlineSubmission.gradeover, 10),
|
||||
) || defaultGrade,
|
||||
value: offlineSubmission.gradeover || -1,
|
||||
};
|
||||
} catch {
|
||||
// Ignore errors.
|
||||
this.hasOffline = false;
|
||||
} finally {
|
||||
this.originalEvaluation.published = this.evaluate!.published;
|
||||
this.originalEvaluation.text = this.evaluate!.text;
|
||||
this.originalEvaluation.grade = this.evaluate!.grade.value;
|
||||
|
||||
this.feedbackForm.controls['published'].setValue(this.evaluate!.published);
|
||||
this.feedbackForm.controls['grade'].setValue(this.evaluate!.grade.value);
|
||||
this.feedbackForm.controls['text'].setValue(this.evaluate!.text);
|
||||
}
|
||||
|
||||
return;
|
||||
}));
|
||||
} else if (this.workshop.phase == AddonModWorkshopPhase.PHASE_CLOSED && this.submission.gradeoverby &&
|
||||
promises.push(this.fillEvaluationsGrades());
|
||||
} else if (this.workshop.phase === AddonModWorkshopPhase.PHASE_CLOSED && this.submission.gradeoverby &&
|
||||
this.evaluate && this.evaluate.text) {
|
||||
promises.push(CoreUser.getProfile(this.submission.gradeoverby, this.courseId, true).then((profile) => {
|
||||
this.evaluateByProfile = profile;
|
||||
|
@ -362,6 +321,49 @@ export class AddonModWorkshopSubmissionPage implements OnInit, OnDestroy, CanLea
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill evaluation grade info.
|
||||
*/
|
||||
protected async fillEvaluationsGrades(): Promise<void> {
|
||||
const defaultGrade = Translate.instant('addon.mod_workshop.notoverridden');
|
||||
|
||||
this.evaluationGrades = await CoreGradesHelper.makeGradesMenu(this.workshop.grade || 0, undefined, defaultGrade, -1);
|
||||
|
||||
if (!this.evaluate) {
|
||||
// Should not happen.
|
||||
return;
|
||||
}
|
||||
|
||||
this.evaluate.grade = {
|
||||
label: CoreGradesHelper.getGradeLabelFromValue(this.evaluationGrades, this.submissionInfo.gradeover) || defaultGrade,
|
||||
value: this.submissionInfo.gradeover || -1,
|
||||
};
|
||||
|
||||
try {
|
||||
const offlineSubmission = await AddonModWorkshopOffline.getEvaluateSubmission(this.workshopId, this.submissionId);
|
||||
|
||||
this.hasOffline = true;
|
||||
this.evaluate.published = offlineSubmission.published;
|
||||
this.evaluate.text = offlineSubmission.feedbacktext;
|
||||
this.evaluate.grade = {
|
||||
label: CoreGradesHelper.getGradeLabelFromValue(this.evaluationGrades, parseInt(offlineSubmission.gradeover, 10)) ||
|
||||
defaultGrade,
|
||||
value: offlineSubmission.gradeover || -1,
|
||||
};
|
||||
} catch {
|
||||
// Ignore errors.
|
||||
this.hasOffline = false;
|
||||
} finally {
|
||||
this.originalEvaluation.published = this.evaluate.published;
|
||||
this.originalEvaluation.text = this.evaluate.text;
|
||||
this.originalEvaluation.grade = this.evaluate.grade.value;
|
||||
|
||||
this.feedbackForm.controls['published'].setValue(this.evaluate.published);
|
||||
this.feedbackForm.controls['grade'].setValue(this.evaluate.grade.value);
|
||||
this.feedbackForm.controls['text'].setValue(this.evaluate.text);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse assessment to be shown.
|
||||
*
|
||||
|
|
|
@ -71,7 +71,7 @@ export class AddonModWorkshopPrefetchHandlerService extends CoreCourseActivityPr
|
|||
): Promise<{ workshop?: AddonModWorkshopData; groups: CoreGroup[]; files: CoreWSFile[]}> {
|
||||
let groups: CoreGroup[] = [];
|
||||
let files: CoreWSFile[] = [];
|
||||
let workshop: AddonModWorkshopData | undefined;
|
||||
let workshop: AddonModWorkshopData;
|
||||
let access: AddonModWorkshopGetWorkshopAccessInformationWSResponse | undefined;
|
||||
|
||||
const modOptions = {
|
||||
|
@ -79,11 +79,25 @@ export class AddonModWorkshopPrefetchHandlerService extends CoreCourseActivityPr
|
|||
...options, // Include all options.
|
||||
};
|
||||
|
||||
try {
|
||||
const site = await CoreSites.getSite(options.siteId);
|
||||
const userId = site.getUserId();
|
||||
const workshop = await AddonModWorkshop.getWorkshop(courseId, module.id, options);
|
||||
const site = await CoreSites.getSite(options.siteId);
|
||||
options.siteId = options.siteId ?? site.getId();
|
||||
const userId = site.getUserId();
|
||||
|
||||
try {
|
||||
workshop = await AddonModWorkshop.getWorkshop(courseId, module.id, options);
|
||||
} catch (error) {
|
||||
if (options.omitFail) {
|
||||
// Any error, return the info we have.
|
||||
return {
|
||||
groups: [],
|
||||
files: [],
|
||||
};
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
try {
|
||||
files = this.getIntroFilesFromInstance(module, workshop);
|
||||
files = files.concat(workshop.instructauthorsfiles || []).concat(workshop.instructreviewersfiles || []);
|
||||
|
||||
|
@ -124,7 +138,7 @@ export class AddonModWorkshopPrefetchHandlerService extends CoreCourseActivityPr
|
|||
await Promise.all(submissions.map(async (submission) => {
|
||||
files = files.concat(submission.contentfiles || []).concat(submission.attachmentfiles || []);
|
||||
|
||||
const assessments = await AddonModWorkshop.getSubmissionAssessments(workshop!.id, submission.id, {
|
||||
const assessments = await AddonModWorkshop.getSubmissionAssessments(workshop.id, submission.id, {
|
||||
cmId: module.id,
|
||||
});
|
||||
|
||||
|
@ -133,9 +147,9 @@ export class AddonModWorkshopPrefetchHandlerService extends CoreCourseActivityPr
|
|||
.concat(assessment.feedbackcontentfiles);
|
||||
});
|
||||
|
||||
if (workshop!.phase >= AddonModWorkshopPhase.PHASE_ASSESSMENT && canAssess) {
|
||||
if (workshop.phase >= AddonModWorkshopPhase.PHASE_ASSESSMENT && canAssess) {
|
||||
await Promise.all(assessments.map((assessment) =>
|
||||
AddonModWorkshopHelper.getReviewerAssessmentById(workshop!.id, assessment.id)));
|
||||
AddonModWorkshopHelper.getReviewerAssessmentById(workshop.id, assessment.id)));
|
||||
}
|
||||
}));
|
||||
|
||||
|
@ -267,7 +281,12 @@ export class AddonModWorkshopPrefetchHandlerService extends CoreCourseActivityPr
|
|||
|
||||
// Prefetch the workshop data.
|
||||
const info = await this.getWorkshopInfoHelper(module, courseId, commonOptions);
|
||||
const workshop = info.workshop!;
|
||||
if (!info.workshop) {
|
||||
// It would throw an exception so it would not happen.
|
||||
return;
|
||||
}
|
||||
|
||||
const workshop = info.workshop;
|
||||
const promises: Promise<unknown>[] = [];
|
||||
const assessmentIds: number[] = [];
|
||||
|
||||
|
|
|
@ -181,7 +181,7 @@ export class AddonModWorkshopHelperProvider {
|
|||
assessment = await AddonModWorkshop.getAssessment(workshopId, assessmentId, options);
|
||||
} catch (error) {
|
||||
const assessments = await AddonModWorkshop.getReviewerAssessments(workshopId, options);
|
||||
assessment = assessments.find((assessment_1) => assessment_1.id == assessmentId);
|
||||
assessment = assessments.find((ass) => ass.id === assessmentId);
|
||||
|
||||
if (!assessment) {
|
||||
throw error;
|
||||
|
@ -266,9 +266,7 @@ export class AddonModWorkshopHelperProvider {
|
|||
* Upload or store some files for a submission, depending if the user is offline or not.
|
||||
*
|
||||
* @param workshopId Workshop ID.
|
||||
* @param submissionId If not editing, it will refer to timecreated.
|
||||
* @param files List of files.
|
||||
* @param editing If the submission is being edited or added otherwise.
|
||||
* @param offline True if files sould be stored for offline, false to upload them.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @returns Promise resolved if success.
|
||||
|
@ -451,30 +449,26 @@ export class AddonModWorkshopHelperProvider {
|
|||
* @returns Promise resolved with the files.
|
||||
*/
|
||||
async applyOfflineData(
|
||||
submission?: AddonModWorkshopSubmissionDataWithOfflineData,
|
||||
submission: AddonModWorkshopSubmissionDataWithOfflineData = {
|
||||
id: 0,
|
||||
workshopid: 0,
|
||||
title: '',
|
||||
content: '',
|
||||
timemodified: 0,
|
||||
example: false,
|
||||
authorid: 0,
|
||||
timecreated: 0,
|
||||
contenttrust: 0,
|
||||
attachment: 0,
|
||||
published: false,
|
||||
late: 0,
|
||||
},
|
||||
actions: AddonModWorkshopOfflineSubmission[] = [],
|
||||
): Promise<AddonModWorkshopSubmissionDataWithOfflineData | undefined> {
|
||||
if (actions.length == 0) {
|
||||
if (actions.length === 0) {
|
||||
return submission;
|
||||
}
|
||||
|
||||
if (submission === undefined) {
|
||||
submission = {
|
||||
id: 0,
|
||||
workshopid: 0,
|
||||
title: '',
|
||||
content: '',
|
||||
timemodified: 0,
|
||||
example: false,
|
||||
authorid: 0,
|
||||
timecreated: 0,
|
||||
contenttrust: 0,
|
||||
attachment: 0,
|
||||
published: false,
|
||||
late: 0,
|
||||
};
|
||||
}
|
||||
|
||||
let attachmentsId: CoreFileUploaderStoreFilesResult | undefined;
|
||||
const workshopId = actions[0].workshopid;
|
||||
|
||||
|
@ -482,17 +476,17 @@ export class AddonModWorkshopHelperProvider {
|
|||
switch (action.action) {
|
||||
case AddonModWorkshopAction.ADD:
|
||||
case AddonModWorkshopAction.UPDATE:
|
||||
submission!.title = action.title;
|
||||
submission!.content = action.content;
|
||||
submission!.title = action.title;
|
||||
submission!.courseid = action.courseid;
|
||||
submission!.submissionmodified = action.timemodified / 1000;
|
||||
submission!.offline = true;
|
||||
submission.title = action.title;
|
||||
submission.content = action.content;
|
||||
submission.title = action.title;
|
||||
submission.courseid = action.courseid;
|
||||
submission.submissionmodified = action.timemodified / 1000;
|
||||
submission.offline = true;
|
||||
attachmentsId = action.attachmentsid as CoreFileUploaderStoreFilesResult;
|
||||
break;
|
||||
case AddonModWorkshopAction.DELETE:
|
||||
submission!.deleted = true;
|
||||
submission!.submissionmodified = action.timemodified / 1000;
|
||||
submission.deleted = true;
|
||||
submission.submissionmodified = action.timemodified / 1000;
|
||||
break;
|
||||
default:
|
||||
}
|
||||
|
@ -534,7 +528,8 @@ export class AddonModWorkshopHelperProvider {
|
|||
}
|
||||
|
||||
const data =
|
||||
(await AddonWorkshopAssessmentStrategyDelegate.prepareAssessmentData(workshop.strategy!, selectedValues, form)) || {};
|
||||
(await AddonWorkshopAssessmentStrategyDelegate.prepareAssessmentData(workshop.strategy ?? '', selectedValues, form)) ||
|
||||
{};
|
||||
data.feedbackauthor = feedbackText;
|
||||
data.feedbackauthorattachmentsid = attachmentsId;
|
||||
data.nodims = form.dimenssionscount;
|
||||
|
@ -551,16 +546,16 @@ export class AddonModWorkshopHelperProvider {
|
|||
* @returns Real grade formatted.
|
||||
*/
|
||||
protected realGradeValueHelper(value?: number | string, max = 0, decimals = 0): string | undefined {
|
||||
if (typeof value == 'string') {
|
||||
if (typeof value === 'string') {
|
||||
// Already treated.
|
||||
return value;
|
||||
}
|
||||
|
||||
if (value == null || value === undefined) {
|
||||
if (value === null || value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (max == 0) {
|
||||
if (max === 0) {
|
||||
return '0';
|
||||
}
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ import { CoreFileUploaderStoreFilesResult } from '@features/fileuploader/service
|
|||
import { CoreNetwork } from '@services/network';
|
||||
import { CoreFileEntry } from '@services/file-helper';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreSync } from '@services/sync';
|
||||
import { CoreSync, CoreSyncResult } from '@services/sync';
|
||||
import { CoreTextUtils } from '@services/utils/text';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { Translate, makeSingleton } from '@singletons';
|
||||
|
@ -639,7 +639,4 @@ export type AddonModWorkshopAutoSyncData = {
|
|||
warnings: string[];
|
||||
};
|
||||
|
||||
export type AddonModWorkshopSyncResult = {
|
||||
warnings: string[];
|
||||
updated: boolean;
|
||||
};
|
||||
export type AddonModWorkshopSyncResult = CoreSyncResult;
|
||||
|
|
|
@ -610,13 +610,16 @@ export class AddonModWorkshopProvider {
|
|||
grades: AddonModWorkshopGradesData[],
|
||||
options: AddonModWorkshopGetGradesReportOptions = {},
|
||||
): Promise<AddonModWorkshopGradesData[]> {
|
||||
options.page = options.page ?? 0;
|
||||
options.perPage = options.perPage ?? AddonModWorkshopProvider.PER_PAGE;
|
||||
|
||||
const report = await this.getGradesReport(workshopId, options);
|
||||
|
||||
Array.prototype.push.apply(grades, report.grades);
|
||||
const canLoadMore = ((options.page! + 1) * options.perPage!) < report.totalcount;
|
||||
const canLoadMore = ((options.page + 1) * options.perPage) < report.totalcount;
|
||||
|
||||
if (canLoadMore) {
|
||||
options.page!++;
|
||||
options.page++;
|
||||
|
||||
return this.fetchGradeReportsRecursive(workshopId, grades, options);
|
||||
}
|
||||
|
@ -778,7 +781,11 @@ export class AddonModWorkshopProvider {
|
|||
// Other errors ocurring.
|
||||
CoreWS.throwOnFailedStatus(response, 'Add submission failed');
|
||||
|
||||
return response.submissionid!;
|
||||
if (!response.submissionid) {
|
||||
throw new CoreError('Add submission failed, no submission id was returned');
|
||||
}
|
||||
|
||||
return response.submissionid;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,57 +1,57 @@
|
|||
<ion-list class="addon-qtype-calculated-container" *ngIf="calcQuestion && (calcQuestion.text || calcQuestion.text === '')">
|
||||
<ion-list class="addon-qtype-calculated-container" *ngIf="question && (question.text || question.text === '')">
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-label>
|
||||
<core-format-text [component]="component" [componentId]="componentId" [text]="calcQuestion.text" [contextLevel]="contextLevel"
|
||||
<core-format-text [component]="component" [componentId]="componentId" [text]="question.text" [contextLevel]="contextLevel"
|
||||
[contextInstanceId]="contextInstanceId" [courseId]="courseId">
|
||||
</core-format-text>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<!-- Display unit options before the answer input. -->
|
||||
<ng-container *ngIf="calcQuestion.options && calcQuestion.options.length && calcQuestion.optionsFirst">
|
||||
<ng-container *ngIf="question.options && question.options.length && question.optionsFirst">
|
||||
<ng-container *ngTemplateOutlet="radioUnits"></ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ion-item *ngIf="calcQuestion.input" class="ion-text-wrap core-{{calcQuestion.input.correctIconColor}}-item">
|
||||
<ion-item *ngIf="question.input" class="ion-text-wrap core-{{question.input.correctIconColor}}-item">
|
||||
<ion-label position="stacked">{{ 'addon.mod_quiz.answercolon' | translate }}</ion-label>
|
||||
|
||||
<div class="flex-row">
|
||||
<!-- Display unit select before the answer input. -->
|
||||
<ng-container *ngIf="calcQuestion.select && calcQuestion.selectFirst">
|
||||
<ng-container *ngIf="question.select && question.selectFirst">
|
||||
<ng-container *ngTemplateOutlet="selectUnits"></ng-container>
|
||||
</ng-container>
|
||||
|
||||
<!-- Input to enter the answer. -->
|
||||
<ion-input type="text" [attr.name]="calcQuestion.input.name"
|
||||
[placeholder]="calcQuestion.input.readOnly ? '' : 'core.question.answer' | translate" [value]="calcQuestion.input.value"
|
||||
[disabled]="calcQuestion.input.readOnly" autocorrect="off">
|
||||
<ion-input type="text" [attr.name]="question.input.name"
|
||||
[placeholder]="question.input.readOnly ? '' : 'core.question.answer' | translate" [value]="question.input.value"
|
||||
[disabled]="question.input.readOnly" autocorrect="off">
|
||||
</ion-input>
|
||||
|
||||
<!-- Display unit select after the answer input. -->
|
||||
<ng-container *ngIf="calcQuestion.select && !calcQuestion.selectFirst">
|
||||
<ng-container *ngIf="question.select && !question.selectFirst">
|
||||
<ng-container *ngTemplateOutlet="selectUnits"></ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
<ion-icon *ngIf="calcQuestion.input.correctIcon" class="core-correct-icon ion-align-self-center" slot="end"
|
||||
[name]="calcQuestion.input.correctIcon" [color]="[calcQuestion.input.correctIconColor]">
|
||||
<ion-icon *ngIf="question.input.correctIcon" class="core-correct-icon ion-align-self-center" slot="end"
|
||||
[name]="question.input.correctIcon" [color]="[question.input.correctIconColor]">
|
||||
</ion-icon>
|
||||
</ion-item>
|
||||
|
||||
<!-- Display unit options after the answer input. -->
|
||||
<ng-container *ngIf="calcQuestion.options && calcQuestion.options.length && !calcQuestion.optionsFirst">
|
||||
<ng-container *ngIf="question.options && question.options.length && !question.optionsFirst">
|
||||
<ng-container *ngTemplateOutlet="radioUnits"></ng-container>
|
||||
</ng-container>
|
||||
</ion-list>
|
||||
|
||||
<!-- Template for units entered using a select. -->
|
||||
<ng-template #selectUnits>
|
||||
<label *ngIf="calcQuestion!.select!.accessibilityLabel" class="accesshide" for="{{calcQuestion!.select!.id}}">
|
||||
{{ calcQuestion!.select!.accessibilityLabel }}
|
||||
<label *ngIf="question!.select!.accessibilityLabel" class="accesshide" for="{{question!.select!.id}}">
|
||||
{{ question!.select!.accessibilityLabel }}
|
||||
</label>
|
||||
<ion-select id="{{calcQuestion!.select!.id}}" [name]="calcQuestion!.select!.name" [(ngModel)]="calcQuestion!.select!.selected"
|
||||
interface="action-sheet" [disabled]="calcQuestion!.select!.disabled" [slot]="calcQuestion?.selectFirst ? 'start' : 'end'"
|
||||
<ion-select id="{{question!.select!.id}}" [name]="question!.select!.name" [(ngModel)]="question!.select!.selected"
|
||||
interface="action-sheet" [disabled]="question!.select!.disabled" [slot]="question?.selectFirst ? 'start' : 'end'"
|
||||
[interfaceOptions]="{header: 'addon.mod_quiz.unit' | translate}">
|
||||
<ion-select-option *ngFor="let option of calcQuestion!.select!.options" [value]="option.value">
|
||||
<ion-select-option *ngFor="let option of question!.select!.options" [value]="option.value">
|
||||
{{option.label}}
|
||||
</ion-select-option>
|
||||
</ion-select>
|
||||
|
@ -59,15 +59,15 @@
|
|||
|
||||
<!-- Template for units entered using radio buttons. -->
|
||||
<ng-template #radioUnits>
|
||||
<ion-radio-group [(ngModel)]="calcQuestion!.unit" [name]="calcQuestion!.optionsName">
|
||||
<ion-item class="ion-text-wrap" *ngFor="let option of calcQuestion!.options">
|
||||
<ion-radio-group [(ngModel)]="question!.unit" [name]="question!.optionsName">
|
||||
<ion-item class="ion-text-wrap" *ngFor="let option of question!.options">
|
||||
<ion-label>{{ option.text }}</ion-label>
|
||||
<ion-radio slot="end" [value]="option.value" [disabled]="option.disabled || calcQuestion!.input?.readOnly"
|
||||
[color]="calcQuestion!.input?.correctIconColor">
|
||||
<ion-radio slot="end" [value]="option.value" [disabled]="option.disabled || question!.input?.readOnly"
|
||||
[color]="question!.input?.correctIconColor">
|
||||
</ion-radio>
|
||||
</ion-item>
|
||||
|
||||
<!-- ion-radio doesn't use an input. Create a hidden input to hold the selected value. -->
|
||||
<input type="hidden" [ngModel]="calcQuestion!.unit" [attr.name]="calcQuestion!.optionsName">
|
||||
<input type="hidden" [ngModel]="question!.unit" [attr.name]="question!.optionsName">
|
||||
</ion-radio-group>
|
||||
</ng-template>
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, OnInit, ElementRef } from '@angular/core';
|
||||
import { Component, ElementRef } from '@angular/core';
|
||||
|
||||
import { AddonModQuizCalculatedQuestion, CoreQuestionBaseComponent } from '@features/question/classes/base-question-component';
|
||||
|
||||
|
@ -23,9 +23,7 @@ import { AddonModQuizCalculatedQuestion, CoreQuestionBaseComponent } from '@feat
|
|||
selector: 'addon-qtype-calculated',
|
||||
templateUrl: 'addon-qtype-calculated.html',
|
||||
})
|
||||
export class AddonQtypeCalculatedComponent extends CoreQuestionBaseComponent implements OnInit {
|
||||
|
||||
calcQuestion?: AddonModQuizCalculatedQuestion;
|
||||
export class AddonQtypeCalculatedComponent extends CoreQuestionBaseComponent<AddonModQuizCalculatedQuestion> {
|
||||
|
||||
constructor(elementRef: ElementRef) {
|
||||
super('AddonQtypeCalculatedComponent', elementRef);
|
||||
|
@ -34,10 +32,8 @@ export class AddonQtypeCalculatedComponent extends CoreQuestionBaseComponent imp
|
|||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
init(): void {
|
||||
this.initCalculatedComponent();
|
||||
|
||||
this.calcQuestion = this.question;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -708,9 +708,15 @@ export class AddonQtypeDdImageOrTextQuestionDocStructure {
|
|||
this.topNode = this.container.querySelector<HTMLElement>('.addon-qtype-ddimageortext-container');
|
||||
this.dragItemsArea = this.topNode?.querySelector<HTMLElement>('div.draghomes') || null;
|
||||
|
||||
if (!this.topNode) {
|
||||
this.logger.error('ddimageortext container not found');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.dragItemsArea) {
|
||||
// On 3.9+ dragitems were removed.
|
||||
const dragItems = this.topNode!.querySelector('div.dragitems');
|
||||
const dragItems = this.topNode.querySelector('div.dragitems');
|
||||
|
||||
if (dragItems) {
|
||||
// Remove empty div.dragitems.
|
||||
|
@ -718,10 +724,10 @@ export class AddonQtypeDdImageOrTextQuestionDocStructure {
|
|||
}
|
||||
|
||||
// 3.6+ site, transform HTML so it has the same structure as in Moodle 3.5.
|
||||
const ddArea = this.topNode!.querySelector('div.ddarea');
|
||||
const ddArea = this.topNode.querySelector('div.ddarea');
|
||||
if (ddArea) {
|
||||
// Move div.dropzones to div.ddarea.
|
||||
const dropZones = this.topNode!.querySelector('div.dropzones');
|
||||
const dropZones = this.topNode.querySelector('div.dropzones');
|
||||
if (dropZones) {
|
||||
ddArea.appendChild(dropZones);
|
||||
}
|
||||
|
@ -738,7 +744,7 @@ export class AddonQtypeDdImageOrTextQuestionDocStructure {
|
|||
draghome.classList.add(`dragitemhomes${index}`);
|
||||
});
|
||||
} else {
|
||||
this.dragItemsArea = this.topNode!.querySelector<HTMLElement>('div.dragitems');
|
||||
this.dragItemsArea = this.topNode.querySelector<HTMLElement>('div.dragitems');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -797,14 +803,15 @@ export class AddonQtypeDdImageOrTextQuestionDocStructure {
|
|||
getClassnameNumericSuffix(node: HTMLElement, prefix: string): number | undefined {
|
||||
if (node.classList && node.classList.length) {
|
||||
const patt1 = new RegExp(`^${prefix}([0-9])+$`);
|
||||
const patt2 = new RegExp('([0-9])+$');
|
||||
|
||||
for (let index = 0; index < node.classList.length; index++) {
|
||||
if (patt1.test(node.classList[index])) {
|
||||
const match = patt2.exec(node.classList[index]);
|
||||
const classFound = Array.from(node.classList)
|
||||
.find((className) => patt1.test(className));
|
||||
|
||||
return Number(match![0]);
|
||||
}
|
||||
if (classFound) {
|
||||
const patt2 = new RegExp('([0-9])+$');
|
||||
const match = patt2.exec(classFound);
|
||||
|
||||
return Number(match?.[0]);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,24 +1,24 @@
|
|||
<div *ngIf="ddQuestion && (ddQuestion.text || ddQuestion.text === '')" class="addon-qtype-ddimageortext-container">
|
||||
<div *ngIf="question && (question.text || question.text === '')" class="addon-qtype-ddimageortext-container">
|
||||
<!-- Content is outside the core-loading to let the script calculate drag items position -->
|
||||
<core-loading [hideUntil]="ddQuestion.loaded"></core-loading>
|
||||
<core-loading [hideUntil]="question.loaded"></core-loading>
|
||||
|
||||
<ion-item class="ion-text-wrap" [hidden]="!ddQuestion.loaded">
|
||||
<ion-item class="ion-text-wrap" [hidden]="!question.loaded">
|
||||
<ion-label>
|
||||
<ion-card *ngIf="!ddQuestion.readOnly" class="core-info-card">
|
||||
<ion-card *ngIf="!question.readOnly" class="core-info-card">
|
||||
<ion-item>
|
||||
<ion-icon name="fas-info-circle" slot="start" aria-hidden="true"></ion-icon>
|
||||
<ion-label>{{ 'core.question.howtodraganddrop' | translate }}</ion-label>
|
||||
</ion-item>
|
||||
</ion-card>
|
||||
|
||||
<core-format-text [component]="component" [componentId]="componentId" [text]="ddQuestion.text" [contextLevel]="contextLevel"
|
||||
<core-format-text [component]="component" [componentId]="componentId" [text]="question.text" [contextLevel]="contextLevel"
|
||||
[contextInstanceId]="contextInstanceId" [courseId]="courseId" (afterRender)="textRendered()">
|
||||
</core-format-text>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<div class="fake-ion-item ion-text-wrap" [hidden]="!ddQuestion.loaded">
|
||||
<core-format-text *ngIf="ddQuestion.ddArea" [adaptImg]="false" [component]="component" [componentId]="componentId"
|
||||
[text]="ddQuestion.ddArea" [filter]="false" (afterRender)="ddAreaRendered()">
|
||||
<div class="fake-ion-item ion-text-wrap" [hidden]="!question.loaded">
|
||||
<core-format-text *ngIf="question.ddArea" [adaptImg]="false" [component]="component" [componentId]="componentId"
|
||||
[text]="question.ddArea" [filter]="false" (afterRender)="ddAreaRendered()">
|
||||
</core-format-text>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -12,11 +12,10 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, OnInit, OnDestroy, ElementRef } from '@angular/core';
|
||||
import { Component, OnDestroy, ElementRef } from '@angular/core';
|
||||
|
||||
import { AddonModQuizQuestionBasicData, CoreQuestionBaseComponent } from '@features/question/classes/base-question-component';
|
||||
import { CoreQuestionHelper } from '@features/question/services/question-helper';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { AddonQtypeDdImageOrTextQuestion } from '../classes/ddimageortext';
|
||||
|
||||
/**
|
||||
|
@ -27,9 +26,9 @@ import { AddonQtypeDdImageOrTextQuestion } from '../classes/ddimageortext';
|
|||
templateUrl: 'addon-qtype-ddimageortext.html',
|
||||
styleUrls: ['ddimageortext.scss'],
|
||||
})
|
||||
export class AddonQtypeDdImageOrTextComponent extends CoreQuestionBaseComponent implements OnInit, OnDestroy {
|
||||
|
||||
ddQuestion?: AddonModQuizDdImageOrTextQuestionData;
|
||||
export class AddonQtypeDdImageOrTextComponent
|
||||
extends CoreQuestionBaseComponent<AddonModQuizDdImageOrTextQuestionData>
|
||||
implements OnDestroy {
|
||||
|
||||
protected questionInstance?: AddonQtypeDdImageOrTextQuestion;
|
||||
protected drops?: unknown[]; // The drop zones received in the init object of the question.
|
||||
|
@ -44,50 +43,47 @@ export class AddonQtypeDdImageOrTextComponent extends CoreQuestionBaseComponent
|
|||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
init(): void {
|
||||
if (!this.question) {
|
||||
this.logger.warn('Aborting because of no question received.');
|
||||
|
||||
return CoreQuestionHelper.showComponentError(this.onAbort);
|
||||
return;
|
||||
}
|
||||
|
||||
this.ddQuestion = this.question;
|
||||
|
||||
const element = CoreDomUtils.convertToElement(this.ddQuestion.html);
|
||||
const questionElement = this.initComponent();
|
||||
if (!questionElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get D&D area and question text.
|
||||
const ddArea = element.querySelector('.ddarea');
|
||||
|
||||
this.ddQuestion.text = CoreDomUtils.getContentsOfElement(element, '.qtext');
|
||||
if (!ddArea || this.ddQuestion.text === undefined) {
|
||||
this.logger.warn('Aborting because of an error parsing question.', this.ddQuestion.slot);
|
||||
const ddArea = questionElement.querySelector('.ddarea');
|
||||
if (!ddArea) {
|
||||
this.logger.warn('Aborting because of an error parsing question.', this.question.slot);
|
||||
|
||||
return CoreQuestionHelper.showComponentError(this.onAbort);
|
||||
}
|
||||
|
||||
// Set the D&D area HTML.
|
||||
this.ddQuestion.ddArea = ddArea.outerHTML;
|
||||
this.ddQuestion.readOnly = false;
|
||||
this.question.ddArea = ddArea.outerHTML;
|
||||
this.question.readOnly = false;
|
||||
|
||||
if (this.ddQuestion.initObjects) {
|
||||
if (this.question.initObjects) {
|
||||
// Moodle version = 3.5.
|
||||
if (this.ddQuestion.initObjects.drops !== undefined) {
|
||||
this.drops = <unknown[]> this.ddQuestion.initObjects.drops;
|
||||
if (this.question.initObjects.drops !== undefined) {
|
||||
this.drops = <unknown[]> this.question.initObjects.drops;
|
||||
}
|
||||
if (this.ddQuestion.initObjects.readonly !== undefined) {
|
||||
this.ddQuestion.readOnly = !!this.ddQuestion.initObjects.readonly;
|
||||
if (this.question.initObjects.readonly !== undefined) {
|
||||
this.question.readOnly = !!this.question.initObjects.readonly;
|
||||
}
|
||||
} else if (this.ddQuestion.amdArgs) {
|
||||
} else if (this.question.amdArgs) {
|
||||
// Moodle version >= 3.6.
|
||||
if (this.ddQuestion.amdArgs[1] !== undefined) {
|
||||
this.ddQuestion.readOnly = !!this.ddQuestion.amdArgs[1];
|
||||
if (this.question.amdArgs[1] !== undefined) {
|
||||
this.question.readOnly = !!this.question.amdArgs[1];
|
||||
}
|
||||
if (this.ddQuestion.amdArgs[2] !== undefined) {
|
||||
this.drops = <unknown[]> this.ddQuestion.amdArgs[2];
|
||||
if (this.question.amdArgs[2] !== undefined) {
|
||||
this.drops = <unknown[]> this.question.amdArgs[2];
|
||||
}
|
||||
}
|
||||
|
||||
this.ddQuestion.loaded = false;
|
||||
this.question.loaded = false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -114,12 +110,12 @@ export class AddonQtypeDdImageOrTextComponent extends CoreQuestionBaseComponent
|
|||
* The question has been rendered.
|
||||
*/
|
||||
protected questionRendered(): void {
|
||||
if (!this.destroyed && this.ddQuestion) {
|
||||
if (!this.destroyed && this.question) {
|
||||
// Create the instance.
|
||||
this.questionInstance = new AddonQtypeDdImageOrTextQuestion(
|
||||
this.hostElement,
|
||||
this.ddQuestion,
|
||||
!!this.ddQuestion.readOnly,
|
||||
this.question,
|
||||
!!this.question.readOnly,
|
||||
this.drops,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,23 +1,23 @@
|
|||
<div *ngIf="ddQuestion && (ddQuestion.text || ddQuestion.text === '')" class="addon-qtype-ddmarker-container">
|
||||
<div *ngIf="question && (question.text || question.text === '')" class="addon-qtype-ddmarker-container">
|
||||
<!-- Content is outside the core-loading to let the script calculate drag items position -->
|
||||
<core-loading [hideUntil]="ddQuestion.loaded"></core-loading>
|
||||
<core-loading [hideUntil]="question.loaded"></core-loading>
|
||||
|
||||
<ion-item class="ion-text-wrap" [hidden]="!ddQuestion.loaded">
|
||||
<ion-item class="ion-text-wrap" [hidden]="!question.loaded">
|
||||
<ion-label>
|
||||
<ion-card *ngIf="!ddQuestion.readOnly" class="core-info-card">
|
||||
<ion-card *ngIf="!question.readOnly" class="core-info-card">
|
||||
<ion-item>
|
||||
<ion-icon name="fas-info-circle" slot="start" aria-hidden="true"></ion-icon>
|
||||
<ion-label>{{ 'core.question.howtodraganddrop' | translate }}</ion-label>
|
||||
</ion-item>
|
||||
</ion-card>
|
||||
<core-format-text [component]="component" [componentId]="componentId" [text]="ddQuestion.text" #questiontext
|
||||
<core-format-text [component]="component" [componentId]="componentId" [text]="question.text" #questiontext
|
||||
[contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="courseId" (afterRender)="textRendered()">
|
||||
</core-format-text>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<div class="fake-ion-item ion-text-wrap" [hidden]="!ddQuestion.loaded">
|
||||
<core-format-text *ngIf="ddQuestion.ddArea" [adaptImg]="false" [component]="component" [componentId]="componentId"
|
||||
[text]="ddQuestion.ddArea" [filter]="false" (afterRender)="ddAreaRendered()">
|
||||
<div class="fake-ion-item ion-text-wrap" [hidden]="!question.loaded">
|
||||
<core-format-text *ngIf="question.ddArea" [adaptImg]="false" [component]="component" [componentId]="componentId"
|
||||
[text]="question.ddArea" [filter]="false" (afterRender)="ddAreaRendered()">
|
||||
</core-format-text>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, OnInit, OnDestroy, ElementRef, ViewChild } from '@angular/core';
|
||||
import { Component, OnDestroy, ElementRef, ViewChild } from '@angular/core';
|
||||
|
||||
import { AddonModQuizQuestionBasicData, CoreQuestionBaseComponent } from '@features/question/classes/base-question-component';
|
||||
import { CoreQuestionHelper } from '@features/question/services/question-helper';
|
||||
|
@ -29,12 +29,12 @@ import { AddonQtypeDdMarkerQuestion } from '../classes/ddmarker';
|
|||
templateUrl: 'addon-qtype-ddmarker.html',
|
||||
styleUrls: ['ddmarker.scss'],
|
||||
})
|
||||
export class AddonQtypeDdMarkerComponent extends CoreQuestionBaseComponent implements OnInit, OnDestroy {
|
||||
export class AddonQtypeDdMarkerComponent
|
||||
extends CoreQuestionBaseComponent<AddonQtypeDdMarkerQuestionData>
|
||||
implements OnDestroy {
|
||||
|
||||
@ViewChild('questiontext') questionTextEl?: ElementRef;
|
||||
|
||||
ddQuestion?: AddonQtypeDdMarkerQuestionData;
|
||||
|
||||
protected questionInstance?: AddonQtypeDdMarkerQuestion;
|
||||
protected dropZones: unknown[] = []; // The drop zones received in the init object of the question.
|
||||
protected imgSrc?: string; // Background image URL.
|
||||
|
@ -49,65 +49,64 @@ export class AddonQtypeDdMarkerComponent extends CoreQuestionBaseComponent imple
|
|||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
init(): void {
|
||||
if (!this.question) {
|
||||
this.logger.warn('Aborting because of no question received.');
|
||||
|
||||
return CoreQuestionHelper.showComponentError(this.onAbort);
|
||||
return;
|
||||
}
|
||||
|
||||
this.ddQuestion = this.question;
|
||||
const element = CoreDomUtils.convertToElement(this.question.html);
|
||||
const questionElement = this.initComponent();
|
||||
if (!questionElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get D&D area, form and question text.
|
||||
const ddArea = element.querySelector('.ddarea');
|
||||
const ddForm = element.querySelector('.ddform');
|
||||
const ddArea = questionElement.querySelector('.ddarea');
|
||||
const ddForm = questionElement.querySelector('.ddform');
|
||||
|
||||
this.ddQuestion.text = CoreDomUtils.getContentsOfElement(element, '.qtext');
|
||||
if (!ddArea || !ddForm || this.ddQuestion.text === undefined) {
|
||||
this.logger.warn('Aborting because of an error parsing question.', this.ddQuestion.slot);
|
||||
if (!ddArea || !ddForm) {
|
||||
this.logger.warn('Aborting because of an error parsing question.', this.question.slot);
|
||||
|
||||
return CoreQuestionHelper.showComponentError(this.onAbort);
|
||||
}
|
||||
|
||||
// Build the D&D area HTML.
|
||||
this.ddQuestion.ddArea = ddArea.outerHTML;
|
||||
this.question.ddArea = ddArea.outerHTML;
|
||||
|
||||
const wrongParts = element.querySelector('.wrongparts');
|
||||
const wrongParts = questionElement.querySelector('.wrongparts');
|
||||
if (wrongParts) {
|
||||
this.ddQuestion.ddArea += wrongParts.outerHTML;
|
||||
this.question.ddArea += wrongParts.outerHTML;
|
||||
}
|
||||
this.ddQuestion.ddArea += ddForm.outerHTML;
|
||||
this.ddQuestion.readOnly = false;
|
||||
this.question.ddArea += ddForm.outerHTML;
|
||||
this.question.readOnly = false;
|
||||
|
||||
if (this.ddQuestion.initObjects) {
|
||||
if (this.question.initObjects) {
|
||||
// Moodle version = 3.5.
|
||||
if (this.ddQuestion.initObjects.dropzones !== undefined) {
|
||||
this.dropZones = <unknown[]> this.ddQuestion.initObjects.dropzones;
|
||||
if (this.question.initObjects.dropzones !== undefined) {
|
||||
this.dropZones = <unknown[]> this.question.initObjects.dropzones;
|
||||
}
|
||||
if (this.ddQuestion.initObjects.readonly !== undefined) {
|
||||
this.ddQuestion.readOnly = !!this.ddQuestion.initObjects.readonly;
|
||||
if (this.question.initObjects.readonly !== undefined) {
|
||||
this.question.readOnly = !!this.question.initObjects.readonly;
|
||||
}
|
||||
} else if (this.ddQuestion.amdArgs) {
|
||||
} else if (this.question.amdArgs) {
|
||||
// Moodle version >= 3.6.
|
||||
let nextIndex = 1;
|
||||
// Moodle version >= 3.9, imgSrc is not specified, do not advance index.
|
||||
if (this.ddQuestion.amdArgs[nextIndex] !== undefined && typeof this.ddQuestion.amdArgs[nextIndex] != 'boolean') {
|
||||
this.imgSrc = <string> this.ddQuestion.amdArgs[nextIndex];
|
||||
if (this.question.amdArgs[nextIndex] !== undefined && typeof this.question.amdArgs[nextIndex] !== 'boolean') {
|
||||
this.imgSrc = <string> this.question.amdArgs[nextIndex];
|
||||
nextIndex++;
|
||||
}
|
||||
|
||||
if (this.ddQuestion.amdArgs[nextIndex] !== undefined) {
|
||||
this.ddQuestion.readOnly = !!this.ddQuestion.amdArgs[nextIndex];
|
||||
if (this.question.amdArgs[nextIndex] !== undefined) {
|
||||
this.question.readOnly = !!this.question.amdArgs[nextIndex];
|
||||
}
|
||||
nextIndex++;
|
||||
|
||||
if (this.ddQuestion.amdArgs[nextIndex] !== undefined) {
|
||||
this.dropZones = <unknown[]> this.ddQuestion.amdArgs[nextIndex];
|
||||
if (this.question.amdArgs[nextIndex] !== undefined) {
|
||||
this.dropZones = <unknown[]> this.question.amdArgs[nextIndex];
|
||||
}
|
||||
}
|
||||
|
||||
this.ddQuestion.loaded = false;
|
||||
this.question.loaded = false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -134,9 +133,10 @@ export class AddonQtypeDdMarkerComponent extends CoreQuestionBaseComponent imple
|
|||
* The question has been rendered.
|
||||
*/
|
||||
protected async questionRendered(): Promise<void> {
|
||||
if (this.destroyed) {
|
||||
if (this.destroyed || !this.question) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Download background image (3.6+ sites).
|
||||
let imgSrc = this.imgSrc;
|
||||
const site = CoreSites.getCurrentSite();
|
||||
|
@ -160,8 +160,8 @@ export class AddonQtypeDdMarkerComponent extends CoreQuestionBaseComponent imple
|
|||
// Create the instance.
|
||||
this.questionInstance = new AddonQtypeDdMarkerQuestion(
|
||||
this.hostElement,
|
||||
this.ddQuestion!,
|
||||
!!this.ddQuestion!.readOnly,
|
||||
this.question,
|
||||
!!this.question.readOnly,
|
||||
this.dropZones,
|
||||
imgSrc,
|
||||
);
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
<div *ngIf="ddQuestion && (ddQuestion.text || ddQuestion.text === '')">
|
||||
<div *ngIf="question && (question.text || question.text === '')">
|
||||
<!-- Content is outside the core-loading to let the script calculate drag items position -->
|
||||
<core-loading [hideUntil]="ddQuestion.loaded"></core-loading>
|
||||
<core-loading [hideUntil]="question.loaded"></core-loading>
|
||||
|
||||
<div class="fake-ion-item ion-text-wrap" [hidden]="!ddQuestion.loaded">
|
||||
<ion-card *ngIf="!ddQuestion.readOnly" class="core-info-card">
|
||||
<div class="fake-ion-item ion-text-wrap" [hidden]="!question.loaded">
|
||||
<ion-card *ngIf="!question.readOnly" class="core-info-card">
|
||||
<ion-item>
|
||||
<ion-icon name="fas-info-circle" slot="start" aria-hidden="true"></ion-icon>
|
||||
<ion-label>{{ 'core.question.howtodraganddrop' | translate }}</ion-label>
|
||||
</ion-item>
|
||||
</ion-card>
|
||||
<div class="addon-qtype-ddwtos-container">
|
||||
<core-format-text [component]="component" [componentId]="componentId" [text]="ddQuestion.text" [contextLevel]="contextLevel"
|
||||
<core-format-text [component]="component" [componentId]="componentId" [text]="question.text" [contextLevel]="contextLevel"
|
||||
[contextInstanceId]="contextInstanceId" [courseId]="courseId" #questiontext (afterRender)="textRendered()">
|
||||
</core-format-text>
|
||||
|
||||
<core-format-text *ngIf="ddQuestion.answers" [component]="component" [componentId]="componentId" [text]="ddQuestion.answers"
|
||||
<core-format-text *ngIf="question.answers" [component]="component" [componentId]="componentId" [text]="question.answers"
|
||||
[filter]="false" (afterRender)="answersRendered()">
|
||||
</core-format-text>
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, OnInit, OnDestroy, ElementRef, ViewChild } from '@angular/core';
|
||||
import { Component, OnDestroy, ElementRef, ViewChild } from '@angular/core';
|
||||
|
||||
import { AddonModQuizQuestionBasicData, CoreQuestionBaseComponent } from '@features/question/classes/base-question-component';
|
||||
import { CoreQuestionHelper } from '@features/question/services/question-helper';
|
||||
|
@ -27,12 +27,10 @@ import { AddonQtypeDdwtosQuestion } from '../classes/ddwtos';
|
|||
templateUrl: 'addon-qtype-ddwtos.html',
|
||||
styleUrls: ['ddwtos.scss'],
|
||||
})
|
||||
export class AddonQtypeDdwtosComponent extends CoreQuestionBaseComponent implements OnInit, OnDestroy {
|
||||
export class AddonQtypeDdwtosComponent extends CoreQuestionBaseComponent<AddonModQuizDdwtosQuestionData> implements OnDestroy {
|
||||
|
||||
@ViewChild('questiontext') questionTextEl?: ElementRef;
|
||||
|
||||
ddQuestion?: AddonModQuizDdwtosQuestionData;
|
||||
|
||||
protected questionInstance?: AddonQtypeDdwtosQuestion;
|
||||
protected inputIds: string[] = []; // Ids of the inputs of the question (where the answers will be stored).
|
||||
protected destroyed = false;
|
||||
|
@ -46,52 +44,50 @@ export class AddonQtypeDdwtosComponent extends CoreQuestionBaseComponent impleme
|
|||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
init(): void {
|
||||
if (!this.question) {
|
||||
this.logger.warn('Aborting because of no question received.');
|
||||
|
||||
return CoreQuestionHelper.showComponentError(this.onAbort);
|
||||
return;
|
||||
}
|
||||
|
||||
this.ddQuestion = this.question;
|
||||
const element = CoreDomUtils.convertToElement(this.ddQuestion.html);
|
||||
const questionElement = this.initComponent();
|
||||
if (!questionElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Replace Moodle's correct/incorrect and feedback classes with our own.
|
||||
CoreQuestionHelper.replaceCorrectnessClasses(element);
|
||||
CoreQuestionHelper.replaceFeedbackClasses(element);
|
||||
CoreQuestionHelper.replaceCorrectnessClasses(questionElement);
|
||||
CoreQuestionHelper.replaceFeedbackClasses(questionElement);
|
||||
|
||||
// Treat the correct/incorrect icons.
|
||||
CoreQuestionHelper.treatCorrectnessIcons(element);
|
||||
CoreQuestionHelper.treatCorrectnessIcons(questionElement);
|
||||
|
||||
const answerContainer = element.querySelector('.answercontainer');
|
||||
const answerContainer = questionElement.querySelector('.answercontainer');
|
||||
if (!answerContainer) {
|
||||
this.logger.warn('Aborting because of an error parsing question.', this.ddQuestion.slot);
|
||||
this.logger.warn('Aborting because of an error parsing question.', this.question.slot);
|
||||
|
||||
return CoreQuestionHelper.showComponentError(this.onAbort);
|
||||
}
|
||||
|
||||
this.ddQuestion.readOnly = answerContainer.classList.contains('readonly');
|
||||
this.ddQuestion.answers = answerContainer.outerHTML;
|
||||
|
||||
this.ddQuestion.text = CoreDomUtils.getContentsOfElement(element, '.qtext');
|
||||
if (this.ddQuestion.text === undefined) {
|
||||
this.logger.warn('Aborting because of an error parsing question.', this.ddQuestion.slot);
|
||||
|
||||
return CoreQuestionHelper.showComponentError(this.onAbort);
|
||||
}
|
||||
this.question.readOnly = answerContainer.classList.contains('readonly');
|
||||
this.question.answers = answerContainer.outerHTML;
|
||||
|
||||
// Get the inputs where the answers will be stored and add them to the question text.
|
||||
const inputEls = <HTMLElement[]> Array.from(element.querySelectorAll('input[type="hidden"]:not([name*=sequencecheck])'));
|
||||
const inputEls = Array.from(
|
||||
questionElement.querySelectorAll<HTMLInputElement>('input[type="hidden"]:not([name*=sequencecheck])'),
|
||||
);
|
||||
|
||||
let questionText = this.question.text;
|
||||
inputEls.forEach((inputEl) => {
|
||||
this.ddQuestion!.text += inputEl.outerHTML;
|
||||
questionText += inputEl.outerHTML;
|
||||
const id = inputEl.getAttribute('id');
|
||||
if (id) {
|
||||
this.inputIds.push(id);
|
||||
}
|
||||
});
|
||||
|
||||
this.ddQuestion.loaded = false;
|
||||
this.question.text = questionText;
|
||||
|
||||
this.question.loaded = false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -118,7 +114,7 @@ export class AddonQtypeDdwtosComponent extends CoreQuestionBaseComponent impleme
|
|||
* The question has been rendered.
|
||||
*/
|
||||
protected async questionRendered(): Promise<void> {
|
||||
if (this.destroyed) {
|
||||
if (this.destroyed || !this.question) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -129,8 +125,8 @@ export class AddonQtypeDdwtosComponent extends CoreQuestionBaseComponent impleme
|
|||
// Create the instance.
|
||||
this.questionInstance = new AddonQtypeDdwtosQuestion(
|
||||
this.hostElement,
|
||||
this.ddQuestion!,
|
||||
!!this.ddQuestion!.readOnly,
|
||||
this.question,
|
||||
!!this.question.readOnly,
|
||||
this.inputIds,
|
||||
);
|
||||
|
||||
|
@ -143,7 +139,7 @@ export class AddonQtypeDdwtosComponent extends CoreQuestionBaseComponent impleme
|
|||
this.courseId,
|
||||
);
|
||||
|
||||
this.ddQuestion!.loaded = true;
|
||||
this.question.loaded = true;
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -12,8 +12,7 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, OnInit, ElementRef } from '@angular/core';
|
||||
|
||||
import { Component, ElementRef } from '@angular/core';
|
||||
import { CoreQuestionBaseComponent } from '@features/question/classes/base-question-component';
|
||||
|
||||
/**
|
||||
|
@ -23,7 +22,7 @@ import { CoreQuestionBaseComponent } from '@features/question/classes/base-quest
|
|||
selector: 'addon-qtype-description',
|
||||
templateUrl: 'addon-qtype-description.html',
|
||||
})
|
||||
export class AddonQtypeDescriptionComponent extends CoreQuestionBaseComponent implements OnInit {
|
||||
export class AddonQtypeDescriptionComponent extends CoreQuestionBaseComponent {
|
||||
|
||||
seenInput?: { name: string; value: string };
|
||||
|
||||
|
@ -34,20 +33,22 @@ export class AddonQtypeDescriptionComponent extends CoreQuestionBaseComponent im
|
|||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
init(): void {
|
||||
const questionEl = this.initComponent();
|
||||
if (!questionEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the "seen" hidden input.
|
||||
const input = <HTMLInputElement> questionEl.querySelector('input[type="hidden"][name*=seen]');
|
||||
if (input) {
|
||||
this.seenInput = {
|
||||
name: input.name,
|
||||
value: input.value,
|
||||
};
|
||||
const input = questionEl.querySelector<HTMLInputElement>('input[type="hidden"][name*=seen]');
|
||||
if (!input) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.seenInput = {
|
||||
name: input.name,
|
||||
value: input.value,
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<ion-list *ngIf="essayQuestion && (essayQuestion.text || essayQuestion.text === '')">
|
||||
<ion-list *ngIf="question && (question.text || question.text === '')">
|
||||
<!-- Question text. -->
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-label>
|
||||
<core-format-text [component]="component" [componentId]="componentId" [text]="essayQuestion.text" [contextLevel]="contextLevel"
|
||||
<core-format-text [component]="component" [componentId]="componentId" [text]="question.text" [contextLevel]="contextLevel"
|
||||
[contextInstanceId]="contextInstanceId" [courseId]="courseId">
|
||||
</core-format-text>
|
||||
</ion-label>
|
||||
|
@ -11,27 +11,26 @@
|
|||
<!-- Editing the question. -->
|
||||
<ng-container *ngIf="!review">
|
||||
<!-- Textarea. -->
|
||||
<ion-item *ngIf="essayQuestion.textarea && (!essayQuestion.hasDraftFiles || uploadFilesSupported)">
|
||||
<ion-item *ngIf="question.textarea && (!question.hasDraftFiles || uploadFilesSupported)">
|
||||
<ion-label class="sr-only">{{ 'core.question.answer' | translate }}</ion-label>
|
||||
<!-- "Format" and draftid hidden inputs -->
|
||||
<input *ngIf="essayQuestion.formatInput" type="hidden" [name]="essayQuestion.formatInput.name"
|
||||
[value]="essayQuestion.formatInput.value">
|
||||
<input *ngIf="essayQuestion.answerDraftIdInput" type="hidden" [name]="essayQuestion.answerDraftIdInput.name"
|
||||
[value]="essayQuestion.answerDraftIdInput.value">
|
||||
<input *ngIf="question.formatInput" type="hidden" [name]="question.formatInput.name" [value]="question.formatInput.value">
|
||||
<input *ngIf="question.answerDraftIdInput" type="hidden" [name]="question.answerDraftIdInput.name"
|
||||
[value]="question.answerDraftIdInput.value">
|
||||
<!-- Plain text textarea. -->
|
||||
<ion-textarea *ngIf="essayQuestion.isPlainText" class="core-question-textarea"
|
||||
[ngClass]='{"core-monospaced": essayQuestion.isMonospaced}' placeholder="{{ 'core.question.answer' | translate }}"
|
||||
[attr.name]="essayQuestion.textarea.name" [ngModel]="essayQuestion.textarea.text">
|
||||
<ion-textarea *ngIf="question.isPlainText" class="core-question-textarea" [ngClass]='{"core-monospaced": question.isMonospaced}'
|
||||
placeholder="{{ 'core.question.answer' | translate }}" [attr.name]="question.textarea.name"
|
||||
[ngModel]="question.textarea.text">
|
||||
</ion-textarea>
|
||||
<!-- Rich text editor. -->
|
||||
<core-rich-text-editor *ngIf="!essayQuestion.isPlainText" placeholder="{{ 'core.question.answer' | translate }}"
|
||||
[control]="formControl" [name]="essayQuestion.textarea.name" [component]="component" [componentId]="componentId"
|
||||
<core-rich-text-editor *ngIf="!question.isPlainText" placeholder="{{ 'core.question.answer' | translate }}"
|
||||
[control]="formControl" [name]="question.textarea.name" [component]="component" [componentId]="componentId"
|
||||
[autoSave]="false">
|
||||
</core-rich-text-editor>
|
||||
</ion-item>
|
||||
|
||||
<!-- Draft files not supported. -->
|
||||
<ng-container *ngIf="essayQuestion.textarea && essayQuestion.hasDraftFiles && !uploadFilesSupported">
|
||||
<ng-container *ngIf="question.textarea && question.hasDraftFiles && !uploadFilesSupported">
|
||||
<ion-item class="ion-text-wrap core-danger-item">
|
||||
<ion-label class="core-question-warning">
|
||||
{{ 'core.question.errorembeddedfilesnotsupportedinsite' | translate }}
|
||||
|
@ -39,7 +38,7 @@
|
|||
</ion-item>
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-label>
|
||||
<core-format-text [component]="component" [componentId]="componentId" [text]="essayQuestion.textarea.text"
|
||||
<core-format-text [component]="component" [componentId]="componentId" [text]="question.textarea.text"
|
||||
[contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="courseId">
|
||||
</core-format-text>
|
||||
</ion-label>
|
||||
|
@ -47,15 +46,14 @@
|
|||
</ng-container>
|
||||
|
||||
<!-- Attachments. -->
|
||||
<ng-container *ngIf="essayQuestion.allowsAttachments">
|
||||
<core-attachments *ngIf="uploadFilesSupported && essayQuestion.attachmentsDraftIdInput" [files]="attachments"
|
||||
[component]="component" [componentId]="componentId" [maxSize]="essayQuestion.attachmentsMaxBytes"
|
||||
[maxSubmissions]="essayQuestion.attachmentsMaxFiles" [allowOffline]="offlineEnabled"
|
||||
[acceptedTypes]="essayQuestion.attachmentsAcceptedTypes" [courseId]="courseId">
|
||||
<ng-container *ngIf="question.allowsAttachments">
|
||||
<core-attachments *ngIf="uploadFilesSupported && question.attachmentsDraftIdInput" [files]="attachments" [component]="component"
|
||||
[componentId]="componentId" [maxSize]="question.attachmentsMaxBytes" [maxSubmissions]="question.attachmentsMaxFiles"
|
||||
[allowOffline]="offlineEnabled" [acceptedTypes]="question.attachmentsAcceptedTypes" [courseId]="courseId">
|
||||
</core-attachments>
|
||||
|
||||
<input *ngIf="essayQuestion.attachmentsDraftIdInput" type="hidden" [name]="essayQuestion.attachmentsDraftIdInput.name"
|
||||
[value]="essayQuestion.attachmentsDraftIdInput.value">
|
||||
<input *ngIf="question.attachmentsDraftIdInput" type="hidden" [name]="question.attachmentsDraftIdInput.name"
|
||||
[value]="question.attachmentsDraftIdInput.value">
|
||||
|
||||
<!-- Attachments not supported in this site. -->
|
||||
<ion-item *ngIf="!uploadFilesSupported" class="ion-text-wrap core-danger-item">
|
||||
|
@ -69,36 +67,35 @@
|
|||
<!-- Reviewing the question. -->
|
||||
<ng-container *ngIf="review">
|
||||
<!-- Answer to the question and attachments (reviewing). -->
|
||||
<ion-item class="ion-text-wrap" *ngIf="essayQuestion.answer || essayQuestion.answer == ''">
|
||||
<ion-item class="ion-text-wrap" *ngIf="question.answer || question.answer == ''">
|
||||
<ion-label>
|
||||
<core-format-text [ngClass]='{"core-monospaced": essayQuestion.isMonospaced}' [component]="component"
|
||||
[componentId]="componentId" [text]="essayQuestion.answer" [contextLevel]="contextLevel"
|
||||
[contextInstanceId]="contextInstanceId" [courseId]="courseId">
|
||||
<core-format-text [ngClass]='{"core-monospaced": question.isMonospaced}' [component]="component" [componentId]="componentId"
|
||||
[text]="question.answer" [contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="courseId">
|
||||
</core-format-text>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<!-- Word count info. -->
|
||||
<ion-item class="ion-text-wrap" *ngIf="essayQuestion.wordCountInfo">
|
||||
<ion-item class="ion-text-wrap" *ngIf="question.wordCountInfo">
|
||||
<ion-label>
|
||||
<core-format-text [component]="component" [componentId]="componentId" [text]="essayQuestion.wordCountInfo"
|
||||
<core-format-text [component]="component" [componentId]="componentId" [text]="question.wordCountInfo"
|
||||
[contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="courseId">
|
||||
</core-format-text>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<!-- Answer plagiarism. -->
|
||||
<ion-item class="ion-text-wrap" *ngIf="essayQuestion.answerPlagiarism">
|
||||
<ion-item class="ion-text-wrap" *ngIf="question.answerPlagiarism">
|
||||
<ion-label>
|
||||
<core-format-text [component]="component" [componentId]="componentId" [text]="essayQuestion.answerPlagiarism"
|
||||
<core-format-text [component]="component" [componentId]="componentId" [text]="question.answerPlagiarism"
|
||||
[contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="courseId">
|
||||
</core-format-text>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<!-- List of attachments when reviewing. -->
|
||||
<core-files *ngIf="essayQuestion.attachments" [files]="essayQuestion.attachments" [component]="component"
|
||||
[componentId]="componentId" [extraHtml]="essayQuestion.attachmentsPlagiarisms">
|
||||
<core-files *ngIf="question.attachments" [files]="question.attachments" [component]="component" [componentId]="componentId"
|
||||
[extraHtml]="question.attachmentsPlagiarisms">
|
||||
</core-files>
|
||||
</ng-container>
|
||||
</ion-list>
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, OnInit, ElementRef } from '@angular/core';
|
||||
import { Component, ElementRef } from '@angular/core';
|
||||
import { FormBuilder, FormControl } from '@angular/forms';
|
||||
import { FileEntry } from '@ionic-native/file/ngx';
|
||||
|
||||
|
@ -30,12 +30,11 @@ import { CoreFileEntry } from '@services/file-helper';
|
|||
selector: 'addon-qtype-essay',
|
||||
templateUrl: 'addon-qtype-essay.html',
|
||||
})
|
||||
export class AddonQtypeEssayComponent extends CoreQuestionBaseComponent implements OnInit {
|
||||
export class AddonQtypeEssayComponent extends CoreQuestionBaseComponent<AddonModQuizEssayQuestion> {
|
||||
|
||||
formControl?: FormControl;
|
||||
attachments?: CoreFileEntry[];
|
||||
uploadFilesSupported = false;
|
||||
essayQuestion?: AddonModQuizEssayQuestion;
|
||||
|
||||
constructor(elementRef: ElementRef, protected fb: FormBuilder) {
|
||||
super('AddonQtypeEssayComponent', elementRef);
|
||||
|
@ -44,14 +43,18 @@ export class AddonQtypeEssayComponent extends CoreQuestionBaseComponent implemen
|
|||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.uploadFilesSupported = this.question?.responsefileareas !== undefined;
|
||||
init(): void {
|
||||
if (!this.question) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.uploadFilesSupported = this.question.responsefileareas !== undefined;
|
||||
|
||||
this.initEssayComponent(this.review);
|
||||
this.essayQuestion = this.question;
|
||||
|
||||
this.formControl = this.fb.control(this.essayQuestion?.textarea?.text);
|
||||
this.formControl = this.fb.control(this.question?.textarea?.text);
|
||||
|
||||
if (this.essayQuestion?.allowsAttachments && this.uploadFilesSupported && !this.review) {
|
||||
if (this.question?.allowsAttachments && this.uploadFilesSupported && !this.review) {
|
||||
this.loadAttachments();
|
||||
}
|
||||
}
|
||||
|
@ -62,10 +65,14 @@ export class AddonQtypeEssayComponent extends CoreQuestionBaseComponent implemen
|
|||
* @returns Promise resolved when done.
|
||||
*/
|
||||
async loadAttachments(): Promise<void> {
|
||||
if (this.offlineEnabled && this.essayQuestion?.localAnswers?.attachments_offline) {
|
||||
if (!this.question) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.offlineEnabled && this.question.localAnswers?.attachments_offline) {
|
||||
|
||||
const attachmentsData: CoreFileUploaderStoreFilesResult = CoreTextUtils.parseJSON(
|
||||
this.essayQuestion.localAnswers.attachments_offline,
|
||||
this.question.localAnswers.attachments_offline,
|
||||
{
|
||||
online: [],
|
||||
offline: 0,
|
||||
|
@ -75,7 +82,7 @@ export class AddonQtypeEssayComponent extends CoreQuestionBaseComponent implemen
|
|||
|
||||
if (attachmentsData.offline) {
|
||||
offlineFiles = <FileEntry[]> await CoreQuestionHelper.getStoredQuestionFiles(
|
||||
this.essayQuestion,
|
||||
this.question,
|
||||
this.component || '',
|
||||
this.componentId || -1,
|
||||
);
|
||||
|
@ -83,12 +90,12 @@ export class AddonQtypeEssayComponent extends CoreQuestionBaseComponent implemen
|
|||
|
||||
this.attachments = [...attachmentsData.online, ...offlineFiles];
|
||||
} else {
|
||||
this.attachments = Array.from(CoreQuestionHelper.getResponseFileAreaFiles(this.question!, 'attachments'));
|
||||
this.attachments = Array.from(CoreQuestionHelper.getResponseFileAreaFiles(this.question, 'attachments'));
|
||||
}
|
||||
|
||||
CoreFileSession.setFiles(
|
||||
this.component || '',
|
||||
CoreQuestion.getQuestionComponentId(this.question!, this.componentId || -1),
|
||||
CoreQuestion.getQuestionComponentId(this.question, this.componentId || -1),
|
||||
this.attachments,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, OnInit, ElementRef } from '@angular/core';
|
||||
import { Component, ElementRef } from '@angular/core';
|
||||
|
||||
import { CoreQuestionBaseComponent } from '@features/question/classes/base-question-component';
|
||||
import { CoreQuestionHelper } from '@features/question/services/question-helper';
|
||||
|
@ -25,7 +25,7 @@ import { CoreQuestionHelper } from '@features/question/services/question-helper'
|
|||
templateUrl: 'addon-qtype-gapselect.html',
|
||||
styleUrls: ['gapselect.scss'],
|
||||
})
|
||||
export class AddonQtypeGapSelectComponent extends CoreQuestionBaseComponent implements OnInit {
|
||||
export class AddonQtypeGapSelectComponent extends CoreQuestionBaseComponent {
|
||||
|
||||
constructor(elementRef: ElementRef) {
|
||||
super('AddonQtypeGapSelectComponent', elementRef);
|
||||
|
@ -34,7 +34,7 @@ export class AddonQtypeGapSelectComponent extends CoreQuestionBaseComponent impl
|
|||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
init(): void {
|
||||
this.initOriginalTextComponent('.qtext');
|
||||
}
|
||||
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
<section class="addon-qtype-match-container" *ngIf="matchQuestion && matchQuestion.loaded">
|
||||
<section class="addon-qtype-match-container" *ngIf="question && question.loaded">
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-label>
|
||||
<core-format-text [component]="component" [componentId]="componentId" [text]="matchQuestion.text" [contextLevel]="contextLevel"
|
||||
<core-format-text [component]="component" [componentId]="componentId" [text]="question.text" [contextLevel]="contextLevel"
|
||||
[contextInstanceId]="contextInstanceId" [courseId]="courseId">
|
||||
</core-format-text>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngFor="let row of matchQuestion.rows">
|
||||
<ion-item class="ion-text-wrap" *ngFor="let row of question.rows">
|
||||
<ion-label>
|
||||
<core-format-text id="addon-qtype-match-question-{{row.id}}" [component]="component" [componentId]="componentId"
|
||||
[text]="row.text" [contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="courseId">
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, OnInit, ElementRef } from '@angular/core';
|
||||
import { Component, ElementRef } from '@angular/core';
|
||||
|
||||
import { AddonModQuizMatchQuestion, CoreQuestionBaseComponent } from '@features/question/classes/base-question-component';
|
||||
|
||||
|
@ -24,9 +24,7 @@ import { AddonModQuizMatchQuestion, CoreQuestionBaseComponent } from '@features/
|
|||
templateUrl: 'addon-qtype-match.html',
|
||||
styleUrls: ['match.scss'],
|
||||
})
|
||||
export class AddonQtypeMatchComponent extends CoreQuestionBaseComponent implements OnInit {
|
||||
|
||||
matchQuestion?: AddonModQuizMatchQuestion;
|
||||
export class AddonQtypeMatchComponent extends CoreQuestionBaseComponent<AddonModQuizMatchQuestion> {
|
||||
|
||||
constructor(elementRef: ElementRef) {
|
||||
super('AddonQtypeMatchComponent', elementRef);
|
||||
|
@ -35,9 +33,8 @@ export class AddonQtypeMatchComponent extends CoreQuestionBaseComponent implemen
|
|||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
init(): void {
|
||||
this.initMatchComponent();
|
||||
this.matchQuestion = this.question;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, OnInit, ElementRef } from '@angular/core';
|
||||
import { Component, ElementRef } from '@angular/core';
|
||||
import { CoreQuestionBaseComponent } from '@features/question/classes/base-question-component';
|
||||
import { CoreQuestionHelper } from '@features/question/services/question-helper';
|
||||
|
||||
|
@ -24,7 +24,7 @@ import { CoreQuestionHelper } from '@features/question/services/question-helper'
|
|||
templateUrl: 'addon-qtype-multianswer.html',
|
||||
styleUrls: ['multianswer.scss'],
|
||||
})
|
||||
export class AddonQtypeMultiAnswerComponent extends CoreQuestionBaseComponent implements OnInit {
|
||||
export class AddonQtypeMultiAnswerComponent extends CoreQuestionBaseComponent {
|
||||
|
||||
constructor(elementRef: ElementRef) {
|
||||
super('AddonQtypeMultiAnswerComponent', elementRef);
|
||||
|
@ -33,7 +33,7 @@ export class AddonQtypeMultiAnswerComponent extends CoreQuestionBaseComponent im
|
|||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
init(): void {
|
||||
this.initOriginalTextComponent('.formulation');
|
||||
}
|
||||
|
||||
|
|
|
@ -1,23 +1,23 @@
|
|||
<ion-list *ngIf="multiQuestion && (multiQuestion.text || multiQuestion.text === '')">
|
||||
<ion-list *ngIf="question && (question.text || question.text === '')">
|
||||
<!-- Question text first. -->
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-label>
|
||||
<p>
|
||||
<core-format-text [component]="component" [componentId]="componentId" [text]="multiQuestion.text"
|
||||
[contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="courseId">
|
||||
<core-format-text [component]="component" [componentId]="componentId" [text]="question.text" [contextLevel]="contextLevel"
|
||||
[contextInstanceId]="contextInstanceId" [courseId]="courseId">
|
||||
</core-format-text>
|
||||
</p>
|
||||
<p *ngIf="multiQuestion.prompt">
|
||||
<core-format-text [component]="component" [componentId]="componentId" [text]="multiQuestion.prompt"
|
||||
[contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="courseId">
|
||||
<p *ngIf="question.prompt">
|
||||
<core-format-text [component]="component" [componentId]="componentId" [text]="question.prompt" [contextLevel]="contextLevel"
|
||||
[contextInstanceId]="contextInstanceId" [courseId]="courseId">
|
||||
</core-format-text>
|
||||
</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<!-- Checkbox for multiple choice. -->
|
||||
<ng-container *ngIf="multiQuestion.multi">
|
||||
<ion-item class="ion-text-wrap answer" *ngFor="let option of multiQuestion.options">
|
||||
<ng-container *ngIf="question.multi">
|
||||
<ion-item class="ion-text-wrap answer" *ngFor="let option of question.options">
|
||||
<ion-label [color]='(option.isCorrect === 1 ? "success": "") + (option.isCorrect === 0 ? "danger": "")' [class]="option.class">
|
||||
<core-format-text [component]="component" [componentId]="componentId" [text]="option.text" [contextLevel]="contextLevel"
|
||||
[contextInstanceId]="contextInstanceId" [courseId]="courseId">
|
||||
|
@ -44,8 +44,8 @@
|
|||
</ng-container>
|
||||
|
||||
<!-- Radio buttons for single choice. -->
|
||||
<ion-radio-group *ngIf="!multiQuestion.multi" [(ngModel)]="multiQuestion.singleChoiceModel" [name]="multiQuestion.optionsName">
|
||||
<ion-item class="ion-text-wrap answer" *ngFor="let option of multiQuestion.options">
|
||||
<ion-radio-group *ngIf="!question.multi" [(ngModel)]="question.singleChoiceModel" [name]="question.optionsName">
|
||||
<ion-item class="ion-text-wrap answer" *ngFor="let option of question.options">
|
||||
<ion-label [class]="option.class">
|
||||
<core-format-text [component]="component" [componentId]="componentId" [text]="option.text" [contextLevel]="contextLevel"
|
||||
[contextInstanceId]="contextInstanceId" [courseId]="courseId">
|
||||
|
@ -66,12 +66,12 @@
|
|||
<ion-icon *ngIf="option.isCorrect === 0" class="core-correct-icon" name="fas-times" color="danger"
|
||||
[attr.aria-label]="'core.question.incorrect' | translate"></ion-icon>
|
||||
</ion-item>
|
||||
<ion-button *ngIf="!multiQuestion.disabled" class="ion-text-wrap ion-margin-top" expand="block" fill="outline"
|
||||
[disabled]="!multiQuestion.singleChoiceModel" (click)="clear()" type="button">
|
||||
<ion-button *ngIf="!question.disabled" class="ion-text-wrap ion-margin-top" expand="block" fill="outline"
|
||||
[disabled]="!question.singleChoiceModel" (click)="clear()" type="button">
|
||||
{{ 'addon.mod_quiz.clearchoice' | translate }}
|
||||
</ion-button>
|
||||
|
||||
<!-- ion-radio doesn't use an input. Create a hidden input to hold the selected value. -->
|
||||
<input type="hidden" [ngModel]="multiQuestion.singleChoiceModel" [attr.name]="multiQuestion.optionsName">
|
||||
<input type="hidden" [ngModel]="question.singleChoiceModel" [attr.name]="question.optionsName">
|
||||
</ion-radio-group>
|
||||
</ion-list>
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, OnInit, ElementRef } from '@angular/core';
|
||||
import { Component, ElementRef } from '@angular/core';
|
||||
|
||||
import { AddonModQuizMultichoiceQuestion, CoreQuestionBaseComponent } from '@features/question/classes/base-question-component';
|
||||
|
||||
|
@ -24,9 +24,7 @@ import { AddonModQuizMultichoiceQuestion, CoreQuestionBaseComponent } from '@fea
|
|||
templateUrl: 'addon-qtype-multichoice.html',
|
||||
styleUrls: ['multichoice.scss'],
|
||||
})
|
||||
export class AddonQtypeMultichoiceComponent extends CoreQuestionBaseComponent implements OnInit {
|
||||
|
||||
multiQuestion?: AddonModQuizMultichoiceQuestion;
|
||||
export class AddonQtypeMultichoiceComponent extends CoreQuestionBaseComponent<AddonModQuizMultichoiceQuestion> {
|
||||
|
||||
constructor(elementRef: ElementRef) {
|
||||
super('AddonQtypeMultichoiceComponent', elementRef);
|
||||
|
@ -35,16 +33,19 @@ export class AddonQtypeMultichoiceComponent extends CoreQuestionBaseComponent im
|
|||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
init(): void {
|
||||
this.initMultichoiceComponent();
|
||||
this.multiQuestion = this.question;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear selected choices.
|
||||
*/
|
||||
clear(): void {
|
||||
this.multiQuestion!.singleChoiceModel = undefined;
|
||||
if (!this.question) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.question.singleChoiceModel = undefined;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -117,7 +117,7 @@ export class AddonQtypeMultichoiceHandlerService implements CoreQuestionHandler
|
|||
|
||||
// To know if it's single or multi answer we need to search for answers with "choice" in the name.
|
||||
for (const name in newAnswers) {
|
||||
if (name.indexOf('choice') != -1) {
|
||||
if (name.indexOf('choice') !== -1) {
|
||||
isSingle = false;
|
||||
if (!CoreUtils.sameAtKeyMissingIsBlank(prevAnswers, newAnswers, name)) {
|
||||
isMultiSame = false;
|
||||
|
@ -128,9 +128,9 @@ export class AddonQtypeMultichoiceHandlerService implements CoreQuestionHandler
|
|||
|
||||
if (isSingle) {
|
||||
return this.isSameResponseSingle(prevAnswers, newAnswers);
|
||||
} else {
|
||||
return isMultiSame;
|
||||
}
|
||||
|
||||
return isMultiSame;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -151,10 +151,11 @@ export class AddonQtypeMultichoiceHandlerService implements CoreQuestionHandler
|
|||
question: AddonModQuizMultichoiceQuestion,
|
||||
answers: CoreQuestionsAnswers,
|
||||
): void {
|
||||
if (question && !question.multi && answers[question.optionsName!] !== undefined && !answers[question.optionsName!]) {
|
||||
if (question && !question.multi &&
|
||||
question.optionsName && answers[question.optionsName] !== undefined && !answers[question.optionsName]) {
|
||||
/* It's a single choice and the user hasn't answered. Delete the answer because
|
||||
sending an empty string (default value) will mark the first option as selected. */
|
||||
delete answers[question.optionsName!];
|
||||
delete answers[question.optionsName];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,20 +1,19 @@
|
|||
<ion-list *ngIf="textQuestion && (textQuestion.text || textQuestion.text === '')">
|
||||
<ion-list *ngIf="question && (question.text || question.text === '')">
|
||||
<ion-item class="ion-text-wrap addon-qtype-shortanswer-text">
|
||||
<ion-label>
|
||||
<core-format-text [component]="component" [componentId]="componentId" [text]="textQuestion.text" [contextLevel]="contextLevel"
|
||||
<core-format-text [component]="component" [componentId]="componentId" [text]="question.text" [contextLevel]="contextLevel"
|
||||
[contextInstanceId]="contextInstanceId" [courseId]="courseId">
|
||||
</core-format-text>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item *ngIf="textQuestion.input && !textQuestion.input.isInline"
|
||||
class="ion-text-wrap addon-qtype-shortanswer-input core-{{textQuestion.input.correctIconColor}}-item">
|
||||
<ion-item *ngIf="question.input && !question.input.isInline"
|
||||
class="ion-text-wrap addon-qtype-shortanswer-input core-{{question.input.correctIconColor}}-item">
|
||||
<ion-label position="stacked">{{ 'addon.mod_quiz.answercolon' | translate }}</ion-label>
|
||||
<ion-input type="text" [placeholder]="textQuestion.input.readOnly ? '' : 'core.question.answer' | translate"
|
||||
[attr.name]="textQuestion.input.name" [value]="textQuestion.input.value" autocorrect="off"
|
||||
[disabled]="textQuestion.input.readOnly">
|
||||
<ion-input type="text" [placeholder]="question.input.readOnly ? '' : 'core.question.answer' | translate"
|
||||
[attr.name]="question.input.name" [value]="question.input.value" autocorrect="off" [disabled]="question.input.readOnly">
|
||||
</ion-input>
|
||||
<ion-icon *ngIf="textQuestion.input.correctIcon" class="core-correct-icon" slot="end" [name]="textQuestion.input.correctIcon"
|
||||
[color]="[textQuestion.input.correctIconColor]">
|
||||
<ion-icon *ngIf="question.input.correctIcon" class="core-correct-icon" slot="end" [name]="question.input.correctIcon"
|
||||
[color]="[question.input.correctIconColor]">
|
||||
</ion-icon>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, OnInit, ElementRef } from '@angular/core';
|
||||
import { Component, ElementRef } from '@angular/core';
|
||||
|
||||
import { AddonModQuizTextQuestion, CoreQuestionBaseComponent } from '@features/question/classes/base-question-component';
|
||||
|
||||
|
@ -24,9 +24,7 @@ import { AddonModQuizTextQuestion, CoreQuestionBaseComponent } from '@features/q
|
|||
templateUrl: 'addon-qtype-shortanswer.html',
|
||||
styleUrls: ['shortanswer.scss'],
|
||||
})
|
||||
export class AddonQtypeShortAnswerComponent extends CoreQuestionBaseComponent implements OnInit {
|
||||
|
||||
textQuestion?: AddonModQuizTextQuestion;
|
||||
export class AddonQtypeShortAnswerComponent extends CoreQuestionBaseComponent<AddonModQuizTextQuestion> {
|
||||
|
||||
constructor(elementRef: ElementRef) {
|
||||
super('AddonQtypeShortAnswerComponent', elementRef);
|
||||
|
@ -35,9 +33,8 @@ export class AddonQtypeShortAnswerComponent extends CoreQuestionBaseComponent im
|
|||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
init(): void {
|
||||
this.initInputTextComponent();
|
||||
this.textQuestion = this.question;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -83,9 +83,9 @@ export class AddonQtypeTrueFalseHandlerService implements CoreQuestionHandler {
|
|||
question: AddonModQuizMultichoiceQuestion,
|
||||
answers: CoreQuestionsAnswers,
|
||||
): void | Promise<void> {
|
||||
if (question && answers[question.optionsName!] !== undefined && !answers[question.optionsName!]) {
|
||||
if (question && question.optionsName && answers[question.optionsName] !== undefined && !answers[question.optionsName]) {
|
||||
// The user hasn't answered. Delete the answer to prevent marking one of the answers automatically.
|
||||
delete answers[question.optionsName!];
|
||||
delete answers[question.optionsName];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -145,7 +145,7 @@ export function conditionalRoutes(routes: Routes, condition: () => boolean): Rou
|
|||
|
||||
return {
|
||||
...newRoute,
|
||||
matcher: buildConditionalUrlMatcher(matcher || path!, condition),
|
||||
matcher: buildConditionalUrlMatcher(matcher || path || '', condition),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
|
@ -91,7 +91,12 @@ export class CoreQueueRunner {
|
|||
return;
|
||||
}
|
||||
|
||||
const item = this.orderedQueue.shift()!;
|
||||
const item = this.orderedQueue.shift();
|
||||
if (!item) {
|
||||
// No item found.
|
||||
return;
|
||||
}
|
||||
|
||||
this.numberRunning++;
|
||||
|
||||
try {
|
||||
|
|
|
@ -174,7 +174,8 @@ export class SQLiteDB {
|
|||
sql = equal ? '= ?' : '<> ?';
|
||||
params = Array.isArray(items) ? items : [items];
|
||||
} else {
|
||||
sql = (equal ? '' : 'NOT ') + 'IN (' + ',?'.repeat(items.length).substring(1) + ')';
|
||||
const questionMarks = ',?'.repeat(items.length).substring(1);
|
||||
sql = (equal ? '' : 'NOT ') + `IN (${questionMarks})`;
|
||||
params = items;
|
||||
}
|
||||
|
||||
|
@ -237,7 +238,7 @@ export class SQLiteDB {
|
|||
tableCheck?: string,
|
||||
): string {
|
||||
const columnsSql: string[] = [];
|
||||
let sql = `CREATE TABLE IF NOT EXISTS ${name} (`;
|
||||
let tableStructureSQL = '';
|
||||
|
||||
// First define all the columns.
|
||||
for (const index in columns) {
|
||||
|
@ -245,7 +246,7 @@ export class SQLiteDB {
|
|||
let columnSql: string = column.name || '';
|
||||
|
||||
if (column.type) {
|
||||
columnSql += ' ' + column.type;
|
||||
columnSql += ` ${column.type}`;
|
||||
}
|
||||
|
||||
if (column.primaryKey) {
|
||||
|
@ -273,25 +274,25 @@ export class SQLiteDB {
|
|||
|
||||
columnsSql.push(columnSql);
|
||||
}
|
||||
sql += columnsSql.join(', ');
|
||||
tableStructureSQL += columnsSql.join(', ');
|
||||
|
||||
// Now add the table constraints.
|
||||
|
||||
if (primaryKeys && primaryKeys.length) {
|
||||
sql += `, PRIMARY KEY (${primaryKeys.join(', ')})`;
|
||||
tableStructureSQL += `, PRIMARY KEY (${primaryKeys.join(', ')})`;
|
||||
}
|
||||
|
||||
if (uniqueKeys && uniqueKeys.length) {
|
||||
for (const index in uniqueKeys) {
|
||||
const setOfKeys = uniqueKeys[index];
|
||||
if (setOfKeys && setOfKeys.length) {
|
||||
sql += `, UNIQUE (${setOfKeys.join(', ')})`;
|
||||
tableStructureSQL += `, UNIQUE (${setOfKeys.join(', ')})`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (tableCheck) {
|
||||
sql += `, CHECK (${tableCheck})`;
|
||||
tableStructureSQL += `, CHECK (${tableCheck})`;
|
||||
}
|
||||
|
||||
for (const index in foreignKeys) {
|
||||
|
@ -301,18 +302,18 @@ export class SQLiteDB {
|
|||
continue;
|
||||
}
|
||||
|
||||
sql += `, FOREIGN KEY (${foreignKey.columns.join(', ')}) REFERENCES ${foreignKey.table} `;
|
||||
tableStructureSQL += `, FOREIGN KEY (${foreignKey.columns.join(', ')}) REFERENCES ${foreignKey.table} `;
|
||||
|
||||
if (foreignKey.foreignColumns && foreignKey.foreignColumns.length) {
|
||||
sql += `(${foreignKey.foreignColumns.join(', ')})`;
|
||||
tableStructureSQL += `(${foreignKey.foreignColumns.join(', ')})`;
|
||||
}
|
||||
|
||||
if (foreignKey.actions) {
|
||||
sql += ` ${foreignKey.actions}`;
|
||||
tableStructureSQL += ` ${foreignKey.actions}`;
|
||||
}
|
||||
}
|
||||
|
||||
return sql + ')';
|
||||
return `CREATE TABLE IF NOT EXISTS ${name} (${tableStructureSQL})`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -323,7 +324,7 @@ export class SQLiteDB {
|
|||
async close(): Promise<void> {
|
||||
await this.ready();
|
||||
|
||||
await this.db!.close();
|
||||
await this.db?.close();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -355,7 +356,7 @@ export class SQLiteDB {
|
|||
countItem: string = 'COUNT(\'x\')',
|
||||
): Promise<number> {
|
||||
if (select) {
|
||||
select = 'WHERE ' + select;
|
||||
select = `WHERE ${select}`;
|
||||
}
|
||||
|
||||
return this.countRecordsSql(`SELECT ${countItem} FROM ${table} ${select}`, params);
|
||||
|
@ -470,7 +471,7 @@ export class SQLiteDB {
|
|||
*/
|
||||
async deleteRecordsSelect(table: string, select: string = '', params?: SQLiteDBRecordValue[]): Promise<number> {
|
||||
if (select) {
|
||||
select = 'WHERE ' + select;
|
||||
select = `WHERE ${select}`;
|
||||
}
|
||||
|
||||
const result = await this.execute(`DELETE FROM ${table} ${select}`, params);
|
||||
|
@ -501,7 +502,7 @@ export class SQLiteDB {
|
|||
async execute(sql: string, params?: SQLiteDBRecordValue[]): Promise<any> {
|
||||
await this.ready();
|
||||
|
||||
return this.db!.executeSql(sql, params);
|
||||
return this.db?.executeSql(sql, params);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -516,7 +517,7 @@ export class SQLiteDB {
|
|||
async executeBatch(sqlStatements: (string | string[] | any)[]): Promise<void> {
|
||||
await this.ready();
|
||||
|
||||
await this.db!.sqlBatch(sqlStatements);
|
||||
await this.db?.sqlBatch(sqlStatements);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -529,22 +530,8 @@ export class SQLiteDB {
|
|||
return;
|
||||
}
|
||||
|
||||
// Remove undefined entries and convert null to "NULL".
|
||||
for (const name in data) {
|
||||
if (data[name] === undefined) {
|
||||
delete data[name];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the data to where params.
|
||||
*
|
||||
* @param data Object data.
|
||||
* @returns List of params.
|
||||
*/
|
||||
protected formatDataToSQLParams(data: SQLiteDBRecordValues): SQLiteDBRecordValue[] {
|
||||
return Object.keys(data).map((key) => data[key]!);
|
||||
// Remove undefined entries.
|
||||
Object.keys(data).forEach(key => data[key] === undefined && delete data[key]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -587,7 +574,7 @@ export class SQLiteDB {
|
|||
params?: SQLiteDBRecordValue[],
|
||||
): Promise<SQLiteDBRecordValue> {
|
||||
if (select) {
|
||||
select = 'WHERE ' + select;
|
||||
select = `WHERE ${select}`;
|
||||
}
|
||||
|
||||
return this.getFieldSql(`SELECT ${field} FROM ${table} ${select}`, params);
|
||||
|
@ -648,7 +635,7 @@ export class SQLiteDB {
|
|||
fields: string = '*',
|
||||
): Promise<T> {
|
||||
if (select) {
|
||||
select = ' WHERE ' + select;
|
||||
select = ` WHERE ${select}`;
|
||||
}
|
||||
|
||||
return this.getRecordSql<T>(`SELECT ${fields} FROM ${table} ${select}`, params);
|
||||
|
@ -746,10 +733,10 @@ export class SQLiteDB {
|
|||
limitNum: number = 0,
|
||||
): Promise<T[]> {
|
||||
if (select) {
|
||||
select = ' WHERE ' + select;
|
||||
select = ` WHERE ${select}`;
|
||||
}
|
||||
if (sort) {
|
||||
sort = ' ORDER BY ' + sort;
|
||||
sort = ` ORDER BY ${sort}`;
|
||||
}
|
||||
|
||||
const sql = `SELECT ${fields} FROM ${table} ${select} ${sort}`;
|
||||
|
@ -778,7 +765,7 @@ export class SQLiteDB {
|
|||
if (limits[1] < 1) {
|
||||
limits[1] = Number.MAX_VALUE;
|
||||
}
|
||||
sql += ' LIMIT ' + limits[0] + ', ' + limits[1];
|
||||
sql += ` LIMIT ${limits[0]}, ${limits[1]}`;
|
||||
}
|
||||
|
||||
const result = await this.execute(sql, params);
|
||||
|
@ -807,7 +794,7 @@ export class SQLiteDB {
|
|||
|
||||
return {
|
||||
sql: `INSERT OR REPLACE INTO ${table} (${fields}) VALUES (${questionMarks})`,
|
||||
params: this.formatDataToSQLParams(data),
|
||||
params: Object.values(data),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -897,7 +884,7 @@ export class SQLiteDB {
|
|||
): Promise<void> {
|
||||
try {
|
||||
await this.tableExists(oldTable);
|
||||
} catch (error) {
|
||||
} catch {
|
||||
// Old table does not exist, ignore.
|
||||
return;
|
||||
}
|
||||
|
@ -919,7 +906,7 @@ export class SQLiteDB {
|
|||
|
||||
try {
|
||||
await this.dropTable(oldTable);
|
||||
} catch (error) {
|
||||
} catch {
|
||||
// Error deleting old table, ignore.
|
||||
}
|
||||
}
|
||||
|
@ -958,7 +945,7 @@ export class SQLiteDB {
|
|||
async open(): Promise<void> {
|
||||
await this.ready();
|
||||
|
||||
await this.db!.open();
|
||||
await this.db?.open();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1066,7 +1053,7 @@ export class SQLiteDB {
|
|||
}
|
||||
|
||||
// Create the list of params using the "data" object and the params for the where clause.
|
||||
let params = this.formatDataToSQLParams(data);
|
||||
let params = Object.values(data);
|
||||
if (where && whereParams) {
|
||||
params = params.concat(whereParams);
|
||||
}
|
||||
|
@ -1090,19 +1077,19 @@ export class SQLiteDB {
|
|||
};
|
||||
}
|
||||
|
||||
const where: string[] = [];
|
||||
const params: SQLiteDBRecordValue[] = [];
|
||||
|
||||
for (const key in conditions) {
|
||||
const value = conditions[key];
|
||||
const where = Object.keys(conditions).map((field) => {
|
||||
const value = conditions[field];
|
||||
|
||||
if (value === undefined || value === null) {
|
||||
where.push(key + ' IS NULL');
|
||||
} else {
|
||||
where.push(key + ' = ?');
|
||||
params.push(value);
|
||||
return `${field} IS NULL`;
|
||||
}
|
||||
}
|
||||
|
||||
params.push(value);
|
||||
|
||||
return `${field} = ?`;
|
||||
});
|
||||
|
||||
return {
|
||||
sql: where.join(' AND '),
|
||||
|
@ -1130,7 +1117,7 @@ export class SQLiteDB {
|
|||
|
||||
values.forEach((value) => {
|
||||
if (value === undefined || value === null) {
|
||||
sql = field + ' IS NULL';
|
||||
sql = `${field} IS NULL`;
|
||||
} else {
|
||||
params.push(value);
|
||||
}
|
||||
|
@ -1138,14 +1125,14 @@ export class SQLiteDB {
|
|||
|
||||
if (params && params.length) {
|
||||
if (sql !== '') {
|
||||
sql = sql + ' OR ';
|
||||
sql += ' OR ';
|
||||
}
|
||||
|
||||
if (params.length == 1) {
|
||||
sql = sql + field + ' = ?';
|
||||
sql += `${field} = ?`;
|
||||
} else {
|
||||
const questionMarks = ',?'.repeat(params.length).substring(1);
|
||||
sql = sql + field + ' IN (' + questionMarks + ')';
|
||||
sql += ` ${field} IN (${questionMarks})`;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1232,7 +1219,7 @@ export class SQLiteDB {
|
|||
}
|
||||
|
||||
export type SQLiteDBRecordValues = {
|
||||
[key: string]: SQLiteDBRecordValue | undefined | null;
|
||||
[key: string]: SQLiteDBRecordValue;
|
||||
};
|
||||
|
||||
export type SQLiteDBQueryParams = {
|
||||
|
@ -1240,4 +1227,4 @@ export type SQLiteDBQueryParams = {
|
|||
params: SQLiteDBRecordValue[];
|
||||
};
|
||||
|
||||
export type SQLiteDBRecordValue = number | string;
|
||||
export type SQLiteDBRecordValue = number | string | undefined | null;
|
||||
|
|
|
@ -24,7 +24,7 @@ describe('CoreError', () => {
|
|||
// Arrange
|
||||
const message = Faker.lorem.sentence();
|
||||
|
||||
let error: CoreError | null = null;
|
||||
let error: CoreError;
|
||||
|
||||
// Act
|
||||
try {
|
||||
|
@ -37,10 +37,10 @@ describe('CoreError', () => {
|
|||
expect(error).not.toBeNull();
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
expect(error).toBeInstanceOf(CoreError);
|
||||
expect(error!.name).toEqual('CoreError');
|
||||
expect(error!.message).toEqual(message);
|
||||
expect(error!.stack).not.toBeNull();
|
||||
expect(error!.stack).toContain(agnosticPath('src/core/classes/tests/error.test.ts'));
|
||||
expect(error.name).toEqual('CoreError');
|
||||
expect(error.message).toEqual(message);
|
||||
expect(error.stack).not.toBeNull();
|
||||
expect(error.stack).toContain(agnosticPath('src/core/classes/tests/error.test.ts'));
|
||||
});
|
||||
|
||||
it('can be subclassed', () => {
|
||||
|
@ -55,7 +55,7 @@ describe('CoreError', () => {
|
|||
|
||||
const message = Faker.lorem.sentence();
|
||||
|
||||
let error: CustomCoreError | null = null;
|
||||
let error: CustomCoreError;
|
||||
|
||||
// Act
|
||||
try {
|
||||
|
@ -69,10 +69,10 @@ describe('CoreError', () => {
|
|||
expect(error).toBeInstanceOf(Error);
|
||||
expect(error).toBeInstanceOf(CoreError);
|
||||
expect(error).toBeInstanceOf(CustomCoreError);
|
||||
expect(error!.name).toEqual('CustomCoreError');
|
||||
expect(error!.message).toEqual(`Custom message: ${message}`);
|
||||
expect(error!.stack).not.toBeNull();
|
||||
expect(error!.stack).toContain(agnosticPath('src/core/classes/tests/error.test.ts'));
|
||||
expect(error.name).toEqual('CustomCoreError');
|
||||
expect(error.message).toEqual(`Custom message: ${message}`);
|
||||
expect(error.stack).not.toBeNull();
|
||||
expect(error.stack).toContain(agnosticPath('src/core/classes/tests/error.test.ts'));
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -46,7 +46,7 @@ import { CoreUtils } from '@services/utils/utils';
|
|||
})
|
||||
export class CoreAttachmentsComponent implements OnInit {
|
||||
|
||||
@Input() files?: CoreFileEntry[]; // List of attachments. New attachments will be added to this array.
|
||||
@Input() files: CoreFileEntry[] = []; // List of attachments. New attachments will be added to this array.
|
||||
@Input() maxSize?: number; // Max size. -1 means unlimited, 0 means course/user max size, not defined means unknown.
|
||||
@Input() maxSubmissions?: number; // Max number of attachments. -1 means unlimited, not defined means unknown limit.
|
||||
@Input() component?: string; // Component the downloaded files will be linked to.
|
||||
|
@ -177,7 +177,7 @@ export class CoreAttachmentsComponent implements OnInit {
|
|||
* @param data The data received.
|
||||
*/
|
||||
renamed(index: number, data: { file: FileEntry }): void {
|
||||
this.files![index] = data.file;
|
||||
this.files[index] = data.file;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -71,11 +71,11 @@ export class CoreChartComponent implements OnDestroy, OnInit, OnChanges {
|
|||
generateLabels: (chart: Chart): ChartLegendLabelItem[] => {
|
||||
const data = chart.data;
|
||||
if (data.labels?.length) {
|
||||
const datasets = data.datasets![0];
|
||||
const datasets = data.datasets?.[0];
|
||||
|
||||
return data.labels.map((label, i) => ({
|
||||
text: label + ': ' + datasets.data![i],
|
||||
fillStyle: datasets.backgroundColor![i],
|
||||
return data.labels.map<ChartLegendLabelItem>((label, i) => ({
|
||||
text: label + ': ' + datasets?.data?.[i],
|
||||
fillStyle: datasets?.backgroundColor?.[i],
|
||||
}));
|
||||
}
|
||||
|
||||
|
@ -87,14 +87,18 @@ export class CoreChartComponent implements OnDestroy, OnInit, OnChanges {
|
|||
legend = Object.assign({}, this.legend);
|
||||
}
|
||||
|
||||
if (this.type == 'bar' && this.data.length >= 5) {
|
||||
if (this.type === 'bar' && this.data.length >= 5) {
|
||||
this.type = 'horizontalBar';
|
||||
}
|
||||
|
||||
// Format labels if needed.
|
||||
await this.formatLabels();
|
||||
|
||||
const context = this.canvas!.nativeElement.getContext('2d')!;
|
||||
const context = this.canvas?.nativeElement.getContext('2d');
|
||||
if (!context) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.chart = new Chart(context, {
|
||||
type: this.type,
|
||||
data: {
|
||||
|
@ -123,7 +127,11 @@ export class CoreChartComponent implements OnDestroy, OnInit, OnChanges {
|
|||
await this.formatLabels();
|
||||
}
|
||||
|
||||
this.chart.data.datasets![0] = {
|
||||
if (!this.chart.data.datasets) {
|
||||
this.chart.data.datasets = [];
|
||||
}
|
||||
|
||||
this.chart.data.datasets[0] = {
|
||||
data: this.data,
|
||||
backgroundColor: this.getRandomColors(this.data.length),
|
||||
};
|
||||
|
|
|
@ -33,7 +33,6 @@ import { CoreDynamicComponent } from './dynamic-component/dynamic-component';
|
|||
import { CoreEmptyBoxComponent } from './empty-box/empty-box';
|
||||
import { CoreFileComponent } from './file/file';
|
||||
import { CoreFilesComponent } from './files/files';
|
||||
import { CoreIconComponent } from './icon/icon';
|
||||
import { CoreIframeComponent } from './iframe/iframe';
|
||||
import { CoreInfiniteLoadingComponent } from './infinite-loading/infinite-loading';
|
||||
import { CoreInputErrorsComponent } from './input-errors/input-errors';
|
||||
|
@ -82,7 +81,6 @@ import { CoreSheetModalComponent } from '@components/sheet-modal/sheet-modal';
|
|||
CoreFileComponent,
|
||||
CoreFilesComponent,
|
||||
CoreGroupSelectorComponent,
|
||||
CoreIconComponent,
|
||||
CoreIframeComponent,
|
||||
CoreInfiniteLoadingComponent,
|
||||
CoreInputErrorsComponent,
|
||||
|
@ -136,7 +134,6 @@ import { CoreSheetModalComponent } from '@components/sheet-modal/sheet-modal';
|
|||
CoreFileComponent,
|
||||
CoreFilesComponent,
|
||||
CoreGroupSelectorComponent,
|
||||
CoreIconComponent,
|
||||
CoreIframeComponent,
|
||||
CoreInfiniteLoadingComponent,
|
||||
CoreInputErrorsComponent,
|
||||
|
|
|
@ -149,24 +149,30 @@ export class CoreFileComponent implements OnInit, OnDestroy {
|
|||
* @param isOpenButton Whether the open button was clicked.
|
||||
* @returns Promise resolved when file is opened.
|
||||
*/
|
||||
openFile(ev?: Event, isOpenButton = false): Promise<void> {
|
||||
async openFile(ev?: Event, isOpenButton = false): Promise<void> {
|
||||
ev?.preventDefault();
|
||||
ev?.stopPropagation();
|
||||
|
||||
if (!this.file) {
|
||||
return;
|
||||
}
|
||||
|
||||
const options: CoreUtilsOpenFileOptions = {};
|
||||
if (isOpenButton) {
|
||||
// Use the non-default method.
|
||||
options.iOSOpenFileAction = this.defaultIsOpenWithPicker ? OpenFileAction.OPEN : OpenFileAction.OPEN_WITH;
|
||||
}
|
||||
|
||||
return CoreFileHelper.downloadAndOpenFile(this.file!, this.component, this.componentId, this.state, (event) => {
|
||||
if (event && 'calculating' in event && event.calculating) {
|
||||
// The process is calculating some data required for the download, show the spinner.
|
||||
this.isDownloading = true;
|
||||
}
|
||||
}, undefined, options).catch((error) => {
|
||||
try {
|
||||
return await CoreFileHelper.downloadAndOpenFile(this.file, this.component, this.componentId, this.state, (event) => {
|
||||
if (event && 'calculating' in event && event.calculating) {
|
||||
// The process is calculating some data required for the download, show the spinner.
|
||||
this.isDownloading = true;
|
||||
}
|
||||
}, undefined, options);
|
||||
} catch (error) {
|
||||
CoreDomUtils.showErrorModalDefault(error, 'core.errordownloading', true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -264,7 +270,7 @@ export class CoreFileComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
/**
|
||||
* Component destroyed.
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.observer?.off();
|
||||
|
|
|
@ -30,7 +30,7 @@ import { CoreUtils } from '@services/utils/utils';
|
|||
})
|
||||
export class CoreFilesComponent implements OnInit, DoCheck {
|
||||
|
||||
@Input() files?: CoreFileEntry[]; // List of files.
|
||||
@Input() files: CoreFileEntry[] = []; // List of files.
|
||||
@Input() component?: string; // Component the downloaded files will be linked to.
|
||||
@Input() componentId?: string | number; // Component ID.
|
||||
@Input() alwaysDownload?: boolean | string; // Whether it should always display the refresh button when the file is downloaded.
|
||||
|
@ -75,7 +75,7 @@ export class CoreFilesComponent implements OnInit, DoCheck {
|
|||
* Calculate contentText based on fils that can be rendered inline.
|
||||
*/
|
||||
protected renderInlineFiles(): void {
|
||||
this.contentText = this.files!.reduce((previous, file) => {
|
||||
this.contentText = this.files.reduce((previous, file) => {
|
||||
const text = CoreMimetypeUtils.getEmbeddedHtml(file);
|
||||
|
||||
return text ? previous + '<br>' + text : previous;
|
||||
|
|
|
@ -1,128 +0,0 @@
|
|||
:host {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
:host-context([dir=rtl]).icon-flip-rtl {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
:host-context(ion-item.md) ion-icon {
|
||||
&[slot] {
|
||||
color: rgba(var(--ion-text-color-rgb, 0, 0, 0), 0.54);
|
||||
font-size: 24px;
|
||||
margin-top: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
&[slot=start] {
|
||||
margin-right: 32px;
|
||||
}
|
||||
&[slot=end] {
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
@supports (margin-inline-start: 0) or (-webkit-margin-start: 0) {
|
||||
&[slot=start] {
|
||||
margin-right: unset;
|
||||
-webkit-margin-end: 32px;
|
||||
margin-inline-end: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
@supports (margin-inline-start: 0) or (-webkit-margin-start: 0) {
|
||||
&[slot=end] {
|
||||
margin-left: unset;
|
||||
-webkit-margin-start: 16px;
|
||||
margin-inline-start: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(ion-item.ios) ion-icon {
|
||||
&[slot] {
|
||||
font-size: 1.6em;
|
||||
}
|
||||
&[slot=start] {
|
||||
margin-top: 7px;
|
||||
margin-bottom: 7px;
|
||||
margin-left: 0;
|
||||
margin-right: 20px;
|
||||
}
|
||||
&[slot=end] {
|
||||
margin-top: 7px;
|
||||
margin-bottom: 7px;
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
@supports (margin-inline-start: 0) or (-webkit-margin-start: 0) {
|
||||
&[slot=start] {
|
||||
margin-left: unset;
|
||||
margin-right: unset;
|
||||
-webkit-margin-start: 0;
|
||||
margin-inline-start: 0;
|
||||
-webkit-margin-end: 20px;
|
||||
margin-inline-end: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@supports (margin-inline-start: 0) or (-webkit-margin-start: 0) {
|
||||
&[slot=end] {
|
||||
margin-left: unset;
|
||||
margin-right: unset;
|
||||
-webkit-margin-start: 10px;
|
||||
margin-inline-start: 10px;
|
||||
-webkit-margin-end: 10px;
|
||||
margin-inline-end: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(ion-item.ion-color) {
|
||||
color: var(--ion-color-contrast);
|
||||
}
|
||||
|
||||
:host-context(ion-button.md) ion-icon,
|
||||
:host-context(ion-button.ios) ion-icon {
|
||||
&[slot] {
|
||||
font-size: 1.4em;
|
||||
pointer-events: none;
|
||||
|
||||
}
|
||||
&[slot=start] {
|
||||
margin-left: -0.3em;
|
||||
margin-right: 0.3em;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
&[slot=end] {
|
||||
margin-left: 0.3em;
|
||||
margin-right: -0.2em;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
&[slot=icon-only] {
|
||||
font-size: 1.8em;
|
||||
}
|
||||
|
||||
@supports (margin-inline-start: 0) or (-webkit-margin-start: 0) {
|
||||
&[slot=start] {
|
||||
margin-left: unset;
|
||||
margin-right: unset;
|
||||
-webkit-margin-start: -0.3em;
|
||||
margin-inline-start: -0.3em;
|
||||
-webkit-margin-end: 0.3em;
|
||||
margin-inline-end: 0.3em;
|
||||
}
|
||||
}
|
||||
|
||||
@supports (margin-inline-start: 0) or (-webkit-margin-start: 0) {
|
||||
&[slot=end] {
|
||||
margin-left: unset;
|
||||
margin-right: unset;
|
||||
-webkit-margin-start: 0.3em;
|
||||
margin-inline-start: 0.3em;
|
||||
-webkit-margin-end: -0.2em;
|
||||
margin-inline-end: -0.2em;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,116 +0,0 @@
|
|||
// (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, OnChanges, ElementRef, SimpleChange } from '@angular/core';
|
||||
import { CoreLogger } from '@singletons/logger';
|
||||
|
||||
/**
|
||||
* Core Icon is a component that enables the posibility to add fontawesome icon to the html. It
|
||||
* To use fontawesome just place the full icon name with the fa- prefix and
|
||||
* the component will detect it.
|
||||
*
|
||||
* Check available icons at https://fontawesome.com/icons?d=gallery&m=free
|
||||
*
|
||||
* @deprecated since 3.9.3. Please use <ion-icon name="fas-icon"> instead.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'core-icon',
|
||||
template: '<ion-icon [name]="name"><ng-content></ng-content></ion-icon>',
|
||||
styleUrls: ['icon.scss'],
|
||||
})
|
||||
export class CoreIconComponent implements OnChanges {
|
||||
|
||||
// Common params.
|
||||
@Input() name = '';
|
||||
@Input() color?: string;
|
||||
@Input() slash?: boolean; // Display a red slash over the icon.
|
||||
|
||||
// FontAwesome params.
|
||||
@Input('fixed-width') fixedWidth?: boolean; // eslint-disable-line @angular-eslint/no-input-rename
|
||||
|
||||
@Input() label?: string;
|
||||
@Input() flipRtl?: boolean; // Whether to flip the icon in RTL. Defaults to false.
|
||||
|
||||
protected element: HTMLElement;
|
||||
|
||||
constructor(
|
||||
el: ElementRef,
|
||||
) {
|
||||
this.element = el.nativeElement;
|
||||
|
||||
CoreLogger.getInstance('CoreIconComponent').error('CoreIconComponent is deprecated. Please use ion-icon instead.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect changes on input properties.
|
||||
*/
|
||||
ngOnChanges(changes: {[name: string]: SimpleChange}): void {
|
||||
if (!changes.name || !this.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
this.updateIcon(this.element.children[0]);
|
||||
});
|
||||
}
|
||||
|
||||
protected updateIcon(iconElement: Element): void {
|
||||
!this.label && iconElement.setAttribute('aria-hidden', 'true');
|
||||
!this.label && iconElement.setAttribute('role', 'presentation');
|
||||
this.label && iconElement.setAttribute('aria-label', this.label);
|
||||
this.label && iconElement.setAttribute('title', this.label);
|
||||
|
||||
const attrs = this.element.attributes;
|
||||
for (let i = attrs.length - 1; i >= 0; i--) {
|
||||
if (attrs[i].name != 'name') {
|
||||
iconElement.setAttribute(attrs[i].name, attrs[i].value);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.isTrueProperty(this.slash)) {
|
||||
iconElement.classList.add('icon-slash');
|
||||
} else {
|
||||
iconElement.classList.remove('icon-slash');
|
||||
}
|
||||
|
||||
if (this.isTrueProperty(this.flipRtl)) {
|
||||
iconElement.classList.add('icon-flip-rtl');
|
||||
} else {
|
||||
iconElement.classList.remove('icon-flip-rtl');
|
||||
}
|
||||
|
||||
if (this.isTrueProperty(this.fixedWidth)) {
|
||||
iconElement.classList.add('fa-fw');
|
||||
} else {
|
||||
iconElement.classList.remove('fa-fw');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the value is true or on.
|
||||
*
|
||||
* @param val Value to be checked.
|
||||
* @returns If has a value equivalent to true.
|
||||
*/
|
||||
isTrueProperty(val: unknown): boolean {
|
||||
if (typeof val === 'string') {
|
||||
val = val.toLowerCase().trim();
|
||||
|
||||
return (val === 'true' || val === 'on' || val === '');
|
||||
}
|
||||
|
||||
return !!val;
|
||||
}
|
||||
|
||||
}
|
|
@ -28,10 +28,10 @@ import { CorePath } from '@singletons/path';
|
|||
})
|
||||
export class CoreRecaptchaComponent implements OnInit {
|
||||
|
||||
@Input() model?: Record<string, string>; // The model where to store the recaptcha response.
|
||||
@Input() model: Record<string, string> = {}; // The model where to store the recaptcha response.
|
||||
@Input() publicKey?: string; // The site public key.
|
||||
@Input() modelValueName = 'recaptcharesponse'; // Name of the model property where to store the response.
|
||||
@Input() siteUrl?: string; // The site URL. If not defined, current site.
|
||||
@Input() siteUrl = ''; // The site URL. If not defined, current site.
|
||||
|
||||
expired = false;
|
||||
|
||||
|
@ -45,7 +45,7 @@ export class CoreRecaptchaComponent implements OnInit {
|
|||
* @inheritdoc
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.siteUrl = this.siteUrl || CoreSites.getCurrentSite()?.getURL();
|
||||
this.siteUrl = this.siteUrl || CoreSites.getRequiredCurrentSite().getURL();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -62,7 +62,7 @@ export class CoreRecaptchaComponent implements OnInit {
|
|||
// Open the recaptcha challenge in an InAppBrowser.
|
||||
// The app used to use an iframe for this, but the app can no longer access the iframe to create the required callbacks.
|
||||
// The app cannot render the recaptcha directly because it has problems with the local protocols and domains.
|
||||
const src = CorePath.concatenatePaths(this.siteUrl!, 'webservice/recaptcha.php?lang=' + this.lang);
|
||||
const src = CorePath.concatenatePaths(this.siteUrl, 'webservice/recaptcha.php?lang=' + this.lang);
|
||||
|
||||
const inAppBrowserWindow = CoreUtils.openInApp(src);
|
||||
if (!inAppBrowserWindow) {
|
||||
|
@ -90,7 +90,7 @@ export class CoreRecaptchaComponent implements OnInit {
|
|||
this.expireRecaptchaAnswer();
|
||||
} else if (event.data.action == 'callback') {
|
||||
this.expired = false;
|
||||
this.model![this.modelValueName] = event.data.value;
|
||||
this.model[this.modelValueName] = event.data.value;
|
||||
|
||||
// Close the InAppBrowser now.
|
||||
inAppBrowserWindow.close();
|
||||
|
@ -105,7 +105,7 @@ export class CoreRecaptchaComponent implements OnInit {
|
|||
*/
|
||||
expireRecaptchaAnswer(): void {
|
||||
this.expired = true;
|
||||
this.model![this.modelValueName] = '';
|
||||
this.model[this.modelValueName] = '';
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,36 +0,0 @@
|
|||
// (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 { CoreIconComponent } from '@components/icon/icon';
|
||||
|
||||
import { renderWrapperComponent } from '@/testing/utils';
|
||||
|
||||
describe('CoreIconComponent', () => {
|
||||
|
||||
it('should render', async () => {
|
||||
// Act
|
||||
const fixture = await renderWrapperComponent(CoreIconComponent, 'core-icon', { name: 'fa-thumbs-up' });
|
||||
|
||||
// Assert
|
||||
expect(fixture.nativeElement.innerHTML.trim()).not.toHaveLength(0);
|
||||
|
||||
const icon = fixture.nativeElement.querySelector('ion-icon');
|
||||
const name = icon?.getAttribute('name') || icon?.getAttribute('ng-reflect-name') || '';
|
||||
|
||||
expect(icon).not.toBeNull();
|
||||
expect(name).toEqual('fa-thumbs-up');
|
||||
expect(icon?.getAttribute('role')).toEqual('presentation');
|
||||
});
|
||||
|
||||
});
|
|
@ -54,7 +54,7 @@ export class CoreFaIconDirective implements AfterViewInit, OnChanges {
|
|||
let iconName = this.name;
|
||||
let font = 'ionicons';
|
||||
const parts = iconName.split('-', 2);
|
||||
if (parts.length == 2) {
|
||||
if (parts.length === 2) {
|
||||
switch (parts[0]) {
|
||||
case 'far':
|
||||
library = 'regular';
|
||||
|
@ -82,7 +82,7 @@ export class CoreFaIconDirective implements AfterViewInit, OnChanges {
|
|||
}
|
||||
}
|
||||
|
||||
if (font == 'ionicons') {
|
||||
if (font === 'ionicons') {
|
||||
this.element.removeAttribute('src');
|
||||
this.logger.warn(`Ionic icon ${this.name} detected`);
|
||||
|
||||
|
@ -103,7 +103,7 @@ export class CoreFaIconDirective implements AfterViewInit, OnChanges {
|
|||
ngAfterViewInit(): void {
|
||||
if (!this.element.getAttribute('aria-label') &&
|
||||
!this.element.getAttribute('aria-labelledby') &&
|
||||
this.element.getAttribute('aria-hidden') != 'true') {
|
||||
this.element.getAttribute('aria-hidden') !== 'true') {
|
||||
this.logger.warn('Aria label not set on icon ' + this.name, this.element);
|
||||
|
||||
this.element.setAttribute('aria-hidden', 'true');
|
||||
|
@ -111,7 +111,7 @@ export class CoreFaIconDirective implements AfterViewInit, OnChanges {
|
|||
}
|
||||
|
||||
/**
|
||||
* Detect changes on input properties.
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnChanges(changes: { [name: string]: SimpleChange }): void {
|
||||
if (!changes.name || !this.name) {
|
||||
|
|
|
@ -36,19 +36,23 @@ export class CoreBlockOnlyTitleComponent extends CoreBlockBaseComponent implemen
|
|||
async ngOnInit(): Promise<void> {
|
||||
await super.ngOnInit();
|
||||
|
||||
this.fetchContentDefaultError = 'Error getting ' + this.block.contents?.title + ' data.';
|
||||
this.fetchContentDefaultError = `Error getting ${this.block.contents?.title} data.`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Go to the block page.
|
||||
*/
|
||||
gotoBlock(): void {
|
||||
if (!this.link) {
|
||||
return;
|
||||
}
|
||||
|
||||
const navOptions = this.navOptions || {};
|
||||
if (this.linkParams) {
|
||||
navOptions.params = this.linkParams;
|
||||
}
|
||||
|
||||
CoreNavigator.navigateToSitePath(this.link!, navOptions);
|
||||
CoreNavigator.navigateToSitePath(this.link, navOptions);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -119,7 +119,7 @@ export class CoreCommentsViewerPage implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
/**
|
||||
* View loaded.
|
||||
* @inheritdoc
|
||||
*/
|
||||
async ngOnInit(): Promise<void> {
|
||||
try {
|
||||
|
@ -445,9 +445,13 @@ export class CoreCommentsViewerPage implements OnInit, OnDestroy {
|
|||
* @returns Promise resolved with modified comment when done.
|
||||
*/
|
||||
protected async loadCommentProfile(comment: CoreCommentsDataToDisplay): Promise<CoreCommentsDataToDisplay> {
|
||||
// Get the user profile image.
|
||||
if (!comment.userid) {
|
||||
return comment;
|
||||
}
|
||||
|
||||
try {
|
||||
const user = await CoreUser.getProfile(comment.userid!, undefined, true);
|
||||
// Get the user profile image.
|
||||
const user = await CoreUser.getProfile(comment.userid, undefined, true);
|
||||
comment.profileimageurl = user.profileimageurl;
|
||||
comment.fullname = user.fullname;
|
||||
} catch {
|
||||
|
@ -599,7 +603,7 @@ export class CoreCommentsViewerPage implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
/**
|
||||
* Page destroyed.
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.syncObserver?.off();
|
||||
|
|
|
@ -23,6 +23,7 @@ import { CoreNetwork } from '@services/network';
|
|||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { CoreNetworkError } from '@classes/errors/network-error';
|
||||
import { CoreCommentsDBRecord, CoreCommentsDeletedDBRecord } from './database/comments';
|
||||
import { CoreSyncResult } from '@services/sync';
|
||||
|
||||
/**
|
||||
* Service to sync omments.
|
||||
|
@ -318,10 +319,7 @@ export class CoreCommentsSyncProvider extends CoreSyncBaseProvider<CoreCommentsS
|
|||
}
|
||||
export const CoreCommentsSync = makeSingleton(CoreCommentsSyncProvider);
|
||||
|
||||
export type CoreCommentsSyncResult = {
|
||||
warnings: string[]; // List of warnings.
|
||||
updated: boolean; // Whether some data was sent to the server or offline data was updated.
|
||||
};
|
||||
export type CoreCommentsSyncResult = CoreSyncResult;
|
||||
|
||||
/**
|
||||
* Data passed to AUTO_SYNCED event.
|
||||
|
|
|
@ -152,7 +152,7 @@ export class CoreCommentsProvider {
|
|||
this.invalidateCommentsData(contextLevel, instanceId, component, itemId, area, siteId),
|
||||
);
|
||||
|
||||
return commentsResponse![0];
|
||||
return commentsResponse[0];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -166,9 +166,9 @@ export class CoreCommentsProvider {
|
|||
async addCommentsOnline(
|
||||
comments: CoreCommentsCommentBasicData[],
|
||||
siteId?: string,
|
||||
): Promise<CoreCommentsAddCommentsWSResponse | undefined> {
|
||||
): Promise<CoreCommentsAddCommentsWSResponse> {
|
||||
if (!comments || !comments.length) {
|
||||
return;
|
||||
return [];
|
||||
}
|
||||
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
|
@ -231,8 +231,12 @@ export class CoreCommentsProvider {
|
|||
|
||||
// Convenience function to store the action to be synchronized later.
|
||||
const storeOffline = async (): Promise<boolean> => {
|
||||
if (!comment.id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await CoreCommentsOffline.deleteComment(
|
||||
comment.id!,
|
||||
comment.id,
|
||||
comment.contextlevel,
|
||||
comment.instanceid,
|
||||
comment.component,
|
||||
|
|
|
@ -67,7 +67,7 @@ export class CoreContentLinksModuleGradeHandler extends CoreContentLinksHandlerB
|
|||
courseId?: number,
|
||||
): CoreContentLinksAction[] | Promise<CoreContentLinksAction[]> {
|
||||
|
||||
courseId = Number(courseId || params.courseid || params.cid);
|
||||
const courseIdentifier = Number(courseId || params.courseid || params.cid);
|
||||
|
||||
return [{
|
||||
action: async (siteId): Promise<void> => {
|
||||
|
@ -79,14 +79,14 @@ export class CoreContentLinksModuleGradeHandler extends CoreContentLinksHandlerB
|
|||
CoreCourseHelper.navigateToModule(
|
||||
Number(params.id),
|
||||
{
|
||||
courseId,
|
||||
courseId: courseIdentifier,
|
||||
modName: this.useModNameToGetModule ? this.modName : undefined,
|
||||
siteId,
|
||||
},
|
||||
);
|
||||
} else if (this.canReview) {
|
||||
// Use the goToReview function.
|
||||
this.goToReview(url, params, courseId!, siteId);
|
||||
this.goToReview(url, params, courseIdentifier, siteId);
|
||||
} else {
|
||||
// Not current user and cannot review it in the app, open it in browser.
|
||||
site.openInBrowserWithAutoLogin(url);
|
||||
|
|
|
@ -20,9 +20,9 @@ import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
|||
import { CoreCourse } from '../services/course';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreWSExternalWarning } from '@services/ws';
|
||||
import { CoreCourseContentsPage } from '../pages/contents/contents';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreSyncResult } from '@services/sync';
|
||||
|
||||
/**
|
||||
* Template class to easily create CoreCourseModuleMainComponent of activities.
|
||||
|
@ -188,32 +188,34 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR
|
|||
*
|
||||
* @returns Promise resolved when done.
|
||||
*/
|
||||
protected async sync(): Promise<unknown> {
|
||||
return {};
|
||||
protected async sync(): Promise<CoreSyncResult> {
|
||||
return {
|
||||
updated: false,
|
||||
warnings: [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if sync has succeed from result sync data.
|
||||
* Checks if sync has updated data on the server.
|
||||
*
|
||||
* @param result Data returned on the sync function.
|
||||
* @returns If suceed or not.
|
||||
* @returns If data has been updated or not.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
protected hasSyncSucceed(result: unknown): boolean {
|
||||
return true;
|
||||
protected hasSyncSucceed(result: CoreSyncResult): boolean {
|
||||
return result.updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to synchronize the activity.
|
||||
*
|
||||
* @param showErrors If show errors to the user of hide them.
|
||||
* @returns Promise resolved with true if sync succeed, or false if failed.
|
||||
* @returns Promise resolved with true if sync hast updated data to the server, false otherwise.
|
||||
*/
|
||||
protected async syncActivity(showErrors: boolean = false): Promise<boolean> {
|
||||
try {
|
||||
const result = <{warnings?: CoreWSExternalWarning[]}> await this.sync();
|
||||
const result = await this.sync();
|
||||
|
||||
if (result?.warnings?.length) {
|
||||
if (result.warnings.length) {
|
||||
CoreDomUtils.showErrorModal(result.warnings[0]);
|
||||
}
|
||||
|
||||
|
|
|
@ -76,6 +76,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
|||
@Input() initialSectionNumber?: number; // The section to load first (by number).
|
||||
@Input() moduleId?: number; // The module ID to scroll to. Must be inside the initial selected section.
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@ViewChildren(CoreDynamicComponent) dynamicComponents?: QueryList<CoreDynamicComponent<any>>;
|
||||
|
||||
// All the possible component classes.
|
||||
|
|
|
@ -58,16 +58,19 @@ export class CoreCourseDownloadModuleMainFileDirective implements OnInit {
|
|||
ev.stopPropagation();
|
||||
|
||||
const modal = await CoreDomUtils.showModalLoading();
|
||||
const courseId = typeof this.courseId == 'string' ? parseInt(this.courseId, 10) : this.courseId;
|
||||
const courseId = this.courseId ? Number(this.courseId) : undefined;
|
||||
|
||||
try {
|
||||
if (!this.module) {
|
||||
// Try to get the module from cache.
|
||||
this.moduleId = typeof this.moduleId == 'string' ? parseInt(this.moduleId, 10) : this.moduleId;
|
||||
this.module = await CoreCourse.getModule(this.moduleId!, courseId);
|
||||
const moduleId = Number(this.moduleId);
|
||||
if (!moduleId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.module = await CoreCourse.getModule(moduleId, courseId);
|
||||
}
|
||||
|
||||
const componentId = this.componentId || module.id;
|
||||
const componentId = this.componentId ? Number(this.componentId) : this.module.id;
|
||||
|
||||
await CoreCourseHelper.downloadModuleAndOpenFile(
|
||||
this.module,
|
||||
|
|
|
@ -448,7 +448,11 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate<CoreCourseOpt
|
|||
? (handler as CoreCourseOptionsMenuHandler).getMenuDisplayData
|
||||
: (handler as CoreCourseOptionsHandler).getDisplayData;
|
||||
|
||||
promises.push(Promise.resolve(getFunction!.call(handler, courseWithOptions)).then((data) => {
|
||||
if (!getFunction) {
|
||||
return;
|
||||
}
|
||||
|
||||
promises.push(Promise.resolve(getFunction.call(handler, courseWithOptions)).then((data) => {
|
||||
handlersToDisplay.push({
|
||||
data: data,
|
||||
priority: handler.priority || 0,
|
||||
|
@ -468,7 +472,7 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate<CoreCourseOpt
|
|||
handlersToDisplay.sort((
|
||||
a: CoreCourseOptionsHandlerToDisplay | CoreCourseOptionsMenuHandlerToDisplay,
|
||||
b: CoreCourseOptionsHandlerToDisplay | CoreCourseOptionsMenuHandlerToDisplay,
|
||||
) => b.priority! - a.priority!);
|
||||
) => (b.priority || 0) - (a.priority || 0));
|
||||
|
||||
return handlersToDisplay;
|
||||
}
|
||||
|
|
|
@ -1274,9 +1274,9 @@ export class CoreCourseProvider {
|
|||
if (!result.status) {
|
||||
if (result.warnings && result.warnings.length) {
|
||||
throw new CoreWSError(result.warnings[0]);
|
||||
} else {
|
||||
throw new CoreError('Cannot change completion.');
|
||||
}
|
||||
|
||||
throw new CoreError('Cannot change completion.');
|
||||
}
|
||||
|
||||
return result;
|
||||
|
|
|
@ -141,8 +141,12 @@ ion-card.core-course-list-item {
|
|||
|
||||
// Card layout.
|
||||
ion-card.core-course-list-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
&::part(native) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
height: calc(100% - var(--card-vertical-margin) - var(--card-vertical-margin));
|
||||
margin-top: var(--card-vertical-margin);
|
||||
|
|
|
@ -119,24 +119,20 @@ export class CoreEmulatorCaptureHelperProvider {
|
|||
|
||||
if (mimetypes?.length) {
|
||||
// Search for a supported mimetype.
|
||||
for (let i = 0; i < mimetypes.length; i++) {
|
||||
const mimetype = mimetypes[i];
|
||||
result.mimetype = mimetypes.find((mimetype) => {
|
||||
const matches = mimetype.match(new RegExp('^' + type + '/'));
|
||||
|
||||
if (matches?.length && window.MediaRecorder.isTypeSupported(mimetype)) {
|
||||
result.mimetype = mimetype;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return matches?.length && window.MediaRecorder.isTypeSupported(mimetype);
|
||||
});
|
||||
}
|
||||
|
||||
if (result.mimetype) {
|
||||
// Found a supported mimetype in the mimetypes array, get the extension.
|
||||
result.extension = CoreMimetypeUtils.getExtension(result.mimetype);
|
||||
} else if (type == 'video') {
|
||||
} else if (type === 'video' && this.videoMimeType) {
|
||||
// No mimetype found, use default extension.
|
||||
result.mimetype = this.videoMimeType;
|
||||
result.extension = this.possibleVideoMimeTypes[result.mimetype!];
|
||||
result.extension = this.possibleVideoMimeTypes[result.mimetype];
|
||||
}
|
||||
|
||||
return result;
|
||||
|
|
|
@ -76,7 +76,9 @@ export class FileTransferObjectMock extends FileTransferObject {
|
|||
abort(): void {
|
||||
if (this.xhr) {
|
||||
this.xhr.abort();
|
||||
this.reject!(new FileTransferErrorMock(FileTransferErrorMock.ABORT_ERR, this.source!, this.target!, 0, '', ''));
|
||||
this.reject?.(
|
||||
new FileTransferErrorMock(FileTransferErrorMock.ABORT_ERR, this.source || '', this.target || '', 0, '', ''),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -189,7 +189,7 @@ export class CoreFileUploaderDelegateService extends CoreDelegate<CoreFileUpload
|
|||
}
|
||||
|
||||
// Sort them by priority.
|
||||
handlers.sort((a, b) => a.priority! <= b.priority! ? 1 : -1);
|
||||
handlers.sort((a, b) => (a.priority || 0) <= (b.priority || 0) ? 1 : -1);
|
||||
|
||||
return handlers;
|
||||
}
|
||||
|
|
|
@ -806,8 +806,8 @@ export class CoreFileUploaderHelperProvider {
|
|||
}
|
||||
}
|
||||
|
||||
if (maxSize != -1 && size > maxSize) {
|
||||
throw this.createMaxBytesError(maxSize, file!.name);
|
||||
if (maxSize != -1 && size > maxSize && file) {
|
||||
throw this.createMaxBytesError(maxSize, file.name);
|
||||
}
|
||||
|
||||
if (size > 0) {
|
||||
|
@ -849,12 +849,12 @@ export class CoreFileUploaderHelperProvider {
|
|||
stringKey: string,
|
||||
progress: ProgressEvent | CoreFileProgressEvent,
|
||||
): void {
|
||||
if (!progress || !progress.lengthComputable) {
|
||||
if (!progress || !progress.lengthComputable || progress.loaded === undefined || !progress.total) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate the progress percentage.
|
||||
const perc = Math.min((progress.loaded! / progress.total!) * 100, 100);
|
||||
const perc = Math.min((progress.loaded / progress.total) * 100, 100);
|
||||
|
||||
if (isNaN(perc) || perc < 0) {
|
||||
return;
|
||||
|
|
|
@ -286,8 +286,8 @@ export class CoreFileUploaderProvider {
|
|||
|
||||
if (!stillInList) {
|
||||
filesToDelete.push({
|
||||
filepath: file.filepath!,
|
||||
filename: file.filename!,
|
||||
filepath: file.filepath || '',
|
||||
filename: file.filename || '',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -643,7 +643,7 @@ export class CoreFileUploaderProvider {
|
|||
filesToUpload.push(<FileEntry> file);
|
||||
} else {
|
||||
// It's an online file.
|
||||
usedNames[file.filename!.toLowerCase()] = file;
|
||||
usedNames[(file.filename || '').toLowerCase()] = file;
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -681,7 +681,7 @@ export class CoreFileUploaderProvider {
|
|||
): Promise<number> {
|
||||
siteId = siteId || CoreSites.getCurrentSiteId();
|
||||
|
||||
let fileName: string | undefined;
|
||||
let fileName = '';
|
||||
let fileEntry: FileEntry | undefined;
|
||||
|
||||
const isOnline = !CoreUtils.isFileEntry(file);
|
||||
|
@ -692,7 +692,7 @@ export class CoreFileUploaderProvider {
|
|||
fileEntry = file;
|
||||
} else {
|
||||
// It's an online file. We need to download it and re-upload it.
|
||||
fileName = file.filename;
|
||||
fileName = file.filename || '';
|
||||
|
||||
const path = await CoreFilepool.downloadUrl(
|
||||
siteId,
|
||||
|
@ -710,9 +710,9 @@ export class CoreFileUploaderProvider {
|
|||
}
|
||||
|
||||
// Now upload the file.
|
||||
const extension = CoreMimetypeUtils.getFileExtension(fileName!);
|
||||
const extension = CoreMimetypeUtils.getFileExtension(fileName);
|
||||
const mimetype = extension ? CoreMimetypeUtils.getMimeType(extension) : undefined;
|
||||
const options = this.getFileUploadOptions(fileEntry.toURL(), fileName!, mimetype, isOnline, 'draft', itemId);
|
||||
const options = this.getFileUploadOptions(fileEntry.toURL(), fileName, mimetype, isOnline, 'draft', itemId);
|
||||
|
||||
const result = await this.uploadFile(fileEntry.toURL(), options, undefined, siteId);
|
||||
|
||||
|
|
|
@ -39,7 +39,7 @@ export class CoreGradesReportLinkHandlerService extends CoreContentLinksHandlerB
|
|||
courseId?: number,
|
||||
data?: { cmid?: string },
|
||||
): CoreContentLinksAction[] | Promise<CoreContentLinksAction[]> {
|
||||
courseId = courseId || Number(params.id);
|
||||
const courseIdentifier = courseId || Number(params.id);
|
||||
data = data || {};
|
||||
|
||||
return [{
|
||||
|
@ -47,7 +47,7 @@ export class CoreGradesReportLinkHandlerService extends CoreContentLinksHandlerB
|
|||
const userId = params.userid ? parseInt(params.userid, 10) : undefined;
|
||||
const moduleId = data?.cmid && parseInt(data.cmid, 10) || undefined;
|
||||
|
||||
CoreGradesHelper.goToGrades(courseId!, userId, moduleId, siteId);
|
||||
CoreGradesHelper.goToGrades(courseIdentifier, userId, moduleId, siteId);
|
||||
},
|
||||
}];
|
||||
}
|
||||
|
|
|
@ -39,7 +39,7 @@ export class CoreGradesUserLinkHandlerService extends CoreContentLinksHandlerBas
|
|||
courseId?: number,
|
||||
data?: { cmid?: string },
|
||||
): CoreContentLinksAction[] | Promise<CoreContentLinksAction[]> {
|
||||
courseId = courseId || Number(params.id);
|
||||
const courseIdentifier = courseId || Number(params.id);
|
||||
data = data || {};
|
||||
|
||||
return [{
|
||||
|
@ -47,7 +47,7 @@ export class CoreGradesUserLinkHandlerService extends CoreContentLinksHandlerBas
|
|||
const userId = params.user ? parseInt(params.user, 10) : undefined;
|
||||
const moduleId = data?.cmid && parseInt(data.cmid, 10) || undefined;
|
||||
|
||||
CoreGradesHelper.goToGrades(courseId!, userId, moduleId, siteId);
|
||||
CoreGradesHelper.goToGrades(courseIdentifier, userId, moduleId, siteId);
|
||||
},
|
||||
}];
|
||||
}
|
||||
|
|
|
@ -166,16 +166,16 @@ export class CoreH5PPlayer {
|
|||
* @returns Promise resolved when done.
|
||||
*/
|
||||
async deleteAllContentIndexesForSite(siteId?: string): Promise<void> {
|
||||
siteId = siteId || CoreSites.getCurrentSiteId();
|
||||
const siteIdentifier = siteId || CoreSites.getCurrentSiteId();
|
||||
|
||||
if (!siteId) {
|
||||
if (!siteIdentifier) {
|
||||
return;
|
||||
}
|
||||
|
||||
const records = await this.h5pCore.h5pFramework.getAllContentData(siteId);
|
||||
const records = await this.h5pCore.h5pFramework.getAllContentData(siteIdentifier);
|
||||
|
||||
await Promise.all(records.map(async (record) => {
|
||||
await CoreUtils.ignoreErrors(this.h5pCore.h5pFS.deleteContentIndex(record.foldername, siteId!));
|
||||
await CoreUtils.ignoreErrors(this.h5pCore.h5pFS.deleteContentIndex(record.foldername, siteIdentifier));
|
||||
}));
|
||||
}
|
||||
|
||||
|
|
|
@ -217,12 +217,13 @@ export class CoreLoginEmailSignupPage implements OnInit {
|
|||
this.countryControl.setValue(this.settings.country || '');
|
||||
}
|
||||
|
||||
this.namefieldsErrors = {};
|
||||
const namefieldsErrors = {};
|
||||
if (this.settings.namefields) {
|
||||
this.settings.namefields.forEach((field) => {
|
||||
this.namefieldsErrors![field] = CoreLoginHelper.getErrorMessages('core.login.missing' + field);
|
||||
namefieldsErrors[field] = CoreLoginHelper.getErrorMessages('core.login.missing' + field);
|
||||
});
|
||||
}
|
||||
this.namefieldsErrors = namefieldsErrors;
|
||||
|
||||
this.countries = await CoreUtils.getCountryListSorted();
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue