Merge pull request #3230 from dpalou/MOBILE-3833

Mobile 3833
main
Pau Ferrer Ocaña 2022-04-05 12:37:10 +02:00 committed by GitHub
commit 568f161550
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 108 additions and 91 deletions

View File

@ -32,10 +32,11 @@ export abstract class AddonModDataFieldPluginComponent implements OnInit, OnChan
@Input() error?: string; // Error when editing.
@Input() form?: FormGroup; // Form where to add the form control. Just required for edit and search modes.
@Input() searchFields?: CoreFormFields; // The search value of all fields.
@Output() gotoEntry: EventEmitter<number>; // Action to perform.
@Output() gotoEntry = new EventEmitter<number>(); // Action to perform.
// Output called when the field is initialized with a value and it didn't have one already.
@Output() onFieldInit = new EventEmitter<AddonModDataEntryFieldInitialized>();
constructor(protected fb: FormBuilder) {
this.gotoEntry = new EventEmitter();
}
/**
@ -114,3 +115,11 @@ export abstract class AddonModDataFieldPluginComponent implements OnInit, OnChan
}
}
/**
* Data for an initialized field.
*/
export type AddonModDataEntryFieldInitialized = Partial<AddonModDataEntryField> & {
fieldid: number;
content: string;
};

View File

@ -16,6 +16,7 @@ import { Component, OnInit, OnChanges, ViewChild, Input, Output, SimpleChange, T
import { FormGroup } from '@angular/forms';
import { CoreDynamicComponent } from '@components/dynamic-component/dynamic-component';
import { CoreFormFields } from '@singletons/form';
import { AddonModDataEntryFieldInitialized } from '../../classes/field-plugin-component';
import { AddonModDataData, AddonModDataField, AddonModDataTemplateMode } from '../../services/data';
import { AddonModDataFieldsDelegate } from '../../services/data-fields-delegate';
@ -37,7 +38,9 @@ export class AddonModDataFieldPluginComponent implements OnInit, OnChanges {
@Input() error?: string; // Error when editing.
@Input() form?: FormGroup; // Form where to add the form control. Just required for edit and search modes.
@Input() searchFields?: CoreFormFields; // The search value of all fields.
@Output() gotoEntry = new EventEmitter(); // Action to perform.
@Output() gotoEntry = new EventEmitter<number>(); // Action to perform.
// Output called when the field is initialized with a value and it didn't have one already.
@Output() onFieldInit = new EventEmitter<AddonModDataEntryFieldInitialized>();
fieldComponent?: Type<unknown>; // Component to render the plugin.
pluginData?: AddonDataFieldPluginComponentData; // Data to pass to the component.
@ -65,9 +68,10 @@ export class AddonModDataFieldPluginComponent implements OnInit, OnChanges {
value: this.value,
database: this.database,
error: this.error,
gotoEntry: this.gotoEntry,
form: this.form,
searchFields: this.searchFields,
gotoEntry: this.gotoEntry,
onFieldInit: this.onFieldInit,
};
}
} finally {
@ -99,5 +103,6 @@ export type AddonDataFieldPluginComponentData = {
error?: string; // Error when editing.
form?: FormGroup; // Form where to add the form control. Just required for edit and search modes.
searchFields?: CoreFormFields; // The search value of all fields.
gotoEntry: EventEmitter<unknown>;
gotoEntry: EventEmitter<number>;
onFieldInit: EventEmitter<AddonModDataEntryFieldInitialized>;
};

View File

@ -68,7 +68,16 @@ export class AddonModDataFieldDateComponent extends AddonModDataFieldPluginCompo
}
this.addControl('f_' + this.field.id, CoreTimeUtils.toDatetimeFormat(date.getTime()));
const seconds = Math.floor(date.getTime() / 1000);
this.addControl('f_' + this.field.id, CoreTimeUtils.toDatetimeFormat(seconds * 1000));
if (!this.searchMode && !this.value?.content) {
this.onFieldInit.emit({
fieldid: this.field.id,
content: String(seconds),
});
}
}
}

View File

@ -41,6 +41,7 @@ import {
} from '../../services/data';
import { AddonModDataHelper } from '../../services/data-helper';
import { CoreDom } from '@singletons/dom';
import { AddonModDataEntryFieldInitialized } from '../../classes/field-plugin-component';
/**
* Page that displays the view edit page.
@ -62,6 +63,7 @@ export class AddonModDataEditPage implements OnInit {
protected forceLeave = false; // To allow leaving the page without checking for changes.
protected initialSelectedGroup?: number;
protected isEditing = false;
protected originalData: AddonModDataEntryFields = {};
entry?: AddonModDataEntry;
fields: Record<number, AddonModDataField> = {};
@ -83,6 +85,7 @@ export class AddonModDataEditPage implements OnInit {
contents: AddonModDataEntryFields;
errors?: Record<number, string>;
form: FormGroup;
onFieldInit: (data: AddonModDataEntryFieldInitialized) => void;
};
errors: Record<number, string> = {};
@ -128,7 +131,7 @@ export class AddonModDataEditPage implements OnInit {
const inputData = this.editForm.value;
let changed = AddonModDataHelper.hasEditDataChanged(inputData, this.fieldsArray, this.entry.contents);
let changed = AddonModDataHelper.hasEditDataChanged(inputData, this.fieldsArray, this.originalData);
changed = changed || (!this.isEditing && this.initialSelectedGroup != this.selectedGroup);
if (changed) {
@ -162,6 +165,7 @@ export class AddonModDataEditPage implements OnInit {
const entry = await AddonModDataHelper.fetchEntry(this.database, this.fieldsArray, this.entryId || 0);
this.entry = entry.entry;
this.originalData = CoreUtils.clone(this.entry.contents);
if (this.entryId) {
// Load correct group.
@ -401,6 +405,7 @@ export class AddonModDataEditPage implements OnInit {
form: this.editForm,
database: this.database,
errors: this.errors,
onFieldInit: this.onFieldInit.bind(this),
};
let template = AddonModDataHelper.getTemplate(this.database!, AddonModDataTemplateType.ADD, this.fieldsArray);
@ -414,7 +419,7 @@ export class AddonModDataEditPage implements OnInit {
// Replace field by a generic directive.
const render = '<addon-mod-data-field-plugin [class.has-errors]="!!errors[' + field.id + ']" mode="edit" \
[field]="fields[' + field.id + ']" [value]="contents[' + field.id + ']" [form]="form" [database]="database" \
[error]="errors[' + field.id + ']"></addon-mod-data-field-plugin>';
[error]="errors[' + field.id + ']" (onFieldInit)="onFieldInit($event)"></addon-mod-data-field-plugin>';
template = template.replace(replaceRegEx, render);
// Replace the field id tag.
@ -435,6 +440,27 @@ export class AddonModDataEditPage implements OnInit {
return template;
}
/**
* A certain value has been initialized.
*
* @param data Data.
*/
onFieldInit(data: AddonModDataEntryFieldInitialized): void {
if (!this.originalData[data.fieldid]) {
this.originalData[data.fieldid] = {
id: 0,
recordid: this.entry?.id ?? 0,
fieldid: data.fieldid,
content: data.content,
content1: data.content1 ?? null,
content2: data.content2 ?? null,
content3: data.content3 ?? null,
content4: data.content4 ?? null,
files: data.files ?? [],
};
}
}
/**
* Return to the entry list (previous page) discarding temp data.
*

View File

@ -212,7 +212,7 @@ export class AddonModDataEntryPage implements OnInit, OnDestroy {
this.logAfterFetch = false;
await CoreUtils.ignoreErrors(AddonModData.logView(this.database.id, this.database.name));
// Store module viewed. It's done in this page because it can be reached using a link.
// Store module viewed because this page also updates recent accessed items block.
CoreCourse.storeModuleViewed(this.courseId, this.moduleId);
}
} catch (error) {

View File

@ -213,8 +213,8 @@ export class AddonModDataHelperProvider {
// Replace field by a generic directive.
const render = '<addon-mod-data-field-plugin [field]="fields[' + field.id + ']" [value]="entries[' + entry.id +
'].contents[' + field.id + ']" mode="' + mode + '" [database]="database" (gotoEntry)="gotoEntry(' + entry.id +
')"></addon-mod-data-field-plugin>';
'].contents[' + field.id + ']" mode="' + mode + '" [database]="database" (gotoEntry)="gotoEntry($event)">' +
'</addon-mod-data-field-plugin>';
template = template.replace(replaceRegex, render);
});

View File

@ -1134,10 +1134,10 @@ export type AddonModDataEntryField = {
fieldid: number; // The field type of the content.
recordid: number; // The record this content belongs to.
content: string; // Contents.
content1: string; // Contents.
content2: string; // Contents.
content3: string; // Contents.
content4: string; // Contents.
content1: string | null; // Contents.
content2: string | null; // Contents.
content3: string | null; // Contents.
content4: string | null; // Contents.
files: CoreFileEntry[];
};

View File

@ -16,7 +16,6 @@ import { Component, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRouteSnapshot } from '@angular/router';
import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker';
import { CoreSwipeNavigationItemsManager } from '@classes/items-management/swipe-navigation-items-manager';
import { CoreCourse } from '@features/course/services/course';
import { CoreNavigator } from '@services/navigator';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreTextUtils } from '@services/utils/text';
@ -50,7 +49,6 @@ export class AddonModFeedbackAttemptPage implements OnInit, OnDestroy {
loaded = false;
protected attemptId: number;
protected fetchSuccess = false;
constructor() {
this.cmId = CoreNavigator.getRequiredRouteNumberParam('cmId');
@ -132,12 +130,6 @@ export class AddonModFeedbackAttemptPage implements OnInit, OnDestroy {
return attemptItem;
}).filter((itemData) => itemData); // Filter items with errors.
if (!this.fetchSuccess) {
this.fetchSuccess = true;
// Store module viewed. It's done in this page because it can be reached using a link.
CoreCourse.storeModuleViewed(this.courseId, this.cmId);
}
} catch (message) {
// Some call failed on fetch, go back.
CoreDomUtils.showErrorModalDefault(message, 'core.course.errorgetmodule', true);

View File

@ -25,7 +25,6 @@ import { CoreDomUtils } from '@services/utils/dom';
import { CoreUtils } from '@services/utils/utils';
import { AddonModFeedbackAttemptItem, AddonModFeedbackAttemptsSource } from '../../classes/feedback-attempts-source';
import { AddonModFeedbackWSAnonAttempt, AddonModFeedbackWSAttempt } from '../../services/feedback';
import { CoreCourse } from '@features/course/services/course';
/**
* Page that displays feedback attempts.
@ -187,13 +186,4 @@ export class AddonModFeedbackAttemptsPage implements AfterViewInit, OnDestroy {
* Attempts manager.
*/
class AddonModFeedbackAttemptsManager extends CoreListItemsManager<AddonModFeedbackAttemptItem, AddonModFeedbackAttemptsSource> {
/**
* @inheritdoc
*/
protected async logActivity(): Promise<void> {
// Store module viewed. It's done in this page because it can be reached using a link.
CoreCourse.storeModuleViewed(this.getSource().COURSE_ID, this.getSource().CM_ID);
}
}

View File

@ -13,7 +13,6 @@
// limitations under the License.
import { Component, OnInit } from '@angular/core';
import { CoreCourse } from '@features/course/services/course';
import { IonRefresher } from '@ionic/angular';
import { CoreGroupInfo, CoreGroups } from '@services/groups';
import { CoreNavigator } from '@services/navigator';
@ -35,7 +34,6 @@ export class AddonModFeedbackNonRespondentsPage implements OnInit {
protected courseId!: number;
protected feedback?: AddonModFeedbackWSFeedback;
protected page = 0;
protected fetchSuccess = false;
selectedGroup!: number;
groupInfo?: CoreGroupInfo;
@ -83,12 +81,6 @@ export class AddonModFeedbackNonRespondentsPage implements OnInit {
this.selectedGroup = CoreGroups.validateGroupId(this.selectedGroup, this.groupInfo);
await this.loadGroupUsers(this.selectedGroup);
if (!this.fetchSuccess) {
this.fetchSuccess = true;
// Store module viewed. It's done in this page because it can be reached using a link.
CoreCourse.storeModuleViewed(this.courseId, this.cmId);
}
} catch (message) {
CoreDomUtils.showErrorModalDefault(message, 'core.course.errorgetmodule', true);

View File

@ -17,7 +17,6 @@ import { Component, OnDestroy, ViewChild, OnInit, AfterViewInit, ElementRef, Opt
import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router';
import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker';
import { CoreSplitViewComponent } from '@components/split-view/split-view';
import { CoreCourse } from '@features/course/services/course';
import { CoreFileUploader } from '@features/fileuploader/services/fileuploader';
import { CoreRatingInfo, CoreRatingProvider } from '@features/rating/services/rating';
import { CoreRatingOffline } from '@features/rating/services/rating-offline';
@ -120,7 +119,6 @@ export class AddonModForumDiscussionPage implements OnInit, AfterViewInit, OnDes
protected ratingOfflineObserver?: CoreEventObserver;
protected ratingSyncObserver?: CoreEventObserver;
protected changeDiscObserver?: CoreEventObserver;
protected fetchSuccess = false;
constructor(
@Optional() protected splitView: CoreSplitViewComponent,
@ -547,12 +545,6 @@ export class AddonModForumDiscussionPage implements OnInit, AfterViewInit, OnDes
this.hasOfflineRatings =
await CoreRatingOffline.hasRatings('mod_forum', 'post', ContextLevel.MODULE, this.cmId, this.discussionId);
if (!this.fetchSuccess) {
this.fetchSuccess = true;
// Store module viewed. It's done in this page because it can be reached using a link.
this.courseId && this.cmId && CoreCourse.storeModuleViewed(this.courseId, this.cmId);
}
} catch (error) {
CoreDomUtils.showErrorModal(error);
} finally {

View File

@ -17,7 +17,6 @@ import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router';
import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker';
import { CoreCommentsCommentsComponent } from '@features/comments/components/comments/comments';
import { CoreComments } from '@features/comments/services/comments';
import { CoreCourse } from '@features/course/services/course';
import { CoreRatingInfo } from '@features/rating/services/rating';
import { CoreTag } from '@features/tag/services/tag';
import { IonRefresher } from '@ionic/angular';
@ -59,7 +58,6 @@ export class AddonModGlossaryEntryPage implements OnInit, OnDestroy {
cmId?: number;
protected entryId!: number;
protected fetchSuccess = false;
constructor(protected route: ActivatedRoute) {}
@ -148,12 +146,6 @@ export class AddonModGlossaryEntryPage implements OnInit, OnDestroy {
this.entry = result.entry;
this.ratingInfo = result.ratinginfo;
if (!this.fetchSuccess) {
this.fetchSuccess = true;
// Store module viewed. It's done in this page because it can be reached using a link.
this.cmId && CoreCourse.storeModuleViewed(this.courseId, this.cmId);
}
if (this.glossary) {
// Glossary already loaded, nothing else to load.
return;

View File

@ -25,7 +25,6 @@ import {
AddonModH5PActivityData,
AddonModH5PActivityAttemptResults,
} from '../../services/h5pactivity';
import { CoreCourse } from '@features/course/services/course';
/**
* Page that displays results of an attempt.
@ -100,9 +99,6 @@ export class AddonModH5PActivityAttemptResultsPage implements OnInit {
this.h5pActivity.name,
{ attemptId: this.attemptId },
));
// Store module viewed. It's done in this page because it can be reached using a link.
CoreCourse.storeModuleViewed(this.courseId, this.cmId);
}
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'Error loading attempt.');

View File

@ -26,7 +26,6 @@ import {
AddonModH5PActivityData,
AddonModH5PActivityUserAttempts,
} from '../../services/h5pactivity';
import { CoreCourse } from '@features/course/services/course';
/**
* Page that displays user attempts of a certain user.
@ -102,9 +101,6 @@ export class AddonModH5PActivityUserAttemptsPage implements OnInit {
this.h5pActivity.name,
{ userId: this.userId },
));
// Store module viewed. It's done in this page because it can be reached using a link.
CoreCourse.storeModuleViewed(this.courseId, this.cmId);
}
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'Error loading attempts.');

View File

@ -13,7 +13,6 @@
// limitations under the License.
import { Component, OnInit } from '@angular/core';
import { CoreCourse } from '@features/course/services/course';
import { CoreUser, CoreUserProfile } from '@features/user/services/user';
import { IonRefresher } from '@ionic/angular';
@ -93,9 +92,6 @@ export class AddonModH5PActivityUsersAttemptsPage implements OnInit {
if (!this.fetchSuccess) {
this.fetchSuccess = true;
CoreUtils.ignoreErrors(AddonModH5PActivity.logViewReport(this.h5pActivity.id, this.h5pActivity.name));
// Store module viewed. It's done in this page because it can be reached using a link.
CoreCourse.storeModuleViewed(this.courseId, this.cmId);
}
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'Error loading attempts.');

View File

@ -34,7 +34,6 @@ import {
AddonModLessonUserAttemptAnswerPageWSData,
} from '../../services/lesson';
import { AddonModLessonAnswerData, AddonModLessonHelper } from '../../services/lesson-helper';
import { CoreCourse } from '@features/course/services/course';
import { CoreTime } from '@singletons/time';
/**
@ -60,7 +59,6 @@ export class AddonModLessonUserRetakePage implements OnInit {
protected userId?: number; // User ID to see the retakes.
protected retakeNumber?: number; // Number of the initial retake to see.
protected previousSelectedRetake?: number; // To be able to detect the previous selected retake when it has changed.
protected fetchSuccess = false;
/**
* Component being initialized.
@ -162,12 +160,6 @@ export class AddonModLessonUserRetakePage implements OnInit {
this.student.profileimageurl = user?.profileimageurl;
await this.setRetake(this.selectedRetake);
if (!this.fetchSuccess) {
this.fetchSuccess = true;
// Store module viewed. It's done in this page because it can be reached using a link.
CoreCourse.storeModuleViewed(this.courseId, this.cmId);
}
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'Error getting data.', true);
}

View File

@ -13,7 +13,6 @@
// limitations under the License.
import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
import { CoreCourse } from '@features/course/services/course';
import { CoreQuestionQuestionParsed } from '@features/question/services/question';
import { CoreQuestionHelper } from '@features/question/services/question-helper';
import { IonContent, IonRefresher } from '@ionic/angular';
@ -162,9 +161,6 @@ export class AddonModQuizReviewPage implements OnInit {
CoreUtils.ignoreErrors(
AddonModQuiz.logViewAttemptReview(this.attemptId, this.quiz.id, this.quiz.name),
);
// Store module viewed. It's done in this page because it can be reached using a link.
CoreCourse.storeModuleViewed(this.courseId, this.cmId);
}
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_quiz.errorgetquiz', true);

View File

@ -447,6 +447,16 @@ export class AddonModWikiIndexComponent extends CoreCourseModuleMainActivityComp
}
}
/**
* @inheritdoc
*/
protected async storeModuleViewed(): Promise<void> {
// Only store module viewed when viewing the main page.
if (!this.pageId) {
await super.storeModuleViewed();
}
}
/**
* Get path to the wiki home view. If cannot determine or it's current view, return undefined.
*

View File

@ -18,7 +18,7 @@
<core-context-menu-item [hidden]="!(commentsLoaded && !hasOffline)" [priority]="100" [content]="'core.refresh' | translate"
(action)="refreshComments(false)" [iconAction]="refreshIcon" [closeOnClick]="true">
</core-context-menu-item>
<core-context-menu-item [hidden]="!(commentsLoaded && hasOffline)" [priority]="100"
<core-context-menu-item [hidden]="!(commentsLoaded && hasOffline && isOnline)" [priority]="100"
[content]="'core.settings.synchronizenow' | translate" (action)="refreshComments(true)" [iconAction]="syncIcon"
[closeOnClick]="false">
</core-context-menu-item>
@ -31,7 +31,7 @@
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="commentsLoaded">
<core-empty-box *ngIf="!comments || !comments.length" icon="fas-comments" [message]="'core.comments.nocomments' | translate">
<core-empty-box *ngIf="!comments?.length && !offlineComment" icon="fas-comments" [message]="'core.comments.nocomments' | translate">
</core-empty-box>
<!-- Load previous messages. -->

View File

@ -30,7 +30,7 @@ import {
import { IonContent, IonRefresher } from '@ionic/angular';
import { ContextLevel, CoreConstants } from '@/core/constants';
import { CoreNavigator } from '@services/navigator';
import { Translate } from '@singletons';
import { Network, NgZone, Translate } from '@singletons';
import { CoreUtils } from '@services/utils/utils';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreUser } from '@features/user/services/user';
@ -41,6 +41,7 @@ import { CoreCommentsDBRecord } from '@features/comments/services/database/comme
import { CoreTimeUtils } from '@services/utils/time';
import { CoreApp } from '@services/app';
import moment from 'moment';
import { Subscription } from 'rxjs';
/**
* Page that displays comments.
@ -77,9 +78,11 @@ export class CoreCommentsViewerPage implements OnInit, OnDestroy {
currentUserId: number;
sending = false;
newComment = '';
isOnline: boolean;
protected addDeleteCommentsAvailable = false;
protected syncObserver?: CoreEventObserver;
protected onlineObserver: Subscription;
protected viewDestroyed = false;
constructor(
@ -104,6 +107,14 @@ export class CoreCommentsViewerPage implements OnInit, OnDestroy {
this.fetchComments(false);
}
}, CoreSites.getCurrentSiteId());
this.isOnline = CoreApp.isOnline();
this.onlineObserver = Network.onChange().subscribe(() => {
// Execute the callback in the Angular zone, so change detection doesn't stop working.
NgZone.run(() => {
this.isOnline = CoreApp.isOnline();
});
});
}
/**
@ -596,6 +607,7 @@ export class CoreCommentsViewerPage implements OnInit, OnDestroy {
*/
ngOnDestroy(): void {
this.syncObserver?.off();
this.onlineObserver.unsubscribe();
this.viewDestroyed = true;
}

View File

@ -442,7 +442,7 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy,
}
this.fetchSuccess = true;
CoreCourse.storeModuleViewed(this.courseId, this.module.id, { sectionId: this.module.section });
this.storeModuleViewed();
// Log activity now.
try {
@ -456,6 +456,15 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy,
}
}
/**
* Store module as viewed.
*
* @return Promise resolved when done.
*/
protected async storeModuleViewed(): Promise<void> {
await CoreCourse.storeModuleViewed(this.courseId, this.module.id, { sectionId: this.module.section });
}
/**
* Log activity.
*

View File

@ -79,12 +79,15 @@ export class CoreMainMenuUserMenuComponent implements OnInit, OnDestroy {
return;
}
this.handlers = [];
handlers.forEach((handler) => {
if (handler.type == CoreUserDelegateService.TYPE_NEW_PAGE) {
this.handlers.push(handler.data);
}
});
const newHandlers = handlers
.filter((handler) => handler.type === CoreUserDelegateService.TYPE_NEW_PAGE)
.map((handler) => handler.data);
// Only update handlers if they have changed, to prevent a blink effect.
if (newHandlers.length !== this.handlers.length ||
JSON.stringify(newHandlers) !== JSON.stringify(this.handlers)) {
this.handlers = newHandlers;
}
this.handlersLoaded = CoreUserDelegate.areHandlersLoaded(this.user.id, CoreUserDelegateContext.USER_MENU);
});