Merge pull request #2718 from crazyserver/MOBILE-3640

Mobile 3640
main
Dani Palou 2021-04-01 10:21:29 +02:00 committed by GitHub
commit a9df9c6411
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
156 changed files with 10884 additions and 265 deletions

View File

@ -43,6 +43,7 @@ import { AddonCalendarOfflineEventDBRecord } from '../../services/database/calen
import { CoreError } from '@classes/errors/error';
import { CoreNavigator } from '@services/navigator';
import { CanLeave } from '@guards/can-leave';
import { CoreForms } from '@singletons/form';
/**
* Page that displays a form to create/edit an event.
@ -518,7 +519,7 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave {
const result = await AddonCalendar.submitEvent(this.eventId, data);
event = result.event;
CoreDomUtils.triggerFormSubmittedEvent(this.formElement, result.sent, this.currentSite.getId());
CoreForms.triggerFormSubmittedEvent(this.formElement, result.sent, this.currentSite.getId());
if (result.sent) {
// Event created or edited, invalidate right days & months.
@ -588,7 +589,7 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave {
try {
await AddonCalendarOffline.deleteEvent(this.eventId!);
CoreDomUtils.triggerFormCancelledEvent(this.formElement, this.currentSite.getId());
CoreForms.triggerFormCancelledEvent(this.formElement, this.currentSite.getId());
this.returnToList();
} catch {
@ -611,7 +612,7 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave {
await CoreDomUtils.showConfirm(Translate.instant('core.confirmcanceledit'));
}
CoreDomUtils.triggerFormCancelledEvent(this.formElement, this.currentSite.getId());
CoreForms.triggerFormCancelledEvent(this.formElement, this.currentSite.getId());
return true;
}

View File

@ -201,7 +201,6 @@ export const CALENDAR_SITE_SCHEMA: CoreSiteSchema = {
],
async migrate(db: SQLiteDB, oldVersion: number): Promise<void> {
if (oldVersion < 3) {
const newTable = EVENTS_TABLE;
let oldTable = 'addon_calendar_events_2';
try {
@ -211,19 +210,7 @@ export const CALENDAR_SITE_SCHEMA: CoreSiteSchema = {
oldTable = 'addon_calendar_events';
}
try {
await db.tableExists(oldTable);
// Move the records from the old table.
const events = await db.getAllRecords<AddonCalendarEventDBRecord>(oldTable);
const promises = events.map((event) => db.insertRecord(newTable, event));
await Promise.all(promises);
db.dropTable(oldTable);
} catch {
// Old table does not exist, ignore.
}
await db.migrateTable(oldTable, EVENTS_TABLE);
}
},
};

View File

@ -15,6 +15,7 @@
import { Component, Input, ViewChild, ElementRef } from '@angular/core';
import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreFormFields, CoreForms } from '@singletons/form';
import { CoreUtils } from '@services/utils/utils';
import { ModalController, Translate } from '@singletons';
import { AddonModAssignAssign, AddonModAssignPlugin, AddonModAssignSubmission } from '../../services/assign';
@ -47,7 +48,7 @@ export class AddonModAssignEditFeedbackModalComponent {
await CoreDomUtils.showConfirm(Translate.instant('core.confirmcanceledit'));
}
CoreDomUtils.triggerFormCancelledEvent(this.formElement, CoreSites.getCurrentSiteId());
CoreForms.triggerFormCancelledEvent(this.formElement, CoreSites.getCurrentSiteId());
ModalController.dismiss();
}
@ -61,7 +62,7 @@ export class AddonModAssignEditFeedbackModalComponent {
e.preventDefault();
e.stopPropagation();
CoreDomUtils.triggerFormSubmittedEvent(this.formElement, false, CoreSites.getCurrentSiteId());
CoreForms.triggerFormSubmittedEvent(this.formElement, false, CoreSites.getCurrentSiteId());
// Close the modal, sending the input data.
ModalController.dismiss(this.getInputData());
@ -72,8 +73,8 @@ export class AddonModAssignEditFeedbackModalComponent {
*
* @return Object with the data.
*/
protected getInputData(): Record<string, unknown> {
return CoreDomUtils.getDataFromForm(document.forms['addon-mod_assign-edit-feedback-form']);
protected getInputData(): CoreFormFields {
return CoreForms.getDataFromForm(document.forms['addon-mod_assign-edit-feedback-form']);
}
/**

View File

@ -21,6 +21,7 @@ import { CoreNavigator } from '@services/navigator';
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
import { CoreSync } from '@services/sync';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreFormFields, CoreForms } from '@singletons/form';
import { Translate } from '@singletons';
import { CoreEvents } from '@singletons/events';
import {
@ -105,7 +106,7 @@ export class AddonModAssignEditPage implements OnInit, OnDestroy, CanLeave {
// Nothing has changed or user confirmed to leave. Clear temporary data from plugins.
AddonModAssignHelper.clearSubmissionPluginTmpData(this.assign!, this.userSubmission, this.getInputData());
CoreDomUtils.triggerFormCancelledEvent(this.formElement, CoreSites.getCurrentSiteId());
CoreForms.triggerFormCancelledEvent(this.formElement, CoreSites.getCurrentSiteId());
return true;
}
@ -199,8 +200,8 @@ export class AddonModAssignEditPage implements OnInit, OnDestroy, CanLeave {
*
* @return Input data.
*/
protected getInputData(): Record<string, unknown> {
return CoreDomUtils.getDataFromForm(document.forms['addon-mod_assign-edit-form']);
protected getInputData(): CoreFormFields {
return CoreForms.getDataFromForm(document.forms['addon-mod_assign-edit-form']);
}
/**
@ -234,7 +235,7 @@ export class AddonModAssignEditPage implements OnInit, OnDestroy, CanLeave {
* @param inputData The input data.
* @return Promise resolved with the data to submit.
*/
protected prepareSubmissionData(inputData: Record<string, unknown>): Promise<AddonModAssignSavePluginData> {
protected prepareSubmissionData(inputData: CoreFormFields): Promise<AddonModAssignSavePluginData> {
// If there's offline data, always save it in offline.
this.saveOffline = this.hasOffline;
@ -353,7 +354,7 @@ export class AddonModAssignEditPage implements OnInit, OnDestroy, CanLeave {
}
// Submission saved, trigger events.
CoreDomUtils.triggerFormSubmittedEvent(this.formElement, sent, CoreSites.getCurrentSiteId());
CoreForms.triggerFormSubmittedEvent(this.formElement, sent, CoreSites.getCurrentSiteId());
CoreEvents.trigger(
AddonModAssignProvider.SUBMISSION_SAVED_EVENT,

View File

@ -35,6 +35,7 @@ import { CoreGroups } from '@services/groups';
import { AddonModAssignSubmissionDelegate } from './submission-delegate';
import { AddonModAssignFeedbackDelegate } from './feedback-delegate';
import { makeSingleton } from '@singletons';
import { CoreFormFields } from '@singletons/form';
/**
* Service that provides some helper functions for assign.
@ -88,7 +89,7 @@ export class AddonModAssignHelperProvider {
clearSubmissionPluginTmpData(
assign: AddonModAssignAssign,
submission: AddonModAssignSubmission | undefined,
inputData: Record<string, unknown>,
inputData: CoreFormFields,
): void {
if (!submission) {
return;
@ -362,7 +363,7 @@ export class AddonModAssignHelperProvider {
async getSubmissionSizeForEdit(
assign: AddonModAssignAssign,
submission: AddonModAssignSubmission,
inputData: Record<string, unknown>,
inputData: CoreFormFields,
): Promise<number> {
let totalSize = 0;
@ -537,7 +538,7 @@ export class AddonModAssignHelperProvider {
async hasSubmissionDataChanged(
assign: AddonModAssignAssign,
submission: AddonModAssignSubmission | undefined,
inputData: Record<string, unknown>,
inputData: CoreFormFields,
): Promise<boolean> {
if (!submission) {
return false;
@ -580,7 +581,7 @@ export class AddonModAssignHelperProvider {
siteId?: string,
): Promise<AddonModAssignSavePluginData> {
const pluginData: Record<string, unknown> = {};
const pluginData: CoreFormFields = {};
const promises = feedback.plugins
? feedback.plugins.map((plugin) =>
AddonModAssignFeedbackDelegate.preparePluginFeedbackData(assignId, userId, plugin, pluginData, siteId))
@ -603,7 +604,7 @@ export class AddonModAssignHelperProvider {
async prepareSubmissionPluginData(
assign: AddonModAssignAssign,
submission: AddonModAssignSubmission | undefined,
inputData: Record<string, unknown>,
inputData: CoreFormFields,
offline = false,
): Promise<AddonModAssignSavePluginData> {

View File

@ -186,7 +186,7 @@ export class AddonModAssignSyncProvider extends CoreCourseActivitySyncBaseProvid
* Perform the assign submission.
*
* @param assignId Assign ID.
* @param siteId Site ID. If not defined, current site.
* @param siteId Site ID.
* @return Promise resolved in success.
*/
protected async performSyncAssign(assignId: number, siteId: string): Promise<AddonModAssignSyncResult> {

View File

@ -33,6 +33,7 @@ import { CoreComments } from '@features/comments/services/comments';
import { AddonModAssignSubmissionFormatted } from './assign-helper';
import { CoreWSError } from '@classes/errors/wserror';
import { AddonModAssignAutoSyncData, AddonModAssignManualSyncData, AddonModAssignSyncProvider } from './assign-sync';
import { CoreFormFields } from '@singletons/form';
const ROOT_CACHE_KEY = 'mmaModAssign:';
@ -467,7 +468,7 @@ export class AddonModAssignProvider {
): Promise<{ canviewsubmissions: boolean; submissions?: AddonModAssignSubmission[] }> {
const site = await CoreSites.getSite(options.siteId);
const params: ModAssignGetSubmissionsWSParams = {
const params: AddonModAssignGetSubmissionsWSParams = {
assignmentids: [assignId],
};
const preSets: CoreSiteWSPreSets = {
@ -1681,7 +1682,7 @@ export type AddonModAssignGetAssignmentsWSResponse = {
/**
* Params of mod_assign_get_submissions WS.
*/
type ModAssignGetSubmissionsWSParams = {
type AddonModAssignGetSubmissionsWSParams = {
assignmentids: number[]; // 1 or more assignment ids.
status?: string; // Status.
since?: number; // Submitted since.
@ -1808,7 +1809,7 @@ type AddonModAssignSaveSubmissionWSParams = {
/**
* All subplugins will decide what to add here.
*/
export type AddonModAssignSavePluginData = Record<string, unknown>;
export type AddonModAssignSavePluginData = CoreFormFields;
/**
* Params of mod_assign_submit_for_grading WS.

View File

@ -19,6 +19,7 @@ import { AddonModAssignAssign, AddonModAssignSubmission, AddonModAssignPlugin, A
import { makeSingleton } from '@singletons';
import { CoreWSExternalFile } from '@services/ws';
import { AddonModAssignSubmissionFormatted } from './assign-helper';
import { CoreFormFields } from '@singletons/form';
/**
* Interface that all feedback handlers must implement.
@ -61,7 +62,7 @@ export interface AddonModAssignFeedbackHandler extends CoreDelegateHandler {
assignId: number,
userId: number,
siteId?: string,
): Record<string, unknown> | Promise<Record<string, unknown> | undefined> | undefined;
): CoreFormFields | Promise<CoreFormFields | undefined> | undefined;
/**
* Get files used by this plugin.
@ -102,7 +103,7 @@ export interface AddonModAssignFeedbackHandler extends CoreDelegateHandler {
assign: AddonModAssignAssign,
submission: AddonModAssignSubmission,
plugin: AddonModAssignPlugin,
inputData: Record<string, unknown>,
inputData: CoreFormFields,
userId: number,
): boolean | Promise<boolean>;
@ -165,7 +166,7 @@ export interface AddonModAssignFeedbackHandler extends CoreDelegateHandler {
assignId: number,
userId: number,
plugin: AddonModAssignPlugin,
data: Record<string, unknown>,
data: CoreFormFields,
siteId?: string,
): void | Promise<void>;
}
@ -276,7 +277,7 @@ export class AddonModAssignFeedbackDelegateService extends CoreDelegate<AddonMod
assign: AddonModAssignAssign,
submission: AddonModAssignSubmission | AddonModAssignSubmissionFormatted,
plugin: AddonModAssignPlugin,
inputData: Record<string, unknown>,
inputData: CoreFormFields,
userId: number,
): Promise<boolean | undefined> {
return await this.executeFunctionOnEnabled(
@ -371,7 +372,7 @@ export class AddonModAssignFeedbackDelegateService extends CoreDelegate<AddonMod
assignId: number,
userId: number,
plugin: AddonModAssignPlugin,
inputData: Record<string, unknown>,
inputData: CoreFormFields,
siteId?: string,
): Promise<void> {
return await this.executeFunctionOnEnabled(

View File

@ -19,6 +19,7 @@ import { AddonModAssignAssign, AddonModAssignSubmission, AddonModAssignPlugin, A
import { makeSingleton } from '@singletons';
import { CoreWSExternalFile } from '@services/ws';
import { AddonModAssignSubmissionsDBRecordFormatted } from './assign-offline';
import { CoreFormFields } from '@singletons/form';
/**
* Interface that all submission handlers must implement.
@ -70,7 +71,7 @@ export interface AddonModAssignSubmissionHandler extends CoreDelegateHandler {
assign: AddonModAssignAssign,
submission: AddonModAssignSubmission,
plugin: AddonModAssignPlugin,
inputData: Record<string, unknown>,
inputData: CoreFormFields,
): void;
/**
@ -173,7 +174,7 @@ export interface AddonModAssignSubmissionHandler extends CoreDelegateHandler {
assign: AddonModAssignAssign,
submission: AddonModAssignSubmission,
plugin: AddonModAssignPlugin,
inputData: Record<string, unknown>,
inputData: CoreFormFields,
): number | Promise<number>;
/**
@ -189,7 +190,7 @@ export interface AddonModAssignSubmissionHandler extends CoreDelegateHandler {
assign: AddonModAssignAssign,
submission: AddonModAssignSubmission,
plugin: AddonModAssignPlugin,
inputData: Record<string, unknown>,
inputData: CoreFormFields,
): boolean | Promise<boolean>;
/**
@ -233,7 +234,7 @@ export interface AddonModAssignSubmissionHandler extends CoreDelegateHandler {
assign: AddonModAssignAssign,
submission: AddonModAssignSubmission,
plugin: AddonModAssignPlugin,
inputData: Record<string, unknown>,
inputData: CoreFormFields,
pluginData: AddonModAssignSavePluginData,
offline?: boolean,
userId?: number,
@ -304,7 +305,7 @@ export class AddonModAssignSubmissionDelegateService extends CoreDelegate<AddonM
assign: AddonModAssignAssign,
submission: AddonModAssignSubmission,
plugin: AddonModAssignPlugin,
inputData: Record<string, unknown>,
inputData: CoreFormFields,
): void {
return this.executeFunctionOnEnabled(plugin.type, 'clearTmpData', [assign, submission, plugin, inputData]);
}
@ -424,7 +425,7 @@ export class AddonModAssignSubmissionDelegateService extends CoreDelegate<AddonM
assign: AddonModAssignAssign,
submission: AddonModAssignSubmission,
plugin: AddonModAssignPlugin,
inputData: Record<string, unknown>,
inputData: CoreFormFields,
): Promise<number | undefined> {
return await this.executeFunctionOnEnabled(
plugin.type,
@ -446,7 +447,7 @@ export class AddonModAssignSubmissionDelegateService extends CoreDelegate<AddonM
assign: AddonModAssignAssign,
submission: AddonModAssignSubmission,
plugin: AddonModAssignPlugin,
inputData: Record<string, unknown>,
inputData: CoreFormFields,
): Promise<boolean | undefined> {
return await this.executeFunctionOnEnabled(
plugin.type,
@ -521,7 +522,7 @@ export class AddonModAssignSubmissionDelegateService extends CoreDelegate<AddonM
assign: AddonModAssignAssign,
submission: AddonModAssignSubmission,
plugin: AddonModAssignPlugin,
inputData: Record<string, unknown>,
inputData: CoreFormFields,
pluginData: AddonModAssignSavePluginData,
offline?: boolean,
userId?: number,

View File

@ -0,0 +1,116 @@
// (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 { Input, Output, OnInit, OnChanges, SimpleChange, EventEmitter, Component } from '@angular/core';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { CoreFormFields } from '@singletons/form';
import { AddonModDataData, AddonModDataEntryField, AddonModDataField, AddonModDataTemplateMode } from '../services/data';
/**
* Base class for component to render a field.
*/
@Component({
template: '',
})
export abstract class AddonModDataFieldPluginComponent implements OnInit, OnChanges {
@Input() mode!: AddonModDataTemplateMode; // The render mode.
@Input() field!: AddonModDataField; // The field to render.
@Input() value?: Partial<AddonModDataEntryField>; // The value of the field.
@Input() database?: AddonModDataData; // Database object.
@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.
constructor(protected fb: FormBuilder) {
this.gotoEntry = new EventEmitter();
}
/**
* Add the form control for the search mode.
*
* @param fieldName Control field name.
* @param value Initial set value.
*/
protected addControl(fieldName: string, value?: unknown): void {
if (!this.form) {
return;
}
if (this.searchMode) {
this.form.addControl(fieldName, this.fb.control(this.searchFields?.[fieldName] || undefined));
}
if (this.editMode) {
this.form.addControl(fieldName, this.fb.control(value, this.field.required ? Validators.required : null));
}
}
/**
* Component being initialized.
*/
ngOnInit(): void {
this.init();
}
/**
* Initialize field.
*/
protected init(): void {
return;
}
/**
* Component being changed.
*/
ngOnChanges(changes: { [name: string]: SimpleChange }): void {
if ((this.showMode || this.listMode) && changes.value) {
this.updateValue(changes.value.currentValue);
}
}
/**
* Update value being shown.
*/
protected updateValue(value?: Partial<AddonModDataEntryField>): void {
this.value = value;
}
/** Magic mode getters */
get listMode(): boolean {
return this.mode == AddonModDataTemplateMode.LIST;
}
get showMode(): boolean {
return this.mode == AddonModDataTemplateMode.SHOW;
}
get displayMode(): boolean {
return this.listMode || this.showMode;
}
get editMode(): boolean {
return this.mode == AddonModDataTemplateMode.EDIT;
}
get searchMode(): boolean {
return this.mode == AddonModDataTemplateMode.SEARCH;
}
get inputMode(): boolean {
return this.searchMode || this.editMode;
}
}

View File

@ -0,0 +1,140 @@
// (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, OnInit, Input } from '@angular/core';
import { Params } from '@angular/router';
import { CoreCourseModule } from '@features/course/services/course-helper';
import { CoreTag } from '@features/tag/services/tag';
import { CoreUser } from '@features/user/services/user';
import { CoreNavigator } from '@services/navigator';
import { CoreSites } from '@services/sites';
import { CoreEvents } from '@singletons/events';
import {
AddonModDataAction,
AddonModDataData,
AddonModDataEntry,
AddonModDataProvider,
AddonModDataTemplateMode,
} from '../../services/data';
import { AddonModDataHelper } from '../../services/data-helper';
import { AddonModDataOffline } from '../../services/data-offline';
import { AddonModDataModuleHandlerService } from '../../services/handlers/module';
/**
* Component that displays a database action.
*/
@Component({
selector: 'addon-mod-data-action',
templateUrl: 'addon-mod-data-action.html',
})
export class AddonModDataActionComponent implements OnInit {
@Input() mode!: AddonModDataTemplateMode; // The render mode.
@Input() action!: AddonModDataAction; // The field to render.
@Input() entry!: AddonModDataEntry; // The value of the field.
@Input() database!: AddonModDataData; // Database object.
@Input() module!: CoreCourseModule; // Module object.
@Input() group = 0; // Module object.
@Input() offset?: number; // Offset of the entry.
siteId: string;
userPicture?: string;
tagsEnabled = false;
constructor() {
this.siteId = CoreSites.getCurrentSiteId();
this.tagsEnabled = CoreTag.areTagsAvailableInSite();
}
/**
* Component being initialized.
*/
async ngOnInit(): Promise<void> {
if (this.action == AddonModDataAction.USERPICTURE) {
const profile = await CoreUser.getProfile(this.entry.userid, this.database.course);
this.userPicture = profile.profileimageurl;
}
}
/**
* Approve the entry.
*/
approveEntry(): void {
AddonModDataHelper.approveOrDisapproveEntry(this.database.id, this.entry.id, true, this.database.course);
}
/**
* Show confirmation modal for deleting the entry.
*/
deleteEntry(): void {
AddonModDataHelper.showDeleteEntryModal(this.database.id, this.entry.id, this.database.course);
}
/**
* Disapprove the entry.
*/
disapproveEntry(): void {
AddonModDataHelper.approveOrDisapproveEntry(this.database.id, this.entry.id, false, this.database.course);
}
/**
* Go to the edit page of the entry.
*/
editEntry(): void {
const params = {
courseId: this.database.course,
module: this.module,
};
CoreNavigator.navigateToSitePath(
`${AddonModDataModuleHandlerService.PAGE_NAME}/${this.module.course}/${this.module.id}/edit/${this.entry.id}`,
{ params },
);
}
/**
* Go to the view page of the entry.
*/
viewEntry(): void {
const params: Params = {
courseId: this.database.course,
module: this.module,
entryId: this.entry.id,
group: this.group,
offset: this.offset,
};
CoreNavigator.navigateToSitePath(
`${AddonModDataModuleHandlerService.PAGE_NAME}/${this.module.course}/${this.module.id}/${this.entry.id}`,
{ params },
);
}
/**
* Undo delete action.
*
* @return Solved when done.
*/
async undoDelete(): Promise<void> {
const dataId = this.database.id;
const entryId = this.entry.id;
await AddonModDataOffline.getEntry(dataId, entryId, AddonModDataAction.DELETE, this.siteId);
// Found. Just delete the action.
await AddonModDataOffline.deleteEntry(dataId, entryId, AddonModDataAction.DELETE, this.siteId);
CoreEvents.trigger(AddonModDataProvider.ENTRY_CHANGED, { dataId: dataId, entryId: entryId }, this.siteId);
}
}

View File

@ -0,0 +1,42 @@
<ion-button *ngIf="action == 'more'" fill="clear" (click)="viewEntry()" [title]="'addon.mod_data.more' | translate">
<ion-icon name="fas-search" slot="icon-only"></ion-icon>
</ion-button>
<ion-button *ngIf="action == 'edit'" fill="clear" (click)="editEntry()" [title]="'core.edit' | translate">
<ion-icon name="fas-cog" slot="icon-only"></ion-icon>
</ion-button>
<ion-button *ngIf="action == 'delete' && !entry.deleted" fill="clear" (click)="deleteEntry()" [title]="'core.delete' | translate">
<ion-icon name="fas-trash" slot="icon-only"></ion-icon>
</ion-button>
<ion-button *ngIf="action == 'delete' && entry.deleted" fill="clear" (click)="undoDelete()" [title]="'core.restore' | translate">
<ion-icon name="fas-undo-alt" slot="icon-only"></ion-icon>
</ion-button>
<ion-button *ngIf="action == 'approve'" fill="clear" (click)="approveEntry()" [title]="'addon.mod_data.approve' | translate">
<ion-icon name="fas-thumbs-up" slot="icon-only"></ion-icon>
</ion-button>
<ion-button *ngIf="action == 'disapprove'" fill="clear" (click)="disapproveEntry()"
[title]="'addon.mod_data.disapprove' | translate">
<ion-icon name="far-thumbs-down" slot="icon-only"></ion-icon>
</ion-button>
<core-comments *ngIf="action == 'comments' && mode == 'list'" contextLevel="module" [instanceId]="database.coursemodule"
component="mod_data" [itemId]="entry.id" area="database_entry" [courseId]="database.course">
</core-comments>
<span *ngIf="action == 'timeadded'">{{ entry.timecreated * 1000 | coreFormatDate }}</span>
<span *ngIf="action == 'timemodified'">{{ entry.timemodified * 1000 | coreFormatDate }}</span>
<a *ngIf="action == 'userpicture'" core-user-link [courseId]="database.course" [userId]="entry.userid" [title]="entry.fullname">
<img class="avatar-round" [src]="userPicture" [alt]="'core.pictureof' | translate:{$a: entry.fullname}" core-external-content
onError="this.src='assets/img/user-avatar.png'" role="presentation">
</a>
<a *ngIf="action == 'user' && entry" core-user-link [courseId]="database.course" [userId]="entry.userid" [title]="entry.fullname">
{{entry.fullname}}
</a>
<core-tag-list *ngIf="tagsEnabled && action == 'tags' && entry" [tags]="entry.tags"></core-tag-list>

View File

@ -0,0 +1,38 @@
// (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 { NgModule } from '@angular/core';
import { AddonModDataFieldPluginComponent } from './field-plugin/field-plugin';
import { AddonModDataActionComponent } from './action/action';
import { CoreSharedModule } from '@/core/shared.module';
import { CoreCommentsComponentsModule } from '@features/comments/components/components.module';
import { CoreTagComponentsModule } from '@features/tag/components/components.module';
// This module is intended to be passed to the compiler in order to avoid circular depencencies.
@NgModule({
declarations: [
AddonModDataFieldPluginComponent,
AddonModDataActionComponent,
],
imports: [
CoreSharedModule,
CoreCommentsComponentsModule,
CoreTagComponentsModule,
],
exports: [
AddonModDataActionComponent,
AddonModDataFieldPluginComponent,
],
})
export class AddonModDataComponentsCompileModule {}

View File

@ -0,0 +1,37 @@
// (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 { NgModule } from '@angular/core';
import { CoreSharedModule } from '@/core/shared.module';
import { CoreCourseComponentsModule } from '@features/course/components/components.module';
import { AddonModDataIndexComponent } from './index';
import { AddonModDataSearchComponent } from './search/search';
import { CoreCompileHtmlComponentModule } from '@features/compile/components/compile-html/compile-html.module';
@NgModule({
declarations: [
AddonModDataIndexComponent,
AddonModDataSearchComponent,
],
imports: [
CoreSharedModule,
CoreCourseComponentsModule,
CoreCompileHtmlComponentModule,
],
exports: [
AddonModDataIndexComponent,
AddonModDataSearchComponent,
],
})
export class AddonModDataComponentsModule {}

View File

@ -0,0 +1,5 @@
<core-dynamic-component [component]="fieldComponent" [data]="pluginData">
<!-- This content will be replaced by the component if found. -->
<core-loading [hideUntil]="fieldLoaded">
</core-loading>
</core-dynamic-component>

View File

@ -0,0 +1,103 @@
// (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, OnInit, OnChanges, ViewChild, Input, Output, SimpleChange, Type, EventEmitter } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { CoreDynamicComponent } from '@components/dynamic-component/dynamic-component';
import { CoreFormFields } from '@singletons/form';
import { AddonModDataData, AddonModDataField, AddonModDataTemplateMode } from '../../services/data';
import { AddonModDataFieldsDelegate } from '../../services/data-fields-delegate';
/**
* Component that displays a database field plugin.
*/
@Component({
selector: 'addon-mod-data-field-plugin',
templateUrl: 'addon-mod-data-field-plugin.html',
})
export class AddonModDataFieldPluginComponent implements OnInit, OnChanges {
@ViewChild(CoreDynamicComponent) dynamicComponent?: CoreDynamicComponent;
@Input() mode!: AddonModDataTemplateMode; // The render mode.
@Input() field!: AddonModDataField; // The field to render.
@Input() value?: unknown; // The value of the field.
@Input() database?: AddonModDataData; // Database object.
@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.
fieldComponent?: Type<unknown>; // Component to render the plugin.
pluginData?: AddonDataFieldPluginComponentData; // Data to pass to the component.
fieldLoaded = false;
/**
* Component being initialized.
*/
async ngOnInit(): Promise<void> {
if (!this.field) {
this.fieldLoaded = true;
return;
}
try{
// Check if the plugin has defined its own component to render itself.
this.fieldComponent = await AddonModDataFieldsDelegate.getComponentForField(this.field);
if (this.fieldComponent) {
// Prepare the data to pass to the component.
this.pluginData = {
mode: this.mode,
field: this.field,
value: this.value,
database: this.database,
error: this.error,
gotoEntry: this.gotoEntry,
form: this.form,
searchFields: this.searchFields,
};
}
} finally {
this.fieldLoaded = true;
}
}
/**
* Component being changed.
*/
ngOnChanges(changes: { [name: string]: SimpleChange }): void {
if (this.fieldLoaded && this.pluginData) {
if (this.mode == AddonModDataTemplateMode.EDIT && changes.error) {
this.pluginData.error = changes.error.currentValue;
}
if ((this.mode == AddonModDataTemplateMode.SHOW || this.mode == AddonModDataTemplateMode.LIST) && changes.value) {
this.pluginData.value = changes.value.currentValue;
}
}
}
}
export type AddonDataFieldPluginComponentData = {
mode: AddonModDataTemplateMode; // The render mode.
field: AddonModDataField; // The field to render.
value?: unknown; // The value of the field.
database?: AddonModDataData; // Database object.
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>;
};

View File

@ -0,0 +1,154 @@
<!-- Buttons to add to the header. -->
<core-navbar-buttons slot="end">
<ion-button *ngIf="canSearch" (click)="showSearch()" [attr.aria-label]="'addon.mod_data.search' | translate">
<ion-icon name="fas-search" slot="icon-only"></ion-icon>
</ion-button>
<core-context-menu>
<core-context-menu-item *ngIf="externalUrl" [priority]="900" [content]="'core.openinbrowser' | translate"
[href]="externalUrl" iconAction="fas-external-link-alt">
</core-context-menu-item>
<core-context-menu-item *ngIf="description" [priority]="800" [content]="'core.moduleintro' | translate"
(action)="expandDescription()" iconAction="fas-arrow-right">
</core-context-menu-item>
<core-context-menu-item *ngIf="blog" [priority]="750" content="{{'addon.blog.blog' | translate}}"
iconAction="far-newspaper" (action)="gotoBlog()">
</core-context-menu-item>
<core-context-menu-item *ngIf="loaded && !(hasOffline || hasOfflineRatings) && isOnline" [priority]="700"
[content]="'core.refresh' | translate" (action)="doRefresh(null, $event)" [iconAction]="refreshIcon"
[closeOnClick]="false">
</core-context-menu-item>
<core-context-menu-item *ngIf="loaded && (hasOffline || hasOfflineRatings) && isOnline" [priority]="600"
[content]="'core.settings.synchronizenow' | translate" (action)="doRefresh(null, $event, true)" [iconAction]="syncIcon"
[closeOnClick]="false">
</core-context-menu-item>
<core-context-menu-item [priority]="500" *ngIf="canAdd" [content]="'addon.mod_data.addentries' | translate"
iconAction="fas-plus" (action)="gotoAddEntries()">
</core-context-menu-item>
<core-context-menu-item [priority]="400" *ngIf="firstEntry" [content]="'addon.mod_data.single' | translate"
iconAction="fas-file" (action)="gotoEntry(firstEntry)">
</core-context-menu-item>
<core-context-menu-item *ngIf="prefetchStatusIcon" [priority]="300" [content]="prefetchText" (action)="prefetch($event)"
[iconAction]="prefetchStatusIcon" [closeOnClick]="false">
</core-context-menu-item>
<core-context-menu-item *ngIf="size" [priority]="200" [content]="'core.clearstoreddata' | translate:{$a: size}"
iconDescription="fas-archive" (action)="removeFiles($event)" iconAction="fas-trash" [closeOnClick]="false">
</core-context-menu-item>
</core-context-menu>
</core-navbar-buttons>
<!-- Content. -->
<ion-content>
<core-loading [hideUntil]="loaded" class="core-loading-center">
<core-course-module-description [description]="description" [component]="component" [componentId]="componentId"
contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
</core-course-module-description>
<!-- Data done in offline but not synchronized -->
<ion-card class="core-warning-card" *ngIf="hasOffline || hasOfflineRatings">
<ion-item>
<ion-icon name="fas-exclamation-triangle" slot="start"></ion-icon>
<ion-label>{{ 'core.hasdatatosync' | translate: {$a: moduleName} }}</ion-label>
</ion-item>
</ion-card>
<ion-item class="ion-text-wrap" *ngIf="groupInfo && (groupInfo.separateGroups || groupInfo.visibleGroups)">
<ion-label id="addon-data-groupslabel">
<ng-container *ngIf="groupInfo.separateGroups">{{'core.groupsseparate' | translate }}</ng-container>
<ng-container *ngIf="groupInfo.visibleGroups">{{'core.groupsvisible' | translate }}</ng-container>
</ion-label>
<ion-select [(ngModel)]="selectedGroup" (ionChange)="setGroup(selectedGroup)" aria-labelledby="addon-data-groupslabel"
interface="action-sheet">
<ion-select-option *ngFor="let groupOpt of groupInfo.groups" [value]="groupOpt.id">
{{groupOpt.name}}
</ion-select-option>
</ion-select>
</ion-item>
<ion-card class="core-info-card" *ngIf="!access?.timeavailable && timeAvailableFrom">
<ion-item>
<ion-icon name="fas-info-circle" slot="start"></ion-icon>
<ion-label>{{ 'addon.mod_data.notopenyet' | translate:{$a: timeAvailableFromReadable} }}</ion-label>
</ion-item>
</ion-card>
<ion-card class="core-info-card" *ngIf="!access?.timeavailable && timeAvailableTo">
<ion-item>
<ion-icon name="fas-info-circle" slot="start"></ion-icon>
<ion-label>{{ 'addon.mod_data.expired' | translate:{$a: timeAvailableToReadable} }}</ion-label>
</ion-item>
</ion-card>
<ion-card class="core-info-card" *ngIf="access && access.entrieslefttoview">>
<ion-item>
<ion-icon name="fas-info-circle" slot="start"></ion-icon>
<ion-label>
{{ 'addon.mod_data.entrieslefttoaddtoview' | translate:{$a: {entrieslefttoview: access.entrieslefttoview} } }}
</ion-label>
</ion-item>
</ion-card>
<ion-card class="core-info-card" *ngIf="access && access.entrieslefttoadd">>
<ion-item>
<ion-icon name="fas-info-circle" slot="start"></ion-icon>
<ion-label>
{{ 'addon.mod_data.entrieslefttoadd' | translate:{$a: {entriesleft: access.entrieslefttoadd} } }}
</ion-label>
</ion-item>
</ion-card>
<!-- Reset search. -->
<ng-container *ngIf="search.searching && !isEmpty">
<ion-item *ngIf="!foundRecordsTranslationData">
<ion-label>
<a (click)="searchReset()">{{ 'addon.mod_data.resetsettings' | translate}}</a>
</ion-label>
</ion-item>
<ion-card class="core-success-card" *ngIf="foundRecordsTranslationData" (click)="searchReset()">
<ion-item><ion-label>
<p [innerHTML]="'addon.mod_data.foundrecords' | translate:{$a: foundRecordsTranslationData}"></p>
</ion-label></ion-item>
</ion-card>
</ng-container>
<div class="addon-data-contents addon-data-entries-{{database.id}} ion-padding-horizontal" *ngIf="!isEmpty && database">
<core-style [css]="database.csstemplate" prefix=".addon-data-entries-{{database.id}}"></core-style>
<core-compile-html [text]="entriesRendered" [jsData]="jsData" [extraImports]="extraImports"></core-compile-html>
</div>
<ion-grid *ngIf="search.page > 0 || hasNextPage">
<ion-row class="ion-align-items-center">
<ion-col *ngIf="search.page > 0">
<ion-button expand="block" fill="outline" (click)="searchEntries(search.page - 1)">
<ion-icon name="fas-chevron-left" slot="start"></ion-icon>
{{ 'core.previous' | translate }}
</ion-button>
</ion-col>
<ion-col *ngIf="hasNextPage">
<ion-button expand="block" (click)="searchEntries(search.page + 1)">
{{ 'core.next' | translate }}
<ion-icon name="fas-chevron-right" slot="end"></ion-icon>
</ion-button>
</ion-col>
</ion-row>
</ion-grid>
<core-empty-box *ngIf="isEmpty && !search.searching" icon="fas-database" [message]="'addon.mod_data.norecords' | translate">
</core-empty-box>
<core-empty-box *ngIf="isEmpty && search.searching" icon="fas-database" [message]="'addon.mod_data.nomatch' | translate"
class="core-empty-box-clickable">
<a (click)="searchReset()">{{ 'addon.mod_data.resetsettings' | translate}}</a>
</core-empty-box>
</core-loading>
<ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end" *ngIf="canAdd">
<ion-fab-button (click)="gotoAddEntries()" [attr.aria-label]="'addon.mod_data.addentries' | translate">
<ion-icon name="fas-plus"></ion-icon>
</ion-fab-button>
</ion-fab>
</ion-content>

View File

@ -0,0 +1,556 @@
// (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 { ContextLevel } from '@/core/constants';
import { Component, OnDestroy, OnInit, Optional, Type } from '@angular/core';
import { Params } from '@angular/router';
import { CoreCommentsProvider } from '@features/comments/services/comments';
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
import { CoreCourseModule } from '@features/course/course.module';
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
import { CoreCourse } from '@features/course/services/course';
import { CoreRatingProvider } from '@features/rating/services/rating';
import { CoreRatingSyncProvider } from '@features/rating/services/rating-sync';
import { IonContent } from '@ionic/angular';
import { CoreGroupInfo, CoreGroups } from '@services/groups';
import { CoreNavigator } from '@services/navigator';
import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreTimeUtils } from '@services/utils/time';
import { CoreUtils } from '@services/utils/utils';
import { ModalController } from '@singletons';
import { CoreEventObserver, CoreEvents } from '@singletons/events';
import {
AddonModDataProvider,
AddonModData,
AddonModDataEntry,
AddonModDataTemplateType,
AddonModDataTemplateMode,
AddonModDataField,
AddonModDataGetDataAccessInformationWSResponse,
AddonModDataData,
AddonModDataSearchEntriesAdvancedField,
} from '../../services/data';
import { AddonModDataHelper } from '../../services/data-helper';
import { AddonModDataAutoSyncData, AddonModDataSyncProvider, AddonModDataSyncResult } from '../../services/data-sync';
import { AddonModDataPrefetchHandler } from '../../services/handlers/prefetch';
import { AddonModDataComponentsCompileModule } from '../components-compile.module';
import { AddonModDataSearchComponent } from '../search/search';
/**
* Component that displays a data index page.
*/
@Component({
selector: 'addon-mod-data-index',
templateUrl: 'addon-mod-data-index.html',
styleUrls: ['../../data.scss'],
})
export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComponent implements OnInit, OnDestroy {
component = AddonModDataProvider.COMPONENT;
moduleName = 'data';
access?: AddonModDataGetDataAccessInformationWSResponse;
database?: AddonModDataData;
fields: Record<number, AddonModDataField> = {};
selectedGroup = 0;
timeAvailableFrom?: number;
timeAvailableFromReadable?: string;
timeAvailableTo?: number;
timeAvailableToReadable?: string;
isEmpty = true;
groupInfo?: CoreGroupInfo;
entries: AddonModDataEntry[] = [];
firstEntry?: number;
canAdd = false;
canSearch = false;
search: AddonModDataSearchDataParams = {
sortBy: '0',
sortDirection: 'DESC',
page: 0,
text: '',
searching: false,
searchingAdvanced: false,
advanced: [],
};
hasNextPage = false;
entriesRendered = '';
extraImports: Type<unknown>[] = [AddonModDataComponentsCompileModule];
jsData? : {
fields: Record<number, AddonModDataField>;
entries: Record<number, AddonModDataEntry>;
database: AddonModDataData;
module: CoreCourseModule;
group: number;
gotoEntry: (a: number) => void;
};
// Data for found records translation.
foundRecordsTranslationData? : {
num: number;
max: number;
reseturl: string;
};;
hasOfflineRatings = false;
protected syncEventName = AddonModDataSyncProvider.AUTO_SYNCED;
protected hasComments = false;
protected fieldsArray: AddonModDataField[] = [];
protected entryChangedObserver?: CoreEventObserver;
protected ratingOfflineObserver?: CoreEventObserver;
protected ratingSyncObserver?: CoreEventObserver;
constructor(
protected content?: IonContent,
@Optional() courseContentsPage?: CoreCourseContentsPage,
) {
super('AddonModDataIndexComponent', content, courseContentsPage);
}
/**
* @inheritdoc
*/
async ngOnInit(): Promise<void> {
await super.ngOnInit();
this.selectedGroup = this.group || 0;
// Refresh entries on change.
this.entryChangedObserver = CoreEvents.on(AddonModDataProvider.ENTRY_CHANGED, (eventData) => {
if (this.database?.id == eventData.dataId) {
this.loaded = false;
return this.loadContent(true);
}
}, this.siteId);
// Listen for offline ratings saved and synced.
this.ratingOfflineObserver = CoreEvents.on(CoreRatingProvider.RATING_SAVED_EVENT, (data) => {
if (data.component == 'mod_data' && data.ratingArea == 'entry' && data.contextLevel == ContextLevel.MODULE
&& data.instanceId == this.database?.coursemodule) {
this.hasOfflineRatings = true;
}
});
this.ratingSyncObserver = CoreEvents.on(CoreRatingSyncProvider.SYNCED_EVENT, (data) => {
if (data.component == 'mod_data' && data.ratingArea == 'entry' && data.contextLevel == ContextLevel.MODULE
&& data.instanceId == this.database?.coursemodule) {
this.hasOfflineRatings = false;
}
});
await this.loadContent(false, true);
await this.logView(true);
}
/**
* Perform the invalidate content function.
*
* @return Resolved when done.
*/
protected async invalidateContent(): Promise<void> {
const promises: Promise<void>[] = [];
promises.push(AddonModData.invalidateDatabaseData(this.courseId));
if (this.database) {
promises.push(AddonModData.invalidateDatabaseAccessInformationData(this.database.id));
promises.push(CoreGroups.invalidateActivityGroupInfo(this.database.coursemodule));
promises.push(AddonModData.invalidateEntriesData(this.database.id));
promises.push(AddonModData.invalidateFieldsData(this.database.id));
if (this.hasComments) {
CoreEvents.trigger(CoreCommentsProvider.REFRESH_COMMENTS_EVENT, {
contextLevel: ContextLevel.MODULE,
instanceId: this.database.coursemodule,
}, CoreSites.getCurrentSiteId());
}
}
await Promise.all(promises);
}
/**
* Compares sync event data with current data to check if refresh content is needed.
*
* @param syncEventData Data receiven on sync observer.
* @return True if refresh is needed, false otherwise.
*/
protected isRefreshSyncNeeded(syncEventData: AddonModDataAutoSyncData): boolean {
if (this.database && syncEventData.dataId == this.database.id && typeof syncEventData.entryId == 'undefined') {
this.loaded = false;
// Refresh the data.
this.content?.scrollToTop();
return true;
}
return false;
}
/**
* Download data contents.
*
* @param refresh If it's refreshing content.
* @param sync If it should try to sync.
* @param showErrors If show errors to the user of hide them.
* @return Promise resolved when done.
*/
protected async fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise<void> {
let canAdd = false;
let canSearch = false;
this.database = await AddonModData.getDatabase(this.courseId, this.module.id);
this.hasComments = this.database.comments;
this.description = this.database.intro;
this.dataRetrieved.emit(this.database);
if (sync) {
// Try to synchronize the data.
await CoreUtils.ignoreErrors(this.syncActivity(showErrors));
}
this.groupInfo = await CoreGroups.getActivityGroupInfo(this.database.coursemodule);
this.selectedGroup = CoreGroups.validateGroupId(this.selectedGroup, this.groupInfo);
this.access = await AddonModData.getDatabaseAccessInformation(this.database.id, {
cmId: this.module.id,
groupId: this.selectedGroup,
});
if (!this.access.timeavailable) {
const time = CoreTimeUtils.timestamp();
this.timeAvailableFrom = this.database.timeavailablefrom && time < this.database.timeavailablefrom
? this.database.timeavailablefrom * 1000
: undefined;
this.timeAvailableFromReadable = this.timeAvailableFrom
? CoreTimeUtils.userDate(this.timeAvailableFrom)
: undefined;
this.timeAvailableTo = this.database.timeavailableto && time > this.database.timeavailableto
? this.database.timeavailableto * 1000
: undefined;
this.timeAvailableToReadable = this.timeAvailableTo
? CoreTimeUtils.userDate(this.timeAvailableTo)
: undefined;
this.isEmpty = true;
this.groupInfo = undefined;
} else {
canSearch = true;
canAdd = this.access.canaddentry;
}
const fields = await AddonModData.getFields(this.database.id, { cmId: this.module.id });
this.search.advanced = [];
this.fields = CoreUtils.arrayToObject(fields, 'id');
this.fieldsArray = CoreUtils.objectToArray(this.fields);
if (this.fieldsArray.length == 0) {
canSearch = false;
canAdd = false;
}
try {
await this.fetchEntriesData();
} finally {
this.canAdd = canAdd;
this.canSearch = canSearch;
this.fillContextMenu(refresh);
}
}
/**
* Fetch current database entries.
*
* @return Resolved then done.
*/
protected async fetchEntriesData(): Promise<void> {
const search = this.search.searching && !this.search.searchingAdvanced ? this.search.text : undefined;
const advSearch = this.search.searching && this.search.searchingAdvanced ? this.search.advanced : undefined;
const entries = await AddonModDataHelper.fetchEntries(this.database!, this.fieldsArray, {
groupId: this.selectedGroup,
search,
advSearch,
sort: Number(this.search.sortBy),
order: this.search.sortDirection,
page: this.search.page,
cmId: this.module.id,
});
const numEntries = entries.entries.length;
const numOfflineEntries = entries.offlineEntries?.length || 0;
this.isEmpty = !numEntries && !numOfflineEntries;
this.hasNextPage = numEntries >= AddonModDataProvider.PER_PAGE && ((this.search.page + 1) *
AddonModDataProvider.PER_PAGE) < entries.totalcount;
this.hasOffline = entries.hasOfflineActions;
this.hasOfflineRatings = !!entries.hasOfflineRatings;
this.entriesRendered = '';
this.foundRecordsTranslationData = typeof entries.maxcount != 'undefined'
? {
num: entries.totalcount,
max: entries.maxcount,
reseturl: '#',
}
: undefined;
if (!this.isEmpty) {
this.entries = (entries.offlineEntries || []).concat(entries.entries);
let entriesHTML = AddonModDataHelper.getTemplate(
this.database!,
AddonModDataTemplateType.LIST_HEADER,
this.fieldsArray,
);
// Get first entry from the whole list.
if (!this.search.searching || !this.firstEntry) {
this.firstEntry = this.entries[0].id;
}
const template = AddonModDataHelper.getTemplate(this.database!, AddonModDataTemplateType.LIST, this.fieldsArray);
const entriesById: Record<number, AddonModDataEntry> = {};
this.entries.forEach((entry, index) => {
entriesById[entry.id] = entry;
const actions = AddonModDataHelper.getActions(this.database!, this.access!, entry);
const offset = this.search.searching
? 0
: this.search.page * AddonModDataProvider.PER_PAGE + index - numOfflineEntries;
entriesHTML += AddonModDataHelper.displayShowFields(
template,
this.fieldsArray,
entry,
offset,
AddonModDataTemplateMode.LIST,
actions,
);
});
entriesHTML += AddonModDataHelper.getTemplate(this.database!, AddonModDataTemplateType.LIST_FOOTER, this.fieldsArray);
this.entriesRendered = CoreDomUtils.fixHtml(entriesHTML);
// Pass the input data to the component.
this.jsData = {
fields: this.fields,
entries: entriesById,
database: this.database!,
module: this.module,
group: this.selectedGroup,
gotoEntry: this.gotoEntry.bind(this),
};
} else if (!this.search.searching) {
// Empty and no searching.
this.canSearch = false;
this.firstEntry = undefined;
} else {
this.firstEntry = undefined;
}
}
/**
* Display the chat users modal.
*/
async showSearch(): Promise<void> {
const modal = await ModalController.create({
component: AddonModDataSearchComponent,
componentProps: {
search: this.search,
fields: this.fields,
database: this.database,
},
});
await modal.present();
const result = await modal.onDidDismiss();
// Add data to search object.
if (result.data) {
this.search = result.data;
this.searchEntries(0);
}
}
/**
* Performs the search and closes the modal.
*
* @param page Page number.
* @return Resolved when done.
*/
async searchEntries(page: number): Promise<void> {
this.loaded = false;
this.search.page = page;
try {
await this.fetchEntriesData();
// Log activity view for coherence with Moodle web.
await this.logView();
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'core.course.errorgetmodule', true);
} finally {
this.loaded = true;
}
}
/**
* Reset all search filters and closes the modal.
*/
searchReset(): void {
this.search.sortBy = '0';
this.search.sortDirection = 'DESC';
this.search.text = '';
this.search.advanced = [];
this.search.searchingAdvanced = false;
this.search.searching = false;
this.searchEntries(0);
}
/**
* Set group to see the database.
*
* @param groupId Group ID.
* @return Resolved when new group is selected or rejected if not.
*/
async setGroup(groupId: number): Promise<void> {
this.selectedGroup = groupId;
this.search.page = 0;
// Only update canAdd if there's any field, otheerwise, canAdd will remain false.
if (this.fieldsArray.length > 0) {
// Update values for current group.
this.access = await AddonModData.getDatabaseAccessInformation(this.database!.id, {
groupId: this.selectedGroup,
cmId: this.module.id,
});
this.canAdd = this.access.canaddentry;
}
try {
await this.fetchEntriesData();
// Log activity view for coherence with Moodle web.
return this.logView();
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'core.course.errorgetmodule', true);
}
}
/**
* Opens add entries form.
*/
gotoAddEntries(): void {
const params: Params = {
module: this.module,
courseId: this.courseId,
group: this.selectedGroup,
};
CoreNavigator.navigate('edit', { params });
}
/**
* Goto the selected entry.
*
* @param entryId Entry ID.
*/
gotoEntry(entryId: number): void {
const params: Params = {
module: this.module,
courseId: this.courseId,
group: this.selectedGroup,
};
// Try to find page number and offset of the entry.
const pageXOffset = this.entries.findIndex((entry) => entry.id == entryId);
if (pageXOffset >= 0) {
params.offset = this.search.page * AddonModDataProvider.PER_PAGE + pageXOffset;
}
CoreNavigator.navigate(String(entryId), { params });
}
/**
* Performs the sync of the activity.
*
* @return Promise resolved when done.
*/
protected async sync(): Promise<void> {
await AddonModDataPrefetchHandler.sync(this.module, this.courseId);
}
/**
* Checks if sync has succeed from result sync data.
*
* @param result Data returned on the sync function.
* @return If suceed or not.
*/
protected hasSyncSucceed(result: AddonModDataSyncResult): boolean {
return result.updated;
}
/**
* Log viewing the activity.
*
* @param checkCompletion Whether to check completion.
* @return Promise resolved when done.
*/
protected async logView(checkCompletion = false): Promise<void> {
if (!this.database || !this.database.id) {
return;
}
try {
await AddonModData.logView(this.database.id, this.database.name);
if (checkCompletion) {
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
}
} catch {
// Ignore errors, the user could be offline.
}
}
/**
* @inheritdoc
*/
ngOnDestroy(): void {
super.ngOnDestroy();
this.entryChangedObserver?.off();
this.ratingOfflineObserver?.off();
this.ratingSyncObserver?.off();
}
}
export type AddonModDataSearchDataParams = {
sortBy: string;
sortDirection: string;
page: number;
text: string;
searching: boolean;
searchingAdvanced: boolean;
advanced?: AddonModDataSearchEntriesAdvancedField[];
};

View File

@ -0,0 +1,68 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
</ion-buttons>
<ion-title>{{ 'addon.mod_data.search' | translate }}</ion-title>
<ion-buttons slot="end">
<ion-button (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
<ion-icon name="fas-times" slot="icon-only"></ion-icon>
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-item>
<ion-label>{{ 'addon.mod_data.advancedsearch' | translate }}</ion-label>
<ion-toggle [(ngModel)]="search.searchingAdvanced"></ion-toggle>
</ion-item>
<form (ngSubmit)="searchEntries($event)" [formGroup]="searchForm" #searchFormEl>
<ion-list class="ion-no-margin">
<ion-item [hidden]="search.searchingAdvanced">
<ion-label></ion-label>
<ion-input type="text" placeholder="{{ 'addon.mod_data.search' | translate}}"
[(ngModel)]="search.text" name="text" formControlName="text">
</ion-input>
</ion-item>
<ion-item class="ion-text-wrap">
<ion-label position="stacked">{{ 'core.sortby' | translate }}</ion-label>
<ion-select interface="action-sheet" name="sortBy" formControlName="sortBy"
[placeholder]="'core.sortby' | translate">
<optgroup *ngIf="fieldsArray.length" label="{{ 'addon.mod_data.fields' | translate }}">
<ion-select-option *ngFor="let field of fieldsArray" [value]="field.id">{{field.name}}</ion-select-option>
</optgroup>
<optgroup label="{{ 'addon.mod_data.other' | translate }}">
<ion-select-option value="0">{{ 'addon.mod_data.timeadded' | translate }}</ion-select-option>
<ion-select-option value="-4">{{ 'addon.mod_data.timemodified' | translate }}</ion-select-option>
<ion-select-option value="-1">{{ 'addon.mod_data.authorfirstname' | translate }}</ion-select-option>
<ion-select-option value="-2">{{ 'addon.mod_data.authorlastname' | translate }}</ion-select-option>
<ion-select-option value="-3" *ngIf="database.approval">
{{ 'addon.mod_data.approved' | translate }}
</ion-select-option>
</optgroup>
</ion-select>
</ion-item>
<ion-list >
<ion-radio-group [(ngModel)]="search.sortDirection" name="sortDirection" formControlName="sortDirection">
<ion-item>
<ion-label>{{ 'addon.mod_data.ascending' | translate }}</ion-label>
<ion-radio slot="start" value="ASC"></ion-radio>
</ion-item>
<ion-item>
<ion-label>{{ 'addon.mod_data.descending' | translate }}</ion-label>
<ion-radio slot="start" value="DESC"></ion-radio>
</ion-item>
</ion-radio-group>
</ion-list>
<div class="ion-padding addon-data-advanced-search" [hidden]="!advancedSearch || !search.searchingAdvanced">
<core-compile-html [text]="advancedSearch" [jsData]="jsData" [extraImports]="extraImports"></core-compile-html>
</div>
</ion-list>
<div class="ion-padding">
<ion-button expand="block" type="submit">
<ion-icon name="fas-search" slot="start"></ion-icon>
{{ 'addon.mod_data.search' | translate }}
</ion-button>
</div>
</form>
</ion-content>

View File

@ -0,0 +1,216 @@
// (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, ElementRef, Input, OnInit, Type, ViewChild } from '@angular/core';
import { FormGroup, FormBuilder } from '@angular/forms';
import { CoreTag } from '@features/tag/services/tag';
import { CoreSites } from '@services/sites';
import { CoreFormFields, CoreForms } from '@singletons/form';
import { CoreTextUtils } from '@services/utils/text';
import { CoreUtils } from '@services/utils/utils';
import { ModalController } from '@singletons';
import {
AddonModDataField,
AddonModDataData,
AddonModDataTemplateType,
AddonModDataSearchEntriesAdvancedField,
} from '../../services/data';
import { AddonModDataFieldsDelegate } from '../../services/data-fields-delegate';
import { AddonModDataHelper } from '../../services/data-helper';
import { AddonModDataComponentsCompileModule } from '../components-compile.module';
import { AddonModDataSearchDataParams } from '../index';
/**
* Page that displays the search modal.
*/
@Component({
selector: 'addon-mod-data-search-modal',
templateUrl: 'search.html',
styleUrls: ['../../data.scss', '../../data-forms.scss'],
})
export class AddonModDataSearchComponent implements OnInit {
@ViewChild('searchFormEl') formElement!: ElementRef;
@Input() search!: AddonModDataSearchDataParams;
@Input() fields!: Record<number, AddonModDataField>;
@Input() database!: AddonModDataData;
advancedSearch = '';
advancedIndexed: CoreFormFields = {};
extraImports: Type<unknown>[] = [AddonModDataComponentsCompileModule];
searchForm: FormGroup;
jsData? : {
fields: Record<number, AddonModDataField>;
form: FormGroup;
search: CoreFormFields;
};
fieldsArray: AddonModDataField[] = [];
constructor(
protected fb: FormBuilder,
) {
this.searchForm = new FormGroup({});
}
ngOnInit(): void {
this.advancedIndexed = {};
this.search.advanced?.forEach((field) => {
if (typeof field != 'undefined') {
this.advancedIndexed[field.name] = field.value
? CoreTextUtils.parseJSON(field.value)
: '';
}
});
this.searchForm.addControl('text', this.fb.control(this.search.text || ''));
this.searchForm.addControl('sortBy', this.fb.control(this.search.sortBy || '0'));
this.searchForm.addControl('sortDirection', this.fb.control(this.search.sortDirection || 'DESC'));
this.searchForm.addControl('firstname', this.fb.control(this.advancedIndexed['firstname'] || ''));
this.searchForm.addControl('lastname', this.fb.control(this.advancedIndexed['lastname'] || ''));
this.fieldsArray = CoreUtils.objectToArray(this.fields);
this.advancedSearch = this.renderAdvancedSearchFields();
}
/**
* Displays Advanced Search Fields.
*
* @return Generated HTML.
*/
protected renderAdvancedSearchFields(): string {
this.jsData = {
fields: this.fields,
form: this.searchForm,
search: this.advancedIndexed,
};
let template = AddonModDataHelper.getTemplate(this.database, AddonModDataTemplateType.SEARCH, this.fieldsArray);
// Replace the fields found on template.
this.fieldsArray.forEach((field) => {
let replace = '[[' + field.name + ']]';
replace = replace.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&');
const replaceRegex = new RegExp(replace, 'gi');
// Replace field by a generic directive.
const render = '<addon-mod-data-field-plugin mode="search" [field]="fields[' + field.id +
']" [form]="form" [searchFields]="search"></addon-mod-data-field-plugin>';
template = template.replace(replaceRegex, render);
});
// Not pluginable other search elements.
// Replace firstname field by the text input.
let replaceRegex = new RegExp('##firstname##', 'gi');
let render = '<span [formGroup]="form"><ion-input type="text" name="firstname" \
[placeholder]="\'addon.mod_data.authorfirstname\' | translate" formControlName="firstname"></ion-input></span>';
template = template.replace(replaceRegex, render);
// Replace lastname field by the text input.
replaceRegex = new RegExp('##lastname##', 'gi');
render = '<span [formGroup]="form"><ion-input type="text" name="lastname" \
[placeholder]="\'addon.mod_data.authorlastname\' | translate" formControlName="lastname"></ion-input></span>';
template = template.replace(replaceRegex, render);
// Searching by tags is not supported.
replaceRegex = new RegExp('##tags##', 'gi');
const message = CoreTag.areTagsAvailableInSite() ?
'<p class="item-dimmed">{{ \'addon.mod_data.searchbytagsnotsupported\' | translate }}</p>'
: '';
template = template.replace(replaceRegex, message);
return template;
}
/**
* Retrieve the entered data in search in a form.
*
* @param searchedData Array with the entered form values.
* @return Array with the answers.
*/
getSearchDataFromForm(searchedData: CoreFormFields): AddonModDataSearchEntriesAdvancedField[] {
const advancedSearch: AddonModDataSearchEntriesAdvancedField[] = [];
// Filter and translate fields to each field plugin.
this.fieldsArray.forEach((field) => {
const fieldData = AddonModDataFieldsDelegate.getFieldSearchData(field, searchedData);
fieldData.forEach((data) => {
// WS wants values in Json format.
advancedSearch.push({
name: data.name,
value: JSON.stringify(data.value),
});
});
});
// Not pluginable other search elements.
if (searchedData.firstname) {
// WS wants values in Json format.
advancedSearch.push({
name: 'firstname',
value: JSON.stringify(searchedData.firstname),
});
}
if (searchedData.lastname) {
// WS wants values in Json format.
advancedSearch.push({
name: 'lastname',
value: JSON.stringify(searchedData.lastname),
});
}
return advancedSearch;
}
/**
* Close modal.
*/
closeModal(): void {
CoreForms.triggerFormCancelledEvent(this.formElement, CoreSites.getCurrentSiteId());
ModalController.dismiss();
}
/**
* Done editing.
*
* @param e Event.
*/
searchEntries(e: Event): void {
e.preventDefault();
e.stopPropagation();
const searchedData = this.searchForm.value;
if (this.search.searchingAdvanced) {
this.search.advanced = this.getSearchDataFromForm(searchedData);
this.search.searching = this.search.advanced.length > 0;
} else {
this.search.text = searchedData.text;
this.search.searching = this.search.text.length > 0;
}
this.search.sortBy = searchedData.sortBy;
this.search.sortDirection = searchedData.sortDirection;
CoreForms.triggerFormSubmittedEvent(this.formElement, false, CoreSites.getCurrentSiteId());
ModalController.dismiss(this.search);
}
}

View File

@ -0,0 +1,106 @@
@import "~theme/globals";
// Edit and search modal.
:host {
--input-border-color: var(--gray);
--input-border-width: 1px;
--select-border-width: 0;
::ng-deep {
table {
width: 100%;
}
td {
vertical-align: top;
}
.addon-data-latlong {
display: flex;
}
}
.addon-data-advanced-search {
padding: 16px;
width: 100%;
// @todo check if needed
// @include safe-area-padding-horizontal(16px !important, 16px !important);
}
.addon-data-contents form,
form .addon-data-advanced-search {
background-color: var(--ion-item-background);
::ng-deep {
ion-input {
border-bottom: var(--input-border-width) solid var(--input-border-color);
&.has-focus,
&.has-focus.ion-valid,
&.ion-touched.ion-invalid {
--input-border-width: 2px;
}
&.has-focus {
--input-border-color: var(--core-color);
}
&.has-focus.ion-valid {
--input-border-color: var(--success);
}
&.ion-touched.ion-invalid {
--input-border-color: var(--danger);
}
}
core-rich-text-editor {
border-bottom: var(--select-border-width) solid var(--input-border-color);
&.ion-touched.ng-valid,
&.ion-touched.ng-invalid {
--select-border-width: 2px;
}
&.ion-touched.ng-valid {
--input-border-color: var(--success);
}
&.ion-touched.ng-invalid {
--input-border-color: var(--danger);
}
}
ion-select {
border-bottom: var(--select-border-width) solid var(--input-border-color);
&.ion-touched.ion-valid,
&.ion-touched.ion-invalid {
--select-border-width: 2px;
}
&.ion-touched.ion-valid {
--input-border-color: var(--success);
}
&.ion-touched.ion-invalid {
--input-border-color: var(--danger);
}
}
.has-errors ion-input.ion-invalid {
--input-border-width: 2px;
--input-border-color: var(--danger);
}
.has-errors ion-select.ion-invalid,
.has-errors core-rich-text-editor.ng-invalid {
--select-border-width: 2px;
--input-border-color: var(--danger);
}
.core-mark-required {
@include float(end);
+ ion-input,
+ ion-select {
@include padding(null, 20px, null, null);
}
}
}
}
}

View File

@ -0,0 +1,65 @@
// (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 { CoreSharedModule } from '@/core/shared.module';
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { CoreCommentsComponentsModule } from '@features/comments/components/components.module';
import { CoreCompileHtmlComponentModule } from '@features/compile/components/compile-html/compile-html.module';
import { CoreRatingComponentsModule } from '@features/rating/components/components.module';
import { CanLeaveGuard } from '@guards/can-leave';
import { AddonModDataComponentsCompileModule } from './components/components-compile.module';
import { AddonModDataComponentsModule } from './components/components.module';
import { AddonModDataEditPage } from './pages/edit/edit';
import { AddonModDataEntryPage } from './pages/entry/entry';
import { AddonModDataIndexPage } from './pages/index/index';
const routes: Routes = [
{
path: ':courseId/:cmId',
component: AddonModDataIndexPage,
},
{
path: ':courseId/:cmId/edit',
component: AddonModDataEditPage,
canDeactivate: [CanLeaveGuard],
},
{
path: ':courseId/:cmId/edit/:entryId',
component: AddonModDataEditPage,
canDeactivate: [CanLeaveGuard],
},
{
path: ':courseId/:cmId/:entryId',
component: AddonModDataEntryPage,
},
];
@NgModule({
imports: [
RouterModule.forChild(routes),
CoreSharedModule,
AddonModDataComponentsModule,
AddonModDataComponentsCompileModule,
CoreCommentsComponentsModule,
CoreRatingComponentsModule,
CoreCompileHtmlComponentModule,
],
declarations: [
AddonModDataIndexPage,
AddonModDataEntryPage,
AddonModDataEditPage,
],
})
export class AddonModDataLazyModule {}

View File

@ -0,0 +1,88 @@
// (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 { APP_INITIALIZER, NgModule, Type } from '@angular/core';
import { Routes } from '@angular/router';
import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate';
import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate';
import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module';
import { CoreTagAreaDelegate } from '@features/tag/services/tag-area-delegate';
import { CoreCronDelegate } from '@services/cron';
import { CORE_SITE_SCHEMAS } from '@services/sites';
import { AddonModDataProvider } from './services/data';
import { AddonModDataFieldsDelegateService } from './services/data-fields-delegate';
import { AddonModDataHelperProvider } from './services/data-helper';
import { AddonModDataOfflineProvider } from './services/data-offline';
import { AddonModDataSyncProvider } from './services/data-sync';
import { ADDON_MOD_DATA_OFFLINE_SITE_SCHEMA } from './services/database/data';
import { AddonModDataApproveLinkHandler } from './services/handlers/approve-link';
import { AddonModDataDeleteLinkHandler } from './services/handlers/delete-link';
import { AddonModDataEditLinkHandler } from './services/handlers/edit-link';
import { AddonModDataIndexLinkHandler } from './services/handlers/index-link';
import { AddonModDataListLinkHandler } from './services/handlers/list-link';
import { AddonModDataModuleHandler, AddonModDataModuleHandlerService } from './services/handlers/module';
import { AddonModDataPrefetchHandler } from './services/handlers/prefetch';
import { AddonModDataShowLinkHandler } from './services/handlers/show-link';
import { AddonModDataSyncCronHandler } from './services/handlers/sync-cron';
import { AddonModDataTagAreaHandler } from './services/handlers/tag-area';
import { AddonModDataFieldModule } from './fields/field.module';
// List of providers (without handlers).
export const ADDON_MOD_DATA_SERVICES: Type<unknown>[] = [
AddonModDataProvider,
AddonModDataHelperProvider,
AddonModDataSyncProvider,
AddonModDataOfflineProvider,
AddonModDataFieldsDelegateService,
];
const routes: Routes = [
{
path: AddonModDataModuleHandlerService.PAGE_NAME,
loadChildren: () => import('./data-lazy.module').then(m => m.AddonModDataLazyModule),
},
];
@NgModule({
imports: [
CoreMainMenuTabRoutingModule.forChild(routes),
AddonModDataFieldModule,
],
providers: [
{
provide: CORE_SITE_SCHEMAS,
useValue: [ADDON_MOD_DATA_OFFLINE_SITE_SCHEMA],
multi: true,
},
{
provide: APP_INITIALIZER,
multi: true,
deps: [],
useFactory: () => () => {
CoreCourseModuleDelegate.registerHandler(AddonModDataModuleHandler.instance);
CoreCourseModulePrefetchDelegate.registerHandler(AddonModDataPrefetchHandler.instance);
CoreCronDelegate.register(AddonModDataSyncCronHandler.instance);
CoreContentLinksDelegate.registerHandler(AddonModDataIndexLinkHandler.instance);
CoreContentLinksDelegate.registerHandler(AddonModDataListLinkHandler.instance);
CoreContentLinksDelegate.registerHandler(AddonModDataApproveLinkHandler.instance);
CoreContentLinksDelegate.registerHandler(AddonModDataDeleteLinkHandler.instance);
CoreContentLinksDelegate.registerHandler(AddonModDataShowLinkHandler.instance);
CoreContentLinksDelegate.registerHandler(AddonModDataEditLinkHandler.instance);
CoreTagAreaDelegate.registerHandler(AddonModDataTagAreaHandler.instance);
},
},
],
})
export class AddonModDataModule {}

View File

@ -0,0 +1,70 @@
@import "~theme/globals";
/// @prop - The padding for the grid column
$grid-column-padding: var(--ion-grid-column-padding, 5px) !default;
/// @prop - The padding for the column at different breakpoints
$grid-column-paddings: (
xs: var(--ion-grid-column-padding-xs, $grid-column-padding),
sm: var(--ion-grid-column-padding-sm, $grid-column-padding),
md: var(--ion-grid-column-padding-md, $grid-column-padding),
lg: var(--ion-grid-column-padding-lg, $grid-column-padding),
xl: var(--ion-grid-column-padding-xl, $grid-column-padding)
) !default;
.addon-data-contents {
overflow: visible;
white-space: normal;
word-break: break-word;
padding: 16px;
// @todo check if needed
// @include safe-area-padding-horizontal(16px !important, 16px !important);
background-color: var(--ion-item-background);
border-width: 1px 0;
border-style: solid;
border-color: var(--gray-dark);
::ng-deep {
table, tbody {
display: block;
}
tr {
// Imported form ion-row;
display: flex;
flex-wrap: wrap;
padding: 0;
@include media-breakpoint-down(sm) {
flex-direction: column;
}
}
td, th {
// Imported form ion-col;
@include make-breakpoint-padding($grid-column-paddings);
@include margin(0);
box-sizing: border-box;
position: relative;
flex-basis: 0;
flex-grow: 1;
width: 100%;
max-width: 100%;
min-height: auto;
}
// Do not let block elements to define widths or heights.
address, article, aside, blockquote, canvas, dd, div, dl, dt, fieldset, figcaption, figure, footer, form,
h1, h2, h3, h4, h5, h6,
header, hr, li, main, nav, noscript, ol, p, pre, section, table, tfoot, ul, video {
width: auto !important;
height: auto !important;
min-width: auto !important;
min-height: auto !important;
// Avoid having one entry over another.
max-height: none !important;
}
}
}

View File

@ -0,0 +1,42 @@
// (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 { APP_INITIALIZER, NgModule } from '@angular/core';
import { AddonModDataFieldCheckboxComponent } from './component/checkbox';
import { CoreSharedModule } from '@/core/shared.module';
import { AddonModDataFieldsDelegate } from '../../services/data-fields-delegate';
import { AddonModDataFieldCheckboxHandler } from './services/handler';
@NgModule({
declarations: [
AddonModDataFieldCheckboxComponent,
],
imports: [
CoreSharedModule,
],
providers: [
{
provide: APP_INITIALIZER,
multi: true,
deps: [],
useFactory: () => () => {
AddonModDataFieldsDelegate.registerHandler(AddonModDataFieldCheckboxHandler.instance);
},
},
],
exports: [
AddonModDataFieldCheckboxComponent,
],
})
export class AddonModDataFieldCheckboxModule {}

View File

@ -0,0 +1,16 @@
<span *ngIf="inputMode && form" [formGroup]="form">
<span *ngIf="editMode" [core-mark-required]="field.required" class="core-mark-required"></span>
<ion-select [formControlName]="'f_'+field.id" multiple="true" [placeholder]="'addon.mod_data.menuchoose' | translate"
[interfaceOptions]="{header: field.name}" interface="alert">
<ion-select-option *ngFor="let option of options" [value]="option.value">{{option.key}}</ion-select-option>
</ion-select>
<core-input-errors *ngIf="error && editMode" [control]="form.controls['f_'+field.id]" [errorText]="error"></core-input-errors>
<ion-item *ngIf="searchMode">
<ion-label>{{ 'addon.mod_data.selectedrequired' | translate }}</ion-label>
<ion-checkbox slot="end" [formControlName]="'f_'+field.id+'_allreq'" [(ngModel)]="searchFields!['f_'+field.id+'_allreq']">
</ion-checkbox>
</ion-item>
</span>
<core-format-text *ngIf="displayMode && value && value.content" [text]="value.content" [filter]="false"></core-format-text>

View File

@ -0,0 +1,70 @@
// (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 { AddonModDataEntryField } from '@addons/mod/data/services/data';
import { Component } from '@angular/core';
import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin-component';
/**
* Component to render data checkbox field.
*/
@Component({
selector: 'addon-mod-data-field-checkbox',
templateUrl: 'addon-mod-data-field-checkbox.html',
})
export class AddonModDataFieldCheckboxComponent extends AddonModDataFieldPluginComponent {
options: {
key: string;
value: string;
}[] = [];
/**
* @inheritdoc
*/
protected init(): void {
if (this.displayMode) {
this.updateValue(this.value);
return;
}
this.options = this.field.param1.split(/\r?\n/).map((option) => ({ key: option, value: option }));
const values: string[] = [];
if (this.editMode && this.value && this.value.content) {
this.value.content.split('##').forEach((value) => {
const x = this.options.findIndex((option) => value == option.key);
if (x >= 0) {
values.push(value);
}
});
}
if (this.searchMode) {
this.addControl('f_' + this.field.id + '_allreq');
}
this.addControl('f_' + this.field.id, values);
}
/**
* @inheritdoc
*/
protected updateValue(value?: Partial<AddonModDataEntryField>): void {
this.value = value || {};
this.value.content = value?.content?.split('##').join('<br>');
}
}

View File

@ -0,0 +1,131 @@
// (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 {
AddonModDataEntryField,
AddonModDataField,
AddonModDataSearchEntriesAdvancedFieldFormatted,
AddonModDataSubfieldData,
} from '@addons/mod/data/services/data';
import { AddonModDataFieldHandler } from '@addons/mod/data/services/data-fields-delegate';
import { Injectable, Type } from '@angular/core';
import { CoreFormFields } from '@singletons/form';
import { makeSingleton, Translate } from '@singletons';
import { AddonModDataFieldCheckboxComponent } from '../component/checkbox';
/**
* Handler for checkbox data field plugin.
*/
@Injectable({ providedIn: 'root' })
export class AddonModDataFieldCheckboxHandlerService implements AddonModDataFieldHandler {
name = 'AddonModDataFieldCheckboxHandler';
type = 'checkbox';
/**
* @inheritdoc
*/
getComponent(): Type<unknown> {
return AddonModDataFieldCheckboxComponent;
}
/**
* @inheritdoc
*/
getFieldSearchData(
field: AddonModDataField,
inputData: CoreFormFields<string[]>,
): AddonModDataSearchEntriesAdvancedFieldFormatted[] {
const fieldName = 'f_' + field.id;
const reqName = 'f_' + field.id + '_allreq';
if (inputData[fieldName]) {
const values: AddonModDataSearchEntriesAdvancedFieldFormatted[] = [];
values.push({
name: fieldName,
value: inputData[fieldName],
});
if (inputData[reqName]) {
values.push({
name: reqName,
value: true,
});
}
return values;
}
return [];
}
/**
* @inheritdoc
*/
getFieldEditData(field: AddonModDataField, inputData: CoreFormFields<string[]>): AddonModDataSubfieldData[] {
const fieldName = 'f_' + field.id;
return [{
fieldid: field.id,
value: inputData[fieldName] || [],
}];
}
/**
* @inheritdoc
*/
hasFieldDataChanged(
field: AddonModDataField,
inputData: CoreFormFields<string[]>,
originalFieldData: AddonModDataEntryField,
): boolean {
const fieldName = 'f_' + field.id;
const content = originalFieldData?.content || '';
return inputData[fieldName].join('##') != content;
}
/**
* Check and get field requeriments.
*
* @param field Defines the field to be rendered.
* @param inputData Data entered in the edit form.
* @return String with the notification or false.
*/
getFieldsNotifications(field: AddonModDataField, inputData: AddonModDataSubfieldData[]): string | undefined {
if (field.required && (!inputData || !inputData.length || !inputData[0].value)) {
return Translate.instant('addon.mod_data.errormustsupplyvalue');
}
}
/**
* @inheritdoc
*/
overrideData(originalContent: AddonModDataEntryField, offlineContent: CoreFormFields<string[]>): AddonModDataEntryField {
originalContent.content = (offlineContent[''] && offlineContent[''].join('##')) || '';
return originalContent;
}
/**
* @inheritdoc
*/
async isEnabled(): Promise<boolean> {
return true;
}
}
export const AddonModDataFieldCheckboxHandler = makeSingleton(AddonModDataFieldCheckboxHandlerService);

View File

@ -0,0 +1,16 @@
<span *ngIf="inputMode && form" [formGroup]="form">
<span *ngIf="editMode" [core-mark-required]="field.required" class="core-mark-required"></span>
<ion-datetime [formControlName]="'f_'+field.id" [placeholder]="'core.date' | translate"
[disabled]="searchMode && !searchFields!['f_'+field.id+'_z']" [displayFormat]="format"></ion-datetime>
<core-input-errors *ngIf="error && editMode" [control]="form.controls['f_'+field.id]" [errorText]="error"></core-input-errors>
<ion-item *ngIf="searchMode">
<ion-label>{{ 'addon.mod_data.usedate' | translate }}</ion-label>
<ion-checkbox slot="end" [formControlName]="'f_'+field.id+'_z'" [(ngModel)]="searchFields!['f_'+field.id+'_z']">
</ion-checkbox>
</ion-item>
</span>
<span *ngIf="displayMode && displayDate">
{{ displayDate | coreFormatDate:'strftimedate' }}
</span>

View File

@ -0,0 +1,70 @@
// (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 } from '@angular/core';
import { CoreTimeUtils } from '@services/utils/time';
import { Translate } from '@singletons';
import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin-component';
/**
* Component to render data date field.
*/
@Component({
selector: 'addon-mod-data-field-date',
templateUrl: 'addon-mod-data-field-date.html',
})
export class AddonModDataFieldDateComponent extends AddonModDataFieldPluginComponent {
format!: string;
displayDate?: number;
/**
* @inheritdoc
*/
protected init(): void {
if (this.displayMode) {
return;
}
let date: Date;
// Calculate format to use.
this.format = CoreTimeUtils.fixFormatForDatetime(CoreTimeUtils.convertPHPToMoment(
Translate.instant('core.strftimedate'),
));
if (this.searchMode) {
this.addControl('f_' + this.field.id + '_z');
date = this.searchFields!['f_' + this.field.id + '_y']
? new Date(this.searchFields!['f_' + this.field.id + '_y'] + '-' +
this.searchFields!['f_' + this.field.id + '_m'] + '-' + this.searchFields!['f_' + this.field.id + '_d'])
: new Date();
this.searchFields!['f_' + this.field.id] = CoreTimeUtils.toDatetimeFormat(date.getTime());
} else {
date = this.value?.content
? new Date(parseInt(this.value.content, 10) * 1000)
: new Date();
this.displayDate = this.value?.content
? parseInt(this.value.content, 10) * 1000
: undefined;
}
this.addControl('f_' + this.field.id, CoreTimeUtils.toDatetimeFormat(date.getTime()));
}
}

View File

@ -0,0 +1,42 @@
// (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 { CoreSharedModule } from '@/core/shared.module';
import { NgModule, APP_INITIALIZER } from '@angular/core';
import { AddonModDataFieldsDelegate } from '../../services/data-fields-delegate';
import { AddonModDataFieldDateComponent } from './component/date';
import { AddonModDataFieldDateHandler } from './services/handler';
@NgModule({
declarations: [
AddonModDataFieldDateComponent,
],
imports: [
CoreSharedModule,
],
providers: [
{
provide: APP_INITIALIZER,
multi: true,
deps: [],
useFactory: () => () => {
AddonModDataFieldsDelegate.registerHandler(AddonModDataFieldDateHandler.instance);
},
},
],
exports: [
AddonModDataFieldDateComponent,
],
})
export class AddonModDataFieldDateModule {}

View File

@ -0,0 +1,165 @@
// (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 {
AddonModDataEntryField,
AddonModDataField,
AddonModDataSearchEntriesAdvancedFieldFormatted,
AddonModDataSubfieldData,
} from '@addons/mod/data/services/data';
import { AddonModDataFieldHandler } from '@addons/mod/data/services/data-fields-delegate';
import { Injectable, Type } from '@angular/core';
import { CoreFormFields } from '@singletons/form';
import { CoreTimeUtils } from '@services/utils/time';
import { makeSingleton, Translate } from '@singletons';
import { AddonModDataFieldDateComponent } from '../component/date';
/**
* Handler for date data field plugin.
*/
@Injectable({ providedIn: 'root' })
export class AddonModDataFieldDateHandlerService implements AddonModDataFieldHandler {
name = 'AddonModDataFieldDateHandler';
type = 'date';
/**
* @inheritdoc
*/
getComponent(): Type<unknown>{
return AddonModDataFieldDateComponent;
}
/**
* @inheritdoc
*/
getFieldSearchData(
field: AddonModDataField,
inputData: CoreFormFields<string>,
): AddonModDataSearchEntriesAdvancedFieldFormatted[] {
const fieldName = 'f_' + field.id;
const enabledName = 'f_' + field.id + '_z';
if (inputData[enabledName] && typeof inputData[fieldName] == 'string') {
const date = inputData[fieldName].substr(0, 10).split('-');
return [
{
name: fieldName + '_y',
value: date[0],
},
{
name: fieldName + '_m',
value: date[1],
},
{
name: fieldName + '_d',
value: date[2],
},
{
name: enabledName,
value: 1,
},
];
}
return [];
}
/**
* @inheritdoc
*/
getFieldEditData(field: AddonModDataField, inputData: CoreFormFields<string>): AddonModDataSubfieldData[] {
const fieldName = 'f_' + field.id;
if (typeof inputData[fieldName] != 'string') {
return [];
}
const date = inputData[fieldName].substr(0, 10).split('-');
return [
{
fieldid: field.id,
subfield: 'year',
value: date[0],
},
{
fieldid: field.id,
subfield: 'month',
value: date[1],
},
{
fieldid: field.id,
subfield: 'day',
value: date[2],
},
];
}
/**
* @inheritdoc
*/
hasFieldDataChanged(
field: AddonModDataField,
inputData: CoreFormFields<string>,
originalFieldData: AddonModDataEntryField,
): boolean {
const fieldName = 'f_' + field.id;
const input = inputData[fieldName] && inputData[fieldName].substr(0, 10) || '';
const content = (originalFieldData && originalFieldData?.content &&
CoreTimeUtils.toDatetimeFormat(parseInt(originalFieldData.content, 10) * 1000).substr(0, 10)) || '';
return input != content;
}
/**
* @inheritdoc
*/
getFieldsNotifications(field: AddonModDataField, inputData: AddonModDataSubfieldData[]): string | undefined {
if (field.required &&
(!inputData || inputData.length < 2 || !inputData[0].value || !inputData[1].value || !inputData[2].value)) {
return Translate.instant('addon.mod_data.errormustsupplyvalue');
}
}
/**
* @inheritdoc
*/
overrideData(originalContent: AddonModDataEntryField, offlineContent: CoreFormFields<string>): AddonModDataEntryField {
if (offlineContent['day']) {
let date = Date.UTC(
parseInt(offlineContent['year'], 10),
parseInt(offlineContent['month'], 10) - 1,
parseInt(offlineContent['day'], 10),
);
date = Math.floor(date / 1000);
originalContent.content = String(date) || '';
}
return originalContent;
}
/**
* @inheritdoc
*/
async isEnabled(): Promise<boolean> {
return true;
}
}
export const AddonModDataFieldDateHandler = makeSingleton(AddonModDataFieldDateHandlerService);

View File

@ -0,0 +1,45 @@
// (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 { NgModule } from '@angular/core';
import { AddonModDataFieldCheckboxModule } from './checkbox/checkbox.module';
import { AddonModDataFieldDateModule } from './date/date.module';
import { AddonModDataFieldFileModule } from './file/file.module';
import { AddonModDataFieldLatlongModule } from './latlong/latlong.module';
import { AddonModDataFieldMenuModule } from './menu/menu.module';
import { AddonModDataFieldMultimenuModule } from './multimenu/multimenu.module';
import { AddonModDataFieldNumberModule } from './number/number.module';
import { AddonModDataFieldPictureModule } from './picture/picture.module';
import { AddonModDataFieldRadiobuttonModule } from './radiobutton/radiobutton.module';
import { AddonModDataFieldTextModule } from './text/text.module';
import { AddonModDataFieldTextareaModule } from './textarea/textarea.module';
import { AddonModDataFieldUrlModule } from './url/url.module';
@NgModule({
imports: [
AddonModDataFieldCheckboxModule,
AddonModDataFieldDateModule,
AddonModDataFieldFileModule,
AddonModDataFieldLatlongModule,
AddonModDataFieldMenuModule,
AddonModDataFieldMultimenuModule,
AddonModDataFieldNumberModule,
AddonModDataFieldPictureModule,
AddonModDataFieldRadiobuttonModule,
AddonModDataFieldTextModule,
AddonModDataFieldTextareaModule,
AddonModDataFieldUrlModule,
],
})
export class AddonModDataFieldModule { }

View File

@ -0,0 +1,17 @@
<span *ngIf="editMode && form">
<span [core-mark-required]="field.required" class="core-mark-required"></span>
<core-attachments [files]="files" [maxSize]="maxSizeBytes" maxSubmissions="1" [component]="component"
[componentId]="componentId" [allowOffline]="true">
</core-attachments>
<core-input-errors *ngIf="error" [errorText]="error"></core-input-errors>
</span>
<span *ngIf="searchMode && form" [formGroup]="form">
<ion-input type="text" [formControlName]="'f_'+field.id" [placeholder]="field.name"></ion-input>
</span>
<ng-container *ngIf="displayMode">
<div lines="none">
<core-files [files]="files" [component]="component" [componentId]="componentId" [alwaysDownload]="true"></core-files>
</div>
</ng-container>

View File

@ -0,0 +1,81 @@
// (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 } from '@angular/core';
import { AddonModDataEntryField, AddonModDataProvider } from '@addons/mod/data/services/data';
import { AddonModDataFieldPluginComponent } from '@addons/mod/data/classes/field-plugin-component';
import { CoreFileSession } from '@services/file-session';
import { CoreWSExternalFile } from '@services/ws';
import { FileEntry } from '@ionic-native/file';
/**
* Component to render data file field.
*/
@Component({
selector: 'addon-mod-data-field-file',
templateUrl: 'addon-mod-data-field-file.html',
})
export class AddonModDataFieldFileComponent extends AddonModDataFieldPluginComponent {
files: (CoreWSExternalFile | FileEntry)[] = [];
component?: string;
componentId?: number;
maxSizeBytes?: number;
/**
* Get the files from the input value.
*
* @param value Input value.
* @return List of files.
*/
protected getFiles(value?: Partial<AddonModDataEntryField>): (CoreWSExternalFile | FileEntry)[] {
let files = value?.files || [];
// Reduce to first element.
if (files.length > 0) {
files = [files[0]];
}
return files;
}
/**
* @inheritdoc
*/
protected init(): void {
if (this.searchMode) {
this.addControl('f_' + this.field.id);
return;
}
this.component = AddonModDataProvider.COMPONENT;
this.componentId = this.database!.coursemodule;
this.updateValue(this.value);
if (this.editMode) {
this.maxSizeBytes = parseInt(this.field.param3, 10);
CoreFileSession.setFiles(this.component, this.database!.id + '_' + this.field.id, this.files);
}
}
/**
* @inheritdoc
*/
protected updateValue(value?: Partial<AddonModDataEntryField>): void {
this.value = value;
this.files = this.getFiles(value);
}
}

View File

@ -0,0 +1,42 @@
// (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 { CoreSharedModule } from '@/core/shared.module';
import { NgModule, APP_INITIALIZER } from '@angular/core';
import { AddonModDataFieldsDelegate } from '../../services/data-fields-delegate';
import { AddonModDataFieldFileComponent } from './component/file';
import { AddonModDataFieldFileHandler } from './services/handler';
@NgModule({
declarations: [
AddonModDataFieldFileComponent,
],
imports: [
CoreSharedModule,
],
providers: [
{
provide: APP_INITIALIZER,
multi: true,
deps: [],
useFactory: () => () => {
AddonModDataFieldsDelegate.registerHandler(AddonModDataFieldFileHandler.instance);
},
},
],
exports: [
AddonModDataFieldFileComponent,
],
})
export class AddonModDataFieldFileModule {}

View File

@ -0,0 +1,136 @@
// (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 {
AddonModDataEntryField,
AddonModDataField,
AddonModDataProvider,
AddonModDataSearchEntriesAdvancedFieldFormatted,
AddonModDataSubfieldData,
} from '@addons/mod/data/services/data';
import { AddonModDataFieldHandler } from '@addons/mod/data/services/data-fields-delegate';
import { Injectable, Type } from '@angular/core';
import { CoreFileUploader, CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader';
import { FileEntry } from '@ionic-native/file';
import { CoreFileSession } from '@services/file-session';
import { CoreFormFields } from '@singletons/form';
import { CoreWSExternalFile } from '@services/ws';
import { makeSingleton, Translate } from '@singletons';
import { AddonModDataFieldFileComponent } from '../component/file';
/**
* Handler for file data field plugin.
*/
@Injectable({ providedIn: 'root' })
export class AddonModDataFieldFileHandlerService implements AddonModDataFieldHandler {
name = 'AddonModDataFieldFileHandler';
type = 'file';
/**
* @inheritdoc
*/
getComponent(): Type<unknown>{
return AddonModDataFieldFileComponent;
}
/**
* @inheritdoc
*/
getFieldSearchData(field: AddonModDataField, inputData: CoreFormFields): AddonModDataSearchEntriesAdvancedFieldFormatted[] {
const fieldName = 'f_' + field.id;
if (inputData[fieldName]) {
return [{
name: fieldName,
value: inputData[fieldName],
}];
}
return [];
}
/**
* @inheritdoc
*/
getFieldEditData(field: AddonModDataField): AddonModDataSubfieldData[] {
const files = this.getFieldEditFiles(field);
return [{
fieldid: field.id,
subfield: 'file',
files: files,
}];
}
/**
* @inheritdoc
*/
getFieldEditFiles(field: AddonModDataField): (CoreWSExternalFile | FileEntry)[] {
return CoreFileSession.getFiles(AddonModDataProvider.COMPONENT, field.dataid + '_' + field.id);
}
/**
* @inheritdoc
*/
hasFieldDataChanged(field: AddonModDataField, inputData: CoreFormFields, originalFieldData: AddonModDataEntryField): boolean {
const files = CoreFileSession.getFiles(AddonModDataProvider.COMPONENT, field.dataid + '_' + field.id) || [];
let originalFiles = (originalFieldData && originalFieldData.files) || [];
if (originalFiles.length) {
originalFiles = [originalFiles[0]];
}
return CoreFileUploader.areFileListDifferent(files, originalFiles);
}
/**
* @inheritdoc
*/
getFieldsNotifications(field: AddonModDataField, inputData: AddonModDataSubfieldData[]): string | undefined {
if (field.required && (!inputData || !inputData.length || !inputData[0].value)) {
return Translate.instant('addon.mod_data.errormustsupplyvalue');
}
}
/**
* @inheritdoc
*/
overrideData(
originalContent: AddonModDataEntryField,
offlineContent: CoreFormFields,
offlineFiles?: FileEntry[],
): AddonModDataEntryField {
const uploadedFilesResult: CoreFileUploaderStoreFilesResult = <CoreFileUploaderStoreFilesResult>offlineContent?.file;
if (uploadedFilesResult && uploadedFilesResult.offline > 0 && offlineFiles && offlineFiles?.length > 0) {
originalContent.content = offlineFiles[0].name;
originalContent.files = [offlineFiles[0]];
} else if (uploadedFilesResult && uploadedFilesResult.online && uploadedFilesResult.online.length > 0) {
originalContent.content = uploadedFilesResult.online[0].filename || '';
originalContent.files = [uploadedFilesResult.online[0]];
}
return originalContent;
}
/**
* @inheritdoc
*/
async isEnabled(): Promise<boolean> {
return true;
}
}
export const AddonModDataFieldFileHandler = makeSingleton(AddonModDataFieldFileHandlerService);

View File

@ -0,0 +1,27 @@
<span *ngIf="inputMode && form" [formGroup]="form">
<ion-input *ngIf="searchMode" type="text" [placeholder]="field.name" [formControlName]="'f_'+field.id"></ion-input>
<ng-container *ngIf="editMode">
<span [core-mark-required]="field.required" class="core-mark-required"></span>
<div class="addon-data-latlong">
<ion-input type="text" [formControlName]="'f_'+field.id+'_0'" maxlength="10"></ion-input>
<span class="placeholder-icon" item-right>°N</span>
</div>
<div class="addon-data-latlong">
<ion-input type="text" [formControlName]="'f_'+field.id+'_1'" maxlength="10"></ion-input>
<span class="placeholder-icon" item-right>°E</span>
</div>
<div class="addon-data-latlong" *ngIf="locationServicesEnabled">
<ion-button (click)="getLocation($event)">
<ion-icon name="fas-crosshairs" slot="start"></ion-icon>
{{ 'addon.mod_data.mylocation' | translate }}
</ion-button>
</div>
<core-input-errors *ngIf="error" [control]="form.controls['f_'+field.id]" [errorText]="error"></core-input-errors>
</ng-container>
</span>
<span *ngIf="displayMode && value">
<a [href]="getLatLongLink(north, east)">{{ formatLatLong(north, east) }}</a>
</span>

View File

@ -0,0 +1,167 @@
// (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 { AddonModDataFieldPluginComponent } from '@addons/mod/data/classes/field-plugin-component';
import { AddonModDataEntryField } from '@addons/mod/data/services/data';
import { Component } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
import { CoreAnyError } from '@classes/errors/error';
import { CoreApp } from '@services/app';
import { CoreGeolocation, CoreGeolocationError, CoreGeolocationErrorReason } from '@services/geolocation';
import { CoreDomUtils } from '@services/utils/dom';
/**
* Component to render data latlong field.
*/
@Component({
selector: 'addon-mod-data-field-latlong',
templateUrl: 'addon-mod-data-field-latlong.html',
})
export class AddonModDataFieldLatlongComponent extends AddonModDataFieldPluginComponent {
north?: number;
east?: number;
locationServicesEnabled = false;
constructor(
fb: FormBuilder,
protected sanitizer: DomSanitizer,
) {
super(fb);
}
/**
* Format latitude and longitude in a simple text.
*
* @param north Degrees north.
* @param east Degrees East.
* @return Readable Latitude and logitude.
*/
formatLatLong(north?: number, east?: number): string {
if (typeof north !== 'undefined' || typeof east !== 'undefined') {
north = north || 0;
east = east || 0;
const northFixed = Math.abs(north).toFixed(4);
const eastFixed = Math.abs(east).toFixed(4);
return northFixed + (north < 0 ? '°S' : '°N') + ' ' + eastFixed + (east < 0 ? '°W' : '°E');
}
return '';
}
/**
* Get link to maps from latitude and longitude.
*
* @param north Degrees north.
* @param east Degrees East.
* @return Link to maps depending on platform.
*/
getLatLongLink(north?: number, east?: number): SafeUrl {
let url = '';
if (typeof north !== 'undefined' || typeof east !== 'undefined') {
const northFixed = north ? north.toFixed(4) : '0.0000';
const eastFixed = east ? east.toFixed(4) : '0.0000';
if (CoreApp.isIOS()) {
url = 'http://maps.apple.com/?ll=' + northFixed + ',' + eastFixed + '&near=' + northFixed + ',' + eastFixed;
} else {
url = 'geo:' + northFixed + ',' + eastFixed;
}
}
return this.sanitizer.bypassSecurityTrustUrl(url);
}
/**
* @inheritdoc
*/
protected async init(): Promise<void> {
if (this.value) {
this.updateValue(this.value);
}
if (this.editMode) {
this.addControl('f_' + this.field.id + '_0', this.north);
this.addControl('f_' + this.field.id + '_1', this.east);
this.locationServicesEnabled = await CoreGeolocation.canRequest();
} else if (this.searchMode) {
this.addControl('f_' + this.field.id);
}
}
/**
* @inheritdoc
*/
protected updateValue(value?: Partial<AddonModDataEntryField>): void {
this.value = value;
this.north = (value && parseFloat(value.content!)) || undefined;
this.east = (value && parseFloat(value.content1!)) || undefined;
}
/**
* Get user location.
*
* @param $event The event.
*/
async getLocation(event: Event): Promise<void> {
event.preventDefault();
const modal = await CoreDomUtils.showModalLoading('addon.mod_data.gettinglocation', true);
try {
const coordinates = await CoreGeolocation.getCoordinates();
this.form?.controls['f_' + this.field.id + '_0'].setValue(coordinates.latitude);
this.form?.controls['f_' + this.field.id + '_1'].setValue(coordinates.longitude);
} catch (error) {
this.showLocationErrorModal(error);
}
modal.dismiss();
}
/**
* Show the appropriate error modal for the given error getting the location.
*
* @param error Location error.
*/
protected showLocationErrorModal(error: CoreAnyError | CoreGeolocationError): void {
if (error instanceof CoreGeolocationError) {
CoreDomUtils.showErrorModal(this.getGeolocationErrorMessage(error), true);
return;
}
CoreDomUtils.showErrorModalDefault(error, 'Error getting location');
}
/**
* Get error message from a geolocation error.
*
* @param error Geolocation error.
*/
protected getGeolocationErrorMessage(error: CoreGeolocationError): string {
// tslint:disable-next-line: switch-default
switch (error.reason) {
case CoreGeolocationErrorReason.PermissionDenied:
return 'addon.mod_data.locationpermissiondenied';
case CoreGeolocationErrorReason.LocationNotEnabled:
return 'addon.mod_data.locationnotenabled';
}
}
}

View File

@ -0,0 +1,42 @@
// (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 { CoreSharedModule } from '@/core/shared.module';
import { NgModule, APP_INITIALIZER } from '@angular/core';
import { AddonModDataFieldsDelegate } from '../../services/data-fields-delegate';
import { AddonModDataFieldLatlongComponent } from './component/latlong';
import { AddonModDataFieldLatlongHandler } from './services/handler';
@NgModule({
declarations: [
AddonModDataFieldLatlongComponent,
],
imports: [
CoreSharedModule,
],
providers: [
{
provide: APP_INITIALIZER,
multi: true,
deps: [],
useFactory: () => () => {
AddonModDataFieldsDelegate.registerHandler(AddonModDataFieldLatlongHandler.instance);
},
},
],
exports: [
AddonModDataFieldLatlongComponent,
],
})
export class AddonModDataFieldLatlongModule {}

View File

@ -0,0 +1,138 @@
// (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 {
AddonModDataEntryField,
AddonModDataField,
AddonModDataSearchEntriesAdvancedFieldFormatted,
AddonModDataSubfieldData,
} from '@addons/mod/data/services/data';
import { AddonModDataFieldHandler } from '@addons/mod/data/services/data-fields-delegate';
import { Injectable, Type } from '@angular/core';
import { CoreFormFields } from '@singletons/form';
import { makeSingleton, Translate } from '@singletons';
import { AddonModDataFieldLatlongComponent } from '../component/latlong';
/**
* Handler for latlong data field plugin.
*/
@Injectable({ providedIn: 'root' })
export class AddonModDataFieldLatlongHandlerService implements AddonModDataFieldHandler {
name = 'AddonModDataFieldLatlongHandler';
type = 'latlong';
/**
* @inheritdoc
*/
getComponent(): Type<unknown>{
return AddonModDataFieldLatlongComponent;
}
/**
* @inheritdoc
*/
getFieldSearchData(
field: AddonModDataField,
inputData: CoreFormFields<string>,
): AddonModDataSearchEntriesAdvancedFieldFormatted[] {
const fieldName = 'f_' + field.id;
if (inputData[fieldName]) {
return [{
name: fieldName,
value: inputData[fieldName],
}];
}
return [];
}
/**
* @inheritdoc
*/
getFieldEditData(field: AddonModDataField, inputData: CoreFormFields<string>): AddonModDataSubfieldData[] {
const fieldName = 'f_' + field.id;
return [
{
fieldid: field.id,
subfield: '0',
value: inputData[fieldName + '_0'] || '',
},
{
fieldid: field.id,
subfield: '1',
value: inputData[fieldName + '_1'] || '',
},
];
}
/**
* @inheritdoc
*/
hasFieldDataChanged(
field: AddonModDataField,
inputData: CoreFormFields<string>,
originalFieldData: AddonModDataEntryField,
): boolean {
const fieldName = 'f_' + field.id;
const lat = inputData[fieldName + '_0'] || '';
const long = inputData[fieldName + '_1'] || '';
const originalLat = (originalFieldData && originalFieldData.content) || '';
const originalLong = (originalFieldData && originalFieldData.content1) || '';
return lat != originalLat || long != originalLong;
}
/**
* @inheritdoc
*/
getFieldsNotifications(field: AddonModDataField, inputData: AddonModDataSubfieldData[]): string | undefined {
let valueCount = 0;
// The lat long class has two values that need to be checked.
inputData.forEach((value) => {
if (typeof value.value != 'undefined' && value.value != '') {
valueCount++;
}
});
// If we get here then only one field has been filled in.
if (valueCount == 1) {
return Translate.instant('addon.mod_data.latlongboth');
} else if (field.required && valueCount == 0) {
return Translate.instant('addon.mod_data.errormustsupplyvalue');
}
}
/**
* @inheritdoc
*/
overrideData(originalContent: AddonModDataEntryField, offlineContent: CoreFormFields<string>): AddonModDataEntryField {
originalContent.content = offlineContent[0] || '';
originalContent.content1 = offlineContent[1] || '';
return originalContent;
}
/**
* @inheritdoc
*/
async isEnabled(): Promise<boolean> {
return true;
}
}
export const AddonModDataFieldLatlongHandler = makeSingleton(AddonModDataFieldLatlongHandlerService);

View File

@ -0,0 +1,11 @@
<span *ngIf="inputMode && form" [formGroup]="form">
<span *ngIf="editMode" [core-mark-required]="field.required" class="core-mark-required"></span>
<ion-select [formControlName]="'f_'+field.id" [placeholder]="'addon.mod_data.menuchoose' | translate"
[interfaceOptions]="{header: field.name}" interface="action-sheet">
<ion-select-option value="">{{ 'addon.mod_data.menuchoose' | translate }}</ion-select-option>
<ion-select-option *ngFor="let option of options" [value]="option">{{option}}</ion-select-option>
</ion-select>
<core-input-errors *ngIf="error && editMode" [control]="form.controls['f_'+field.id]" [errorText]="error"></core-input-errors>
</span>
<span *ngIf="displayMode && value && value.content">{{ value.content }}</span>

View File

@ -0,0 +1,47 @@
// (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 } from '@angular/core';
import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin-component';
/**
* Component to render data menu field.
*/
@Component({
selector: 'addon-mod-data-field-menu',
templateUrl: 'addon-mod-data-field-menu.html',
})
export class AddonModDataFieldMenuComponent extends AddonModDataFieldPluginComponent {
options: string[] = [];
/**
* Initialize field.
*/
protected init(): void {
if (this.displayMode) {
return;
}
this.options = this.field.param1.split('\n');
let val: string | undefined;
if (this.editMode && this.value) {
val = this.value.content;
}
this.addControl('f_' + this.field.id, val);
}
}

View File

@ -0,0 +1,42 @@
// (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 { CoreSharedModule } from '@/core/shared.module';
import { NgModule, APP_INITIALIZER } from '@angular/core';
import { AddonModDataFieldsDelegate } from '../../services/data-fields-delegate';
import { AddonModDataFieldMenuComponent } from './component/menu';
import { AddonModDataFieldMenuHandler } from './services/handler';
@NgModule({
declarations: [
AddonModDataFieldMenuComponent,
],
imports: [
CoreSharedModule,
],
providers: [
{
provide: APP_INITIALIZER,
multi: true,
deps: [],
useFactory: () => () => {
AddonModDataFieldsDelegate.registerHandler(AddonModDataFieldMenuHandler.instance);
},
},
],
exports: [
AddonModDataFieldMenuComponent,
],
})
export class AddonModDataFieldMenuModule {}

View File

@ -0,0 +1,116 @@
// (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 {
AddonModDataEntryField,
AddonModDataField,
AddonModDataSearchEntriesAdvancedFieldFormatted,
AddonModDataSubfieldData,
} from '@addons/mod/data/services/data';
import { AddonModDataFieldHandler } from '@addons/mod/data/services/data-fields-delegate';
import { Injectable, Type } from '@angular/core';
import { CoreFormFields } from '@singletons/form';
import { makeSingleton, Translate } from '@singletons';
import { AddonModDataFieldMenuComponent } from '../component/menu';
/**
* Handler for menu data field plugin.
*/
@Injectable({ providedIn: 'root' })
export class AddonModDataFieldMenuHandlerService implements AddonModDataFieldHandler {
name = 'AddonModDataFieldMenuHandler';
type = 'menu';
/**
* @inheritdoc
*/
getComponent(): Type<unknown>{
return AddonModDataFieldMenuComponent;
}
/**
* @inheritdoc
*/
getFieldSearchData(
field: AddonModDataField,
inputData: CoreFormFields<string>,
): AddonModDataSearchEntriesAdvancedFieldFormatted[] {
const fieldName = 'f_' + field.id;
if (inputData[fieldName]) {
return [{
name: fieldName,
value: inputData[fieldName],
}];
}
return [];
}
/**
* @inheritdoc
*/
getFieldEditData(field: AddonModDataField, inputData: CoreFormFields<string>): AddonModDataSubfieldData[] {
const fieldName = 'f_' + field.id;
if (inputData[fieldName]) {
return [{
fieldid: field.id,
value: inputData[fieldName],
}];
}
return [];
}
hasFieldDataChanged(
field: AddonModDataField,
inputData: CoreFormFields<string>,
originalFieldData: AddonModDataEntryField,
): boolean {
const fieldName = 'f_' + field.id;
const input = inputData[fieldName] || '';
const content = originalFieldData?.content || '';
return input != content;
}
/**
* @inheritdoc
*/
getFieldsNotifications(field: AddonModDataField, inputData: AddonModDataSubfieldData[]): string | undefined {
if (field.required && (!inputData || !inputData.length || !inputData[0].value)) {
return Translate.instant('addon.mod_data.errormustsupplyvalue');
}
}
/**
* @inheritdoc
*/
overrideData(originalContent: AddonModDataEntryField, offlineContent: CoreFormFields<string>): AddonModDataEntryField {
originalContent.content = offlineContent[''] || '';
return originalContent;
}
/**
* @inheritdoc
*/
async isEnabled(): Promise<boolean> {
return true;
}
}
export const AddonModDataFieldMenuHandler = makeSingleton(AddonModDataFieldMenuHandlerService);

View File

@ -0,0 +1,17 @@
<span *ngIf="inputMode && form" [formGroup]="form">
<span *ngIf="editMode" [core-mark-required]="field.required" class="core-mark-required"></span>
<ion-select [formControlName]="'f_'+field.id" multiple="true" [placeholder]="'addon.mod_data.menuchoose' | translate"
[interfaceOptions]="{header: field.name}" interface="alert">
<ion-select-option *ngFor="let option of options" [value]="option.value">{{option.key}}</ion-select-option>
</ion-select>
<core-input-errors *ngIf="error && editMode" [control]="form.controls['f_'+field.id]" [errorText]="error"></core-input-errors>
<ion-item *ngIf="searchMode">
<ion-label>{{ 'addon.mod_data.selectedrequired' | translate }}</ion-label>
<ion-checkbox slot="end" [formControlName]="'f_'+field.id+'_allreq'" [(ngModel)]="searchFields!['f_'+field.id+'_allreq']">
</ion-checkbox>
</ion-item>
</span>
<core-format-text *ngIf="displayMode && value && value.content" [text]="value.content" [filter]="false"></core-format-text>

View File

@ -0,0 +1,70 @@
// (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 { AddonModDataEntryField } from '@addons/mod/data/services/data';
import { Component } from '@angular/core';
import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin-component';
/**
* Component to render data multimenu field.
*/
@Component({
selector: 'addon-mod-data-field-multimenu',
templateUrl: 'addon-mod-data-field-multimenu.html',
})
export class AddonModDataFieldMultimenuComponent extends AddonModDataFieldPluginComponent {
options: {
key: string;
value: string;
}[] = [];
/**
* @inheritdoc
*/
protected init(): void {
if (this.displayMode) {
this.updateValue(this.value);
return;
}
this.options = this.field.param1.split(/\r?\n/).map((option) => ({ key: option, value: option }));
const values: string[] = [];
if (this.editMode && this.value?.content) {
this.value.content.split('##').forEach((value) => {
const x = this.options.findIndex((option) => value == option.key);
if (x >= 0) {
values.push(value);
}
});
}
if (this.searchMode) {
this.addControl('f_' + this.field.id + '_allreq');
}
this.addControl('f_' + this.field.id, values);
}
/**
* @inheritdoc
*/
protected updateValue(value?: Partial<AddonModDataEntryField>): void {
this.value = value || {};
this.value.content = value?.content && value.content.split('##').join('<br>');
}
}

View File

@ -0,0 +1,42 @@
// (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 { CoreSharedModule } from '@/core/shared.module';
import { NgModule, APP_INITIALIZER } from '@angular/core';
import { AddonModDataFieldsDelegate } from '../../services/data-fields-delegate';
import { AddonModDataFieldMultimenuComponent } from './component/multimenu';
import { AddonModDataFieldMultimenuHandler } from './services/handler';
@NgModule({
declarations: [
AddonModDataFieldMultimenuComponent,
],
imports: [
CoreSharedModule,
],
providers: [
{
provide: APP_INITIALIZER,
multi: true,
deps: [],
useFactory: () => () => {
AddonModDataFieldsDelegate.registerHandler(AddonModDataFieldMultimenuHandler.instance);
},
},
],
exports: [
AddonModDataFieldMultimenuComponent,
],
})
export class AddonModDataFieldMultimenuModule {}

View File

@ -0,0 +1,127 @@
// (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 {
AddonModDataEntryField,
AddonModDataField,
AddonModDataSearchEntriesAdvancedFieldFormatted,
AddonModDataSubfieldData,
} from '@addons/mod/data/services/data';
import { AddonModDataFieldHandler } from '@addons/mod/data/services/data-fields-delegate';
import { Injectable, Type } from '@angular/core';
import { CoreFormFields } from '@singletons/form';
import { makeSingleton, Translate } from '@singletons';
import { AddonModDataFieldMultimenuComponent } from '../component/multimenu';
/**
* Handler for multimenu data field plugin.
*/
@Injectable({ providedIn: 'root' })
export class AddonModDataFieldMultimenuHandlerService implements AddonModDataFieldHandler {
name = 'AddonModDataFieldMultimenuHandler';
type = 'multimenu';
/**
* @inheritdoc
*/
getComponent(): Type<unknown>{
return AddonModDataFieldMultimenuComponent;
}
/**
* @inheritdoc
*/
getFieldSearchData(
field: AddonModDataField,
inputData: CoreFormFields<string[]>,
): AddonModDataSearchEntriesAdvancedFieldFormatted[] {
const fieldName = 'f_' + field.id;
const reqName = 'f_' + field.id + '_allreq';
if (inputData[fieldName]) {
const values: AddonModDataSearchEntriesAdvancedFieldFormatted[] = [];
values.push({
name: fieldName,
value: inputData[fieldName],
});
if (inputData[reqName]) {
values.push({
name: reqName,
value: true,
});
}
return values;
}
return [];
}
/**
* @inheritdoc
*/
getFieldEditData(field: AddonModDataField, inputData: CoreFormFields<string[]>): AddonModDataSubfieldData[] {
const fieldName = 'f_' + field.id;
return [{
fieldid: field.id,
value: inputData[fieldName] || [],
}];
}
/**
* @inheritdoc
*/
hasFieldDataChanged(
field: AddonModDataField,
inputData: CoreFormFields<string[]>,
originalFieldData: AddonModDataEntryField,
): boolean {
const fieldName = 'f_' + field.id;
const content = originalFieldData?.content || '';
return inputData[fieldName].join('##') != content;
}
/**
* @inheritdoc
*/
getFieldsNotifications(field: AddonModDataField, inputData: AddonModDataSubfieldData[]): string | undefined {
if (field.required && (!inputData || !inputData.length || !inputData[0].value)) {
return Translate.instant('addon.mod_data.errormustsupplyvalue');
}
}
/**
* @inheritdoc
*/
overrideData(originalContent: AddonModDataEntryField, offlineContent: CoreFormFields<string[]>): AddonModDataEntryField {
originalContent.content = (offlineContent[''] && offlineContent[''].join('##')) || '';
return originalContent;
}
/**
* @inheritdoc
*/
async isEnabled(): Promise<boolean> {
return true;
}
}
export const AddonModDataFieldMultimenuHandler = makeSingleton(AddonModDataFieldMultimenuHandlerService);

View File

@ -0,0 +1,7 @@
<span *ngIf="inputMode && form" [formGroup]="form">
<span *ngIf="editMode" [core-mark-required]="field.required" class="core-mark-required"></span>
<ion-input type="number" [formControlName]="'f_'+field.id" [placeholder]="field.name"></ion-input>
<core-input-errors *ngIf="error && editMode" [control]="form.controls['f_'+field.id]" [errorText]="error"></core-input-errors>
</span>
<span *ngIf="displayMode && value && value.content">{{ value.content }}</span>

View File

@ -0,0 +1,44 @@
// (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 } from '@angular/core';
import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin-component';
/**
* Component to render data number field.
*/
@Component({
selector: 'addon-mod-data-field-number',
templateUrl: 'addon-mod-data-field-number.html',
})
export class AddonModDataFieldNumberComponent extends AddonModDataFieldPluginComponent{
/**
* @inheritdoc
*/
protected init(): void {
if (this.displayMode) {
return;
}
let value: number | string | undefined;
if (this.editMode && this.value) {
const v = parseFloat(this.value.content || '');
value = isNaN(v) ? '' : v;
}
this.addControl('f_' + this.field.id, value);
}
}

View File

@ -0,0 +1,42 @@
// (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 { CoreSharedModule } from '@/core/shared.module';
import { NgModule, APP_INITIALIZER } from '@angular/core';
import { AddonModDataFieldsDelegate } from '../../services/data-fields-delegate';
import { AddonModDataFieldNumberComponent } from './component/number';
import { AddonModDataFieldNumberHandler } from './services/handler';
@NgModule({
declarations: [
AddonModDataFieldNumberComponent,
],
imports: [
CoreSharedModule,
],
providers: [
{
provide: APP_INITIALIZER,
multi: true,
deps: [],
useFactory: () => () => {
AddonModDataFieldsDelegate.registerHandler(AddonModDataFieldNumberHandler.instance);
},
},
],
exports: [
AddonModDataFieldNumberComponent,
],
})
export class AddonModDataFieldNumberModule {}

View File

@ -0,0 +1,63 @@
// (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 { AddonModDataEntryField, AddonModDataField, AddonModDataSubfieldData } from '@addons/mod/data/services/data';
import { Injectable, Type } from '@angular/core';
import { CoreFormFields } from '@singletons/form';
import { makeSingleton, Translate } from '@singletons';
import { AddonModDataFieldTextHandlerService } from '../../text/services/handler';
import { AddonModDataFieldNumberComponent } from '../component/number';
/**
* Handler for number data field plugin.
*/
@Injectable({ providedIn: 'root' })
export class AddonModDataFieldNumberHandlerService extends AddonModDataFieldTextHandlerService {
name = 'AddonModDataFieldNumberHandler';
type = 'number';
/**
* @inheritdoc
*/
getComponent(): Type<unknown>{
return AddonModDataFieldNumberComponent;
}
/**
* @inheritdoc
*/
hasFieldDataChanged(
field: AddonModDataField,
inputData: CoreFormFields,
originalFieldData: AddonModDataEntryField,
): boolean {
const fieldName = 'f_' + field.id;
const input = inputData[fieldName] || '';
const content = originalFieldData?.content || '';
return input != content;
}
/**
* @inheritdoc
*/
getFieldsNotifications(field: AddonModDataField, inputData: AddonModDataSubfieldData[]): string | undefined {
if (field.required && (!inputData || !inputData.length || inputData[0].value == '')) {
return Translate.instant('addon.mod_data.errormustsupplyvalue');
}
}
}
export const AddonModDataFieldNumberHandler = makeSingleton(AddonModDataFieldNumberHandlerService);

View File

@ -0,0 +1,22 @@
<span *ngIf="editMode && form" [formGroup]="form">
<span [core-mark-required]="field.required" class="core-mark-required"></span>
<core-attachments [files]="files" [maxSize]="maxSizeBytes" maxSubmissions="1" [component]="component"
[componentId]="componentId" [allowOffline]="true" acceptedTypes="image">
</core-attachments>
<core-input-errors *ngIf="error" [errorText]="error"></core-input-errors>
<ion-label position="stacked">{{ 'addon.mod_data.alttext' | translate }}</ion-label>
<ion-input type="text" [formControlName]="'f_'+field.id+'_alttext'" [placeholder]=" 'addon.mod_data.alttext' | translate" >
</ion-input>
</span>
<span *ngIf="searchMode && form" [formGroup]="form">
<ion-input type="text" [formControlName]="'f_'+field.id" [placeholder]="field.name"></ion-input>
</span>
<span *ngIf="listMode && imageUrl" (click)="gotoEntry.emit(entryId)">
<img [src]="imageUrl" [alt]="title" class="core-media-adapt-width listMode_picture" core-external-content/>
</span>
<img *ngIf="showMode && imageUrl" [src]="imageUrl" [alt]="title" class="core-media-adapt-width listMode_picture"
[attr.width]="width" [attr.height]="height" core-external-content/>

View File

@ -0,0 +1,142 @@
// (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 { AddonModDataEntryField, AddonModDataProvider } from '@addons/mod/data/services/data';
import { Component } from '@angular/core';
import { FileEntry } from '@ionic-native/file';
import { CoreFileSession } from '@services/file-session';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreWSExternalFile } from '@services/ws';
import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin-component';
/**
* Component to render data picture field.
*/
@Component({
selector: 'addon-mod-data-field-picture',
templateUrl: 'addon-mod-data-field-picture.html',
})
export class AddonModDataFieldPictureComponent extends AddonModDataFieldPluginComponent {
files: (CoreWSExternalFile | FileEntry)[] = [];
component?: string;
componentId?: number;
maxSizeBytes?: number;
image?: CoreWSExternalFile | FileEntry;
entryId?: number;
imageUrl?: string;
title?: string;
width?: string;
height?: string;
/**
* Get the files from the input value.
*
* @param value Input value.
* @return List of files.
*/
protected getFiles(value?: Partial<AddonModDataEntryField>): (CoreWSExternalFile | FileEntry)[] {
let files = value?.files || [];
// Reduce to first element.
if (files.length > 0) {
files = [files[0]];
}
return files;
}
/**
* Find file in a list.
*
* @param files File list where to search.
* @param filenameSeek Filename to search.
* @return File found or false.
*/
protected findFile(
files: (CoreWSExternalFile | FileEntry)[],
filenameSeek: string,
): CoreWSExternalFile | FileEntry | undefined {
return files.find((file) => ('name' in file ? file.name : file.filename) == filenameSeek) || undefined;
}
/**
* @inheritdoc
*/
protected init(): void {
if (this.searchMode) {
this.addControl('f_' + this.field.id);
return;
}
this.component = AddonModDataProvider.COMPONENT;
this.componentId = this.database!.coursemodule;
this.updateValue(this.value);
if (this.editMode) {
this.maxSizeBytes = parseInt(this.field.param3, 10);
CoreFileSession.setFiles(this.component, this.database!.id + '_' + this.field.id, this.files);
const alttext = (this.value && this.value.content1) || '';
this.addControl('f_' + this.field.id + '_alttext', alttext);
}
}
/**
* @inheritdoc
*/
protected updateValue(value?: Partial<AddonModDataEntryField>): void {
// Edit mode, the list shouldn't change so there is no need to watch it.
const files = value?.files || [];
// Get image or thumb.
if (files.length > 0) {
const filenameSeek = this.listMode
? 'thumb_' + value?.content
: value?.content;
this.image = this.findFile(files, filenameSeek || '');
if (!this.image && this.listMode) {
this.image = this.findFile(files, value?.content || '');
}
if (this.image) {
this.files = [this.image];
}
} else {
this.image = undefined;
this.files = [];
}
if (!this.editMode) {
this.entryId = (value && value.recordid) || undefined;
this.title = (value && value.content1) || '';
this.imageUrl = undefined;
setTimeout(() => {
if (this.image) {
this.imageUrl = 'name' in this.image
? this.image.toURL() // Is Offline.
: this.image.fileurl;
}
}, 1);
this.width = CoreDomUtils.formatPixelsSize(this.field.param1);
this.height = CoreDomUtils.formatPixelsSize(this.field.param2);
}
}
}

View File

@ -0,0 +1,42 @@
// (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 { CoreSharedModule } from '@/core/shared.module';
import { NgModule, APP_INITIALIZER } from '@angular/core';
import { AddonModDataFieldsDelegate } from '../../services/data-fields-delegate';
import { AddonModDataFieldPictureComponent } from './component/picture';
import { AddonModDataFieldPictureHandler } from './services/handler';
@NgModule({
declarations: [
AddonModDataFieldPictureComponent,
],
imports: [
CoreSharedModule,
],
providers: [
{
provide: APP_INITIALIZER,
multi: true,
deps: [],
useFactory: () => () => {
AddonModDataFieldsDelegate.registerHandler(AddonModDataFieldPictureHandler.instance);
},
},
],
exports: [
AddonModDataFieldPictureComponent,
],
})
export class AddonModDataFieldPictureModule {}

View File

@ -0,0 +1,181 @@
// (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 {
AddonModDataEntryField,
AddonModDataField,
AddonModDataProvider,
AddonModDataSearchEntriesAdvancedFieldFormatted,
AddonModDataSubfieldData,
} from '@addons/mod/data/services/data';
import { AddonModDataFieldHandler } from '@addons/mod/data/services/data-fields-delegate';
import { Injectable, Type } from '@angular/core';
import { CoreFileUploader, CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader';
import { FileEntry } from '@ionic-native/file';
import { CoreFileSession } from '@services/file-session';
import { CoreFormFields } from '@singletons/form';
import { CoreWSExternalFile } from '@services/ws';
import { makeSingleton, Translate } from '@singletons';
import { AddonModDataFieldPictureComponent } from '../component/picture';
/**
* Handler for picture data field plugin.
*/
@Injectable({ providedIn: 'root' })
export class AddonModDataFieldPictureHandlerService implements AddonModDataFieldHandler {
name = 'AddonModDataFieldPictureHandler';
type = 'picture';
/**
* @inheritdoc
*/
getComponent(): Type<unknown>{
return AddonModDataFieldPictureComponent;
}
/**
* @inheritdoc
*/
getFieldSearchData(
field: AddonModDataField,
inputData: CoreFormFields<string>,
): AddonModDataSearchEntriesAdvancedFieldFormatted[] {
const fieldName = 'f_' + field.id;
if (inputData[fieldName]) {
return [{
name: fieldName,
value: inputData[fieldName],
}];
}
return [];
}
/**
* @inheritdoc
*/
getFieldEditData(field: AddonModDataField, inputData: CoreFormFields<string>): AddonModDataSubfieldData[] {
const files = this.getFieldEditFiles(field);
const fieldName = 'f_' + field.id + '_alttext';
return [
{
fieldid: field.id,
subfield: 'file',
files: files,
},
{
fieldid: field.id,
subfield: 'alttext',
value: inputData[fieldName],
},
];
}
/**
* @inheritdoc
*/
getFieldEditFiles(field: AddonModDataField): (CoreWSExternalFile | FileEntry)[] {
return CoreFileSession.getFiles(AddonModDataProvider.COMPONENT, field.dataid + '_' + field.id);
}
/**
* @inheritdoc
*/
hasFieldDataChanged(
field: AddonModDataField,
inputData: CoreFormFields<string>,
originalFieldData: AddonModDataEntryField,
): boolean {
const fieldName = 'f_' + field.id + '_alttext';
const altText = inputData[fieldName] || '';
const originalAltText = originalFieldData?.content1 || '';
if (altText != originalAltText) {
return true;
}
const files = this.getFieldEditFiles(field) || [];
let originalFiles = originalFieldData?.files || [];
// Get image.
if (originalFiles.length > 0) {
const filenameSeek = originalFieldData?.content || '';
const file = originalFiles.find((file) => ('name' in file ? file.name : file.filename) == filenameSeek);
if (file) {
originalFiles = [file];
}
}
return CoreFileUploader.areFileListDifferent(files, originalFiles);
}
/**
* @inheritdoc
*/
getFieldsNotifications(field: AddonModDataField, inputData: AddonModDataSubfieldData[]): string | undefined {
if (!field.required) {
return;
}
if (!inputData || !inputData.length) {
return Translate.instant('addon.mod_data.errormustsupplyvalue');
}
const found = inputData.some((input) => {
if (typeof input.subfield != 'undefined' && input.subfield == 'file') {
return !!input.value;
}
return false;
});
if (!found) {
return Translate.instant('addon.mod_data.errormustsupplyvalue');
}
}
/**
* @inheritdoc
*/
overrideData(
originalContent: AddonModDataEntryField,
offlineContent: CoreFormFields,
offlineFiles?: FileEntry[],
): AddonModDataEntryField {
const uploadedFilesResult: CoreFileUploaderStoreFilesResult = <CoreFileUploaderStoreFilesResult>offlineContent?.file;
if (uploadedFilesResult && uploadedFilesResult.offline > 0 && offlineFiles && offlineFiles?.length > 0) {
originalContent.content = offlineFiles[0].name;
originalContent.files = [offlineFiles[0]];
} else if (uploadedFilesResult && uploadedFilesResult.online && uploadedFilesResult.online.length > 0) {
originalContent.content = uploadedFilesResult.online[0].filename || '';
originalContent.files = [uploadedFilesResult.online[0]];
}
originalContent.content1 = <string>offlineContent.alttext || '';
return originalContent;
}
/**
* @inheritdoc
*/
async isEnabled(): Promise<boolean> {
return true;
}
}
export const AddonModDataFieldPictureHandler = makeSingleton(AddonModDataFieldPictureHandlerService);

View File

@ -0,0 +1,11 @@
<span *ngIf="inputMode && form" [formGroup]="form">
<span *ngIf="editMode" [core-mark-required]="field.required" class="core-mark-required"></span>
<ion-select [formControlName]="'f_'+field.id" [placeholder]="'addon.mod_data.menuchoose' | translate"
[interfaceOptions]="{header: field.name}" interface="alert">
<ion-select-option value="">{{ 'addon.mod_data.menuchoose' | translate }}</ion-select-option>
<ion-select-option *ngFor="let option of options" [value]="option">{{option}}</ion-select-option>
</ion-select>
<core-input-errors *ngIf="error && editMode" [control]="form.controls['f_'+field.id]" [errorText]="error"></core-input-errors>
</span>
<span *ngIf="displayMode && value && value.content">{{ value.content }}</span>

View File

@ -0,0 +1,46 @@
// (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 } from '@angular/core';
import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin-component';
/**
* Component to render data radiobutton field.
*/
@Component({
selector: 'addon-mod-data-field-radiobutton',
templateUrl: 'addon-mod-data-field-radiobutton.html',
})
export class AddonModDataFieldRadiobuttonComponent extends AddonModDataFieldPluginComponent {
options: string[] = [];
/**
* Initialize field.
*/
protected init(): void {
if (this.displayMode) {
return;
}
this.options = this.field.param1.split('\n');
let val: string | undefined;
if (this.editMode && this.value) {
val = this.value.content;
}
this.addControl('f_' + this.field.id, val);
}
}

View File

@ -0,0 +1,42 @@
// (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 { CoreSharedModule } from '@/core/shared.module';
import { NgModule, APP_INITIALIZER } from '@angular/core';
import { AddonModDataFieldsDelegate } from '../../services/data-fields-delegate';
import { AddonModDataFieldRadiobuttonComponent } from './component/radiobutton';
import { AddonModDataFieldRadiobuttonHandler } from './services/handler';
@NgModule({
declarations: [
AddonModDataFieldRadiobuttonComponent,
],
imports: [
CoreSharedModule,
],
providers: [
{
provide: APP_INITIALIZER,
multi: true,
deps: [],
useFactory: () => () => {
AddonModDataFieldsDelegate.registerHandler(AddonModDataFieldRadiobuttonHandler.instance);
},
},
],
exports: [
AddonModDataFieldRadiobuttonComponent,
],
})
export class AddonModDataFieldRadiobuttonModule {}

View File

@ -0,0 +1,114 @@
// (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 {
AddonModDataEntryField,
AddonModDataField,
AddonModDataSearchEntriesAdvancedFieldFormatted,
AddonModDataSubfieldData,
} from '@addons/mod/data/services/data';
import { AddonModDataFieldHandler } from '@addons/mod/data/services/data-fields-delegate';
import { Injectable, Type } from '@angular/core';
import { CoreFormFields } from '@singletons/form';
import { makeSingleton, Translate } from '@singletons';
import { AddonModDataFieldRadiobuttonComponent } from '../component/radiobutton';
/**
* Handler for checkbox data field plugin.
*/
@Injectable({ providedIn: 'root' })
export class AddonModDataFieldRadiobuttonHandlerService implements AddonModDataFieldHandler {
name = 'AddonModDataFieldRadiobuttonHandler';
type = 'radiobutton';
/**
* @inheritdoc
*/
getComponent(): Type<unknown>{
return AddonModDataFieldRadiobuttonComponent;
}
/**
* @inheritdoc
*/
getFieldSearchData(
field: AddonModDataField,
inputData: CoreFormFields<string>,
): AddonModDataSearchEntriesAdvancedFieldFormatted[] {
const fieldName = 'f_' + field.id;
if (inputData[fieldName]) {
return [{
name: fieldName,
value: inputData[fieldName],
}];
}
return [];
}
/**
* @inheritdoc
*/
getFieldEditData(field: AddonModDataField, inputData: CoreFormFields<string>): AddonModDataSubfieldData[] {
const fieldName = 'f_' + field.id;
return [{
fieldid: field.id,
value: inputData[fieldName] || '',
}];
}
/**
* @inheritdoc
*/
hasFieldDataChanged(
field: AddonModDataField,
inputData: CoreFormFields<string>,
originalFieldData: AddonModDataEntryField,
): boolean {
const fieldName = 'f_' + field.id;
const input = inputData[fieldName] || '';
const content = originalFieldData?.content || '';
return input != content;
}
/**
* @inheritdoc
*/
getFieldsNotifications(field: AddonModDataField, inputData: AddonModDataSubfieldData[]): string | undefined {
if (field.required && (!inputData || !inputData.length || !inputData[0].value)) {
return Translate.instant('addon.mod_data.errormustsupplyvalue');
}
}
/**
* @inheritdoc
*/
overrideData(originalContent: AddonModDataEntryField, offlineContent: CoreFormFields<string>): AddonModDataEntryField {
originalContent.content = offlineContent[''] || '';
return originalContent;
}
/**
* @inheritdoc
*/
async isEnabled(): Promise<boolean> {
return true;
}
}
export const AddonModDataFieldRadiobuttonHandler = makeSingleton(AddonModDataFieldRadiobuttonHandlerService);

View File

@ -0,0 +1,7 @@
<span *ngIf="inputMode && form" [formGroup]="form">
<span *ngIf="editMode" [core-mark-required]="field.required" class="core-mark-required"></span>
<ion-input type="text" [formControlName]="'f_'+field.id" [placeholder]="field.name"></ion-input>
<core-input-errors *ngIf="error && editMode" [control]="form.controls['f_'+field.id]" [errorText]="error"></core-input-errors>
</span>
<span *ngIf="displayMode && value && value.content">{{ value.content }}</span>

View File

@ -0,0 +1,43 @@
// (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 } from '@angular/core';
import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin-component';
/**
* Component to render data text field.
*/
@Component({
selector: 'addon-mod-data-field-text',
templateUrl: 'addon-mod-data-field-text.html',
})
export class AddonModDataFieldTextComponent extends AddonModDataFieldPluginComponent {
/**
* @inheritdoc
*/
protected init(): void {
if (this.displayMode) {
return;
}
let value: string | undefined;
if (this.editMode && this.value) {
value = this.value.content;
}
this.addControl('f_' + this.field.id, value);
}
}

View File

@ -0,0 +1,117 @@
// (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 {
AddonModDataEntryField,
AddonModDataField,
AddonModDataSearchEntriesAdvancedFieldFormatted,
AddonModDataSubfieldData,
} from '@addons/mod/data/services/data';
import { AddonModDataFieldHandler } from '@addons/mod/data/services/data-fields-delegate';
import { Injectable, Type } from '@angular/core';
import { CoreFormFields } from '@singletons/form';
import { makeSingleton, Translate } from '@singletons';
import { AddonModDataFieldTextComponent } from '../component/text';
/**
* Handler for number data field plugin.
*/
@Injectable({ providedIn: 'root' })
export class AddonModDataFieldTextHandlerService implements AddonModDataFieldHandler {
name = 'AddonModDataFieldTextHandler';
type = 'text';
/**
* @inheritdoc
*/
getComponent(): Type<unknown>{
return AddonModDataFieldTextComponent;
}
/**
* @inheritdoc
*/
getFieldSearchData(field: AddonModDataField, inputData: CoreFormFields): AddonModDataSearchEntriesAdvancedFieldFormatted[] {
const fieldName = 'f_' + field.id;
if (inputData[fieldName]) {
return [{
name: fieldName,
value: inputData[fieldName],
}];
}
return [];
}
/**
* @inheritdoc
*/
getFieldEditData(
field: AddonModDataField,
inputData: CoreFormFields<string>,
originalFieldData: AddonModDataEntryField, // eslint-disable-line @typescript-eslint/no-unused-vars
): AddonModDataSubfieldData[] {
const fieldName = 'f_' + field.id;
return [{
fieldid: field.id,
value: inputData[fieldName] || '',
}];
}
/**
* @inheritdoc
*/
hasFieldDataChanged(
field: AddonModDataField,
inputData: CoreFormFields<string>,
originalFieldData: AddonModDataEntryField,
): boolean {
const fieldName = 'f_' + field.id;
const input = inputData[fieldName] || '';
const content = originalFieldData?.content || '';
return input != content;
}
/**
* @inheritdoc
*/
getFieldsNotifications(field: AddonModDataField, inputData: AddonModDataSubfieldData[]): string | undefined {
if (field.required && (!inputData || !inputData.length || !inputData[0].value)) {
return Translate.instant('addon.mod_data.errormustsupplyvalue');
}
}
/**
* @inheritdoc
*/
overrideData(originalContent: AddonModDataEntryField, offlineContent: CoreFormFields<string>): AddonModDataEntryField {
originalContent.content = offlineContent[''] || '';
return originalContent;
}
/**
* @inheritdoc
*/
async isEnabled(): Promise<boolean> {
return true;
}
}
export const AddonModDataFieldTextHandler = makeSingleton(AddonModDataFieldTextHandlerService);

View File

@ -0,0 +1,42 @@
// (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 { CoreSharedModule } from '@/core/shared.module';
import { NgModule, APP_INITIALIZER } from '@angular/core';
import { AddonModDataFieldsDelegate } from '../../services/data-fields-delegate';
import { AddonModDataFieldTextComponent } from './component/text';
import { AddonModDataFieldTextHandler } from './services/handler';
@NgModule({
declarations: [
AddonModDataFieldTextComponent,
],
imports: [
CoreSharedModule,
],
providers: [
{
provide: APP_INITIALIZER,
multi: true,
deps: [],
useFactory: () => () => {
AddonModDataFieldsDelegate.registerHandler(AddonModDataFieldTextHandler.instance);
},
},
],
exports: [
AddonModDataFieldTextComponent,
],
})
export class AddonModDataFieldTextModule {}

View File

@ -0,0 +1,14 @@
<span *ngIf="inputMode && form" [formGroup]="form">
<ion-input *ngIf="searchMode" type="text" [placeholder]="field.name" [formControlName]="'f_'+field.id"></ion-input>
<span *ngIf="editMode" [core-mark-required]="field.required" class="core-mark-required"></span>
<core-rich-text-editor *ngIf="editMode" item-content [control]="form.controls['f_'+field.id]" [placeholder]="field.name"
[formControlName]="'f_'+field.id" [component]="component" [componentId]="componentId" [autoSave]="true"
contextLevel="module" [contextInstanceId]="componentId" [elementId]="'field_'+field.id" ngDefaultControl>
</core-rich-text-editor>
<core-input-errors *ngIf="error && editMode" [control]="form.controls['f_'+field.id]" [errorText]="error"></core-input-errors>
</span>
<core-format-text *ngIf="displayMode && value" [text]="format(value)" [component]="component" [componentId]="componentId"
contextLevel="module" [contextInstanceId]="componentId" [courseId]="database!.course">
</core-format-text>

View File

@ -0,0 +1,65 @@
// (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 } from '@angular/core';
import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin-component';
import { AddonModDataEntryField, AddonModDataProvider } from '@addons/mod/data/services/data';
import { CoreTextUtils } from '@services/utils/text';
import { CoreWSExternalFile } from '@services/ws';
/**
* Component to render data number field.
*/
@Component({
selector: 'addon-mod-data-field-textarea',
templateUrl: 'addon-mod-data-field-textarea.html',
})
export class AddonModDataFieldTextareaComponent extends AddonModDataFieldPluginComponent {
component?: string;
componentId?: number;
/**
* Format value to be shown. Replacing plugin file Urls.
*
* @param value Value to replace.
* @return Replaced string to be rendered.
*/
format(value?: Partial<AddonModDataEntryField>): string {
const files: CoreWSExternalFile[] = (value && <CoreWSExternalFile[]>value.files) || [];
return value ? CoreTextUtils.replacePluginfileUrls(value.content || '', files) : '';
}
/**
* Initialize field.
*/
protected init(): void {
this.component = AddonModDataProvider.COMPONENT;
this.componentId = this.database?.coursemodule;
if (this.displayMode) {
return;
}
let text: string | undefined;
// Check if rich text editor is enabled.
if (this.editMode) {
text = this.format(this.value);
}
this.addControl('f_' + this.field.id, text);
}
}

View File

@ -0,0 +1,127 @@
// (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 { AddonModDataEntryField, AddonModDataField, AddonModDataSubfieldData } from '@addons/mod/data/services/data';
import { Injectable, Type } from '@angular/core';
import { FileEntry } from '@ionic-native/file';
import { CoreFormFields } from '@singletons/form';
import { CoreTextUtils } from '@services/utils/text';
import { CoreWSExternalFile } from '@services/ws';
import { makeSingleton, Translate } from '@singletons';
import { AddonModDataFieldTextHandlerService } from '../../text/services/handler';
import { AddonModDataFieldTextareaComponent } from '../component/textarea';
/**
* Handler for textarea data field plugin.
*/
@Injectable({ providedIn: 'root' })
export class AddonModDataFieldTextareaHandlerService extends AddonModDataFieldTextHandlerService {
name = 'AddonModDataFieldTextareaHandler';
type = 'textarea';
/**
* @inheritdoc
*/
getComponent(): Type<unknown>{
return AddonModDataFieldTextareaComponent;
}
/**
* @inheritdoc
*/
getFieldEditData(
field: AddonModDataField,
inputData: CoreFormFields<string>,
originalFieldData: AddonModDataEntryField,
): AddonModDataSubfieldData[] {
const fieldName = 'f_' + field.id;
const files = this.getFieldEditFiles(field, inputData, originalFieldData);
let text = CoreTextUtils.restorePluginfileUrls(inputData[fieldName] || '', <CoreWSExternalFile[]>files);
// Add some HTML to the text if needed.
text = CoreTextUtils.formatHtmlLines(text);
// WS does not properly check if HTML content is blank when the field is required.
if (CoreTextUtils.htmlIsBlank(text)) {
text = '';
}
return [
{
fieldid: field.id,
value: text,
},
{
fieldid: field.id,
subfield: 'content1',
value: 1,
},
{
fieldid: field.id,
subfield: 'itemid',
files: files,
},
];
}
/**
* @inheritdoc
*/
getFieldEditFiles(
field: AddonModDataField,
inputData: CoreFormFields,
originalFieldData: AddonModDataEntryField,
): (CoreWSExternalFile | FileEntry)[] {
return (originalFieldData && originalFieldData.files) || [];
}
/**
* @inheritdoc
*/
getFieldsNotifications(field: AddonModDataField, inputData: AddonModDataSubfieldData[]): string | undefined {
if (!field.required) {
return;
}
if (!inputData || !inputData.length) {
return Translate.instant('addon.mod_data.errormustsupplyvalue');
}
const value = inputData.find((value) => value.subfield == '');
if (!value || CoreTextUtils.htmlIsBlank(<string>value.value || '')) {
return Translate.instant('addon.mod_data.errormustsupplyvalue');
}
}
/**
* @inheritdoc
*/
overrideData(originalContent: AddonModDataEntryField, offlineContent: CoreFormFields<string>): AddonModDataEntryField {
originalContent.content = offlineContent[''] || '';
if (originalContent.content.length > 0 && originalContent.files && originalContent.files.length > 0) {
// Take the original files since we cannot edit them on the app.
originalContent.content = CoreTextUtils.replacePluginfileUrls(
originalContent.content,
<CoreWSExternalFile[]>originalContent.files,
);
}
return originalContent;
}
}
export const AddonModDataFieldTextareaHandler = makeSingleton(AddonModDataFieldTextareaHandlerService);

View File

@ -0,0 +1,44 @@
// (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 { CoreSharedModule } from '@/core/shared.module';
import { NgModule, APP_INITIALIZER } from '@angular/core';
import { CoreEditorComponentsModule } from '@features/editor/components/components.module';
import { AddonModDataFieldsDelegate } from '../../services/data-fields-delegate';
import { AddonModDataFieldTextareaComponent } from './component/textarea';
import { AddonModDataFieldTextareaHandler } from './services/handler';
@NgModule({
declarations: [
AddonModDataFieldTextareaComponent,
],
imports: [
CoreSharedModule,
CoreEditorComponentsModule,
],
providers: [
{
provide: APP_INITIALIZER,
multi: true,
deps: [],
useFactory: () => () => {
AddonModDataFieldsDelegate.registerHandler(AddonModDataFieldTextareaHandler.instance);
},
},
],
exports: [
AddonModDataFieldTextareaComponent,
],
})
export class AddonModDataFieldTextareaModule {}

View File

@ -0,0 +1,10 @@
<span *ngIf="inputMode && form" [formGroup]="form">
<span *ngIf="editMode" [core-mark-required]="field.required" class="core-mark-required"></span>
<ion-input type="url" [formControlName]="'f_'+field.id" [placeholder]="field.name"></ion-input>
<core-input-errors *ngIf="error && editMode" [control]="form.controls['f_'+field.id]" [errorText]="error"></core-input-errors>
</span>
<ng-container *ngIf="displayMode && value && value.content">
<a *ngIf="autoLink" [href]="value.content" core-link capture="true">{{ displayValue }}</a>
<span *ngIf="!autoLink">{{ displayValue }}</span>
</ng-container>

View File

@ -0,0 +1,78 @@
// (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 { AddonModDataEntryField } from '@addons/mod/data/services/data';
import { Component } from '@angular/core';
import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin-component';
/**
* Component to render data url field.
*/
@Component({
selector: 'addon-mod-data-field-url',
templateUrl: 'addon-mod-data-field-url.html',
})
export class AddonModDataFieldUrlComponent extends AddonModDataFieldPluginComponent {
autoLink = false;
displayValue = '';
/**
* @inheritdoc
*/
protected init(): void {
if (this.displayMode) {
return;
}
let value: string | undefined;
if (this.editMode && this.value) {
value = this.value.content;
}
this.addControl('f_' + this.field.id, value);
}
/**
* Calculate data for show or list mode.
*/
protected calculateShowListData(): void {
if (!this.value || !this.value.content) {
return;
}
const url = this.value.content;
const text = this.field.param2 || this.value.content1; // Param2 forces the text to display.
this.autoLink = parseInt(this.field.param1, 10) === 1;
if (this.autoLink) {
this.displayValue = text || url;
} else {
// No auto link, always display the URL.
this.displayValue = url;
}
}
/**
* @inheritdoc
*/
protected updateValue(value?: Partial<AddonModDataEntryField>): void {
super.updateValue(value);
if (this.displayMode) {
this.calculateShowListData();
}
}
}

View File

@ -0,0 +1,63 @@
// (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 { AddonModDataField, AddonModDataSubfieldData } from '@addons/mod/data/services/data';
import { Injectable, Type } from '@angular/core';
import { CoreFormFields } from '@singletons/form';
import { Translate, makeSingleton } from '@singletons';
import { AddonModDataFieldTextHandlerService } from '../../text/services/handler';
import { AddonModDataFieldUrlComponent } from '../component/url';
/**
* Handler for url data field plugin.
*/
@Injectable({ providedIn: 'root' })
export class AddonModDataFieldUrlHandlerService extends AddonModDataFieldTextHandlerService {
name = 'AddonModDataFieldUrlHandler';
type = 'url';
/**
* @inheritdoc
*/
getComponent(): Type<unknown>{
return AddonModDataFieldUrlComponent;
}
/**
* @inheritdoc
*/
getFieldEditData(field: AddonModDataField, inputData: CoreFormFields<string>): AddonModDataSubfieldData[] {
const fieldName = 'f_' + field.id;
return [
{
fieldid: field.id,
subfield: '0',
value: (inputData[fieldName] && inputData[fieldName].trim()) || '',
},
];
}
/**
* @inheritdoc
*/
getFieldsNotifications(field: AddonModDataField, inputData: AddonModDataSubfieldData[]): string | undefined {
if (field.required && (!inputData || !inputData.length || !inputData[0].value)) {
return Translate.instant('addon.mod_data.errormustsupplyvalue');
}
}
}
export const AddonModDataFieldUrlHandler = makeSingleton(AddonModDataFieldUrlHandlerService);

View File

@ -0,0 +1,42 @@
// (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 { CoreSharedModule } from '@/core/shared.module';
import { NgModule, APP_INITIALIZER } from '@angular/core';
import { AddonModDataFieldsDelegate } from '../../services/data-fields-delegate';
import { AddonModDataFieldUrlComponent } from './component/url';
import { AddonModDataFieldUrlHandler } from './services/handler';
@NgModule({
declarations: [
AddonModDataFieldUrlComponent,
],
imports: [
CoreSharedModule,
],
providers: [
{
provide: APP_INITIALIZER,
multi: true,
deps: [],
useFactory: () => () => {
AddonModDataFieldsDelegate.registerHandler(AddonModDataFieldUrlHandler.instance);
},
},
],
exports: [
AddonModDataFieldUrlComponent,
],
})
export class AddonModDataFieldUrlModule {}

View File

@ -0,0 +1,50 @@
{
"addentries": "Add entries",
"advancedsearch": "Advanced search",
"alttext": "Alternative text",
"approve": "Approve",
"approved": "Approved",
"ascending": "Ascending",
"authorfirstname": "Author first name",
"authorlastname": "Author surname",
"confirmdeleterecord": "Are you sure you want to delete this entry?",
"descending": "Descending",
"disapprove": "Undo approval",
"edittagsnotsupported": "Sorry, editing tags is not supported by the app.",
"emptyaddform": "You did not fill out any fields!",
"entrieslefttoadd": "You must add {{$a.entriesleft}} more entry/entries in order to complete this activity",
"entrieslefttoaddtoview": "You must add {{$a.entrieslefttoview}} more entry/entries before you can view other participants' entries.",
"errorapproving": "Error approving or unapproving entry.",
"errordeleting": "Error deleting entry.",
"errormustsupplyvalue": "You must supply a value here.",
"expired": "Sorry, this activity closed on {{$a}} and is no longer available",
"fields": "Fields",
"foundrecords": "Found records: {{$a.num}}/{{$a.max}} (<a href=\"{{$a.reseturl}}\">Reset filters</a>)",
"gettinglocation": "Getting location",
"latlongboth": "Both latitude and longitude are required.",
"locationpermissiondenied": "Permission to access your location has been denied.",
"locationnotenabled": "Location is not enabled",
"menuchoose": "Choose...",
"modulenameplural": "Databases",
"more": "More",
"mylocation": "My location",
"noaccess": "You do not have access to this page",
"nomatch": "No matching entries found!",
"norecords": "No entries in database",
"notapproved": "Entry is not approved yet.",
"notopenyet": "Sorry, this activity is not available until {{$a}}",
"numrecords": "{{$a}} entries",
"other": "Other",
"recordapproved": "Entry approved",
"recorddeleted": "Entry deleted",
"recorddisapproved": "Entry unapproved",
"resetsettings": "Reset filters",
"search": "Search",
"searchbytagsnotsupported": "Sorry, searching by tags is not supported by the app.",
"selectedrequired": "All selected required",
"single": "View single",
"tagarea_data_records": "Data records",
"timeadded": "Time added",
"timemodified": "Time modified",
"usedate": "Include in search."
}

View File

@ -0,0 +1,40 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
</ion-buttons>
<ion-title>
<core-format-text [text]="title" contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
</core-format-text>
</ion-title>
<ion-buttons slot="end">
<ion-button *ngIf="entry" fill="clear" (click)="save($event)" [attr.aria-label]="'core.save' | translate">
{{ 'core.save' | translate }}
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<core-loading [hideUntil]="loaded">
<ion-item class="ion-text-wrap" *ngIf="groupInfo && (groupInfo.separateGroups || groupInfo.visibleGroups)">
<ion-label id="addon-data-groupslabel">
<ng-container *ngIf="groupInfo.separateGroups">{{ 'core.groupsvisible' | translate }}</ng-container>
<ng-container *ngIf="groupInfo.visibleGroups">{{ 'core.groupsseparate' | translate }}</ng-container>
</ion-label>
<ion-select [(ngModel)]="selectedGroup" (ionChange)="setGroup(selectedGroup)" aria-labelledby="addon-data-groupslabel"
interface="action-sheet">
<ion-select-option *ngFor="let groupOpt of groupInfo.groups" [value]="groupOpt.id">
{{groupOpt.name}}
</ion-select-option>
</ion-select>
</ion-item>
<div class="addon-data-contents {{cssClass}}" *ngIf="database">
<core-style [css]="database.csstemplate" prefix=".{{cssClass}}"></core-style>
<form (ngSubmit)="save($event)" [formGroup]="editForm" #editFormEl>
<core-compile-html [text]="editFormRender" [jsData]="jsData" [extraImports]="extraImports"></core-compile-html>
</form>
</div>
</core-loading>
</ion-content>

View File

@ -0,0 +1,451 @@
// (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, OnInit, ViewChild, ElementRef, Type } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { CoreError } from '@classes/errors/error';
import { CoreCourseModule } from '@features/course/services/course-helper';
import { CoreFileUploader } from '@features/fileuploader/services/fileuploader';
import { CoreTag } from '@features/tag/services/tag';
import { IonContent } from '@ionic/angular';
import { CoreGroupInfo, CoreGroups } from '@services/groups';
import { CoreNavigator } from '@services/navigator';
import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreForms } from '@singletons/form';
import { CoreUtils } from '@services/utils/utils';
import { Translate } from '@singletons';
import { CoreEvents } from '@singletons/events';
import { AddonModDataComponentsCompileModule } from '../../components/components-compile.module';
import {
AddonModDataData,
AddonModDataField,
AddonModDataProvider,
AddonModData,
AddonModDataTemplateType,
AddonModDataEntry,
AddonModDataEntryFields,
AddonModDataEditEntryResult,
AddonModDataAddEntryResult,
AddonModDataEntryWSField,
} from '../../services/data';
import { AddonModDataHelper } from '../../services/data-helper';
/**
* Page that displays the view edit page.
*/
@Component({
selector: 'page-addon-mod-data-edit',
templateUrl: 'edit.html',
styleUrls: ['../../data.scss', '../../data-forms.scss'],
})
export class AddonModDataEditPage implements OnInit {
@ViewChild(IonContent) content?: IonContent;
@ViewChild('editFormEl') formElement!: ElementRef;
protected entryId?: number;
protected fieldsArray: AddonModDataField[] = [];
protected siteId: string;
protected offline = false;
protected forceLeave = false; // To allow leaving the page without checking for changes.
protected initialSelectedGroup?: number;
protected isEditing = false;
entry?: AddonModDataEntry;
fields: Record<number, AddonModDataField> = {};
courseId!: number;
module!: CoreCourseModule;
database?: AddonModDataData;
title = '';
component = AddonModDataProvider.COMPONENT;
loaded = false;
selectedGroup = 0;
cssClass = '';
groupInfo?: CoreGroupInfo;
editFormRender = '';
editForm: FormGroup;
extraImports: Type<unknown>[] = [AddonModDataComponentsCompileModule];
jsData? : {
fields: Record<number, AddonModDataField>;
database?: AddonModDataData;
contents: AddonModDataEntryFields;
errors?: Record<number, string>;
form: FormGroup;
};
errors: Record<number, string> = {};
constructor() {
this.siteId = CoreSites.getCurrentSiteId();
this.editForm = new FormGroup({});
}
/**
* @inheritdoc
*/
ngOnInit(): void {
this.module = CoreNavigator.getRouteParam<CoreCourseModule>('module')!;
this.entryId = CoreNavigator.getRouteNumberParam('entryId') || undefined;
this.courseId = CoreNavigator.getRouteNumberParam('courseId')!;
this.selectedGroup = CoreNavigator.getRouteNumberParam('group') || 0;
// If entryId is lower than 0 or null, it is a new entry or an offline entry.
this.isEditing = typeof this.entryId != 'undefined' && this.entryId > 0;
this.title = this.module.name;
this.fetchEntryData(true);
}
/**
* Check if we can leave the page or not and ask to confirm the lost of data.
*
* @return True if we can leave, false otherwise.
*/
async canLeave(): Promise<boolean> {
if (this.forceLeave || !this.entry) {
return true;
}
const inputData = this.editForm.value;
let changed = AddonModDataHelper.hasEditDataChanged(inputData, this.fieldsArray, this.entry.contents);
changed = changed || (!this.isEditing && this.initialSelectedGroup != this.selectedGroup);
if (changed) {
// Show confirmation if some data has been modified.
await CoreDomUtils.showConfirm(Translate.instant('coentryre.confirmcanceledit'));
}
// Delete the local files from the tmp folder.
const files = await AddonModDataHelper.getEditTmpFiles(inputData, this.fieldsArray, this.entry!.contents);
CoreFileUploader.clearTmpFiles(files);
CoreForms.triggerFormCancelledEvent(this.formElement, this.siteId);
return true;
}
/**
* Fetch the entry data.
*
* @param refresh To refresh all downloaded data.
* @return Resolved when done.
*/
protected async fetchEntryData(refresh = false): Promise<void> {
try {
this.database = await AddonModData.getDatabase(this.courseId, this.module.id);
this.title = this.database.name || this.title;
this.cssClass = 'addon-data-entries-' + this.database.id;
this.fieldsArray = await AddonModData.getFields(this.database.id, { cmId: this.module.id });
this.fields = CoreUtils.arrayToObject(this.fieldsArray, 'id');
const entry = await AddonModDataHelper.fetchEntry(this.database, this.fieldsArray, this.entryId || 0);
this.entry = entry.entry;
// Load correct group.
this.selectedGroup = this.entry.groupid;
// Check permissions when adding a new entry or offline entry.
if (!this.isEditing) {
let haveAccess = false;
if (refresh) {
this.groupInfo = await CoreGroups.getActivityGroupInfo(this.database.coursemodule);
this.selectedGroup = CoreGroups.validateGroupId(this.selectedGroup, this.groupInfo);
this.initialSelectedGroup = this.selectedGroup;
}
if (this.groupInfo?.groups && this.groupInfo.groups.length > 0) {
if (refresh) {
const canAddGroup: Record<number, boolean> = {};
await Promise.all(this.groupInfo.groups.map(async (group) => {
const accessData = await AddonModData.getDatabaseAccessInformation(this.database!.id, {
cmId: this.module.id, groupId: group.id });
canAddGroup[group.id] = accessData.canaddentry;
}));
this.groupInfo.groups = this.groupInfo.groups.filter((group) => !!canAddGroup[group.id]);
haveAccess = canAddGroup[this.selectedGroup];
} else {
// Groups already filtered, so it have access.
haveAccess = true;
}
} else {
const accessData = await AddonModData.getDatabaseAccessInformation(this.database.id, { cmId: this.module.id });
haveAccess = accessData.canaddentry;
}
if (!haveAccess) {
// You shall not pass, go back.
CoreDomUtils.showErrorModal('addon.mod_data.noaccess', true);
// Go back to entry list.
this.forceLeave = true;
CoreNavigator.back();
return;
}
}
this.editFormRender = this.displayEditFields();
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'core.course.errorgetmodule', true);
}
this.loaded = true;
}
/**
* Saves data.
*
* @param e Event.
* @return Resolved when done.
*/
async save(e: Event): Promise<void> {
e.preventDefault();
e.stopPropagation();
const inputData = this.editForm.value;
try {
let changed = AddonModDataHelper.hasEditDataChanged(
inputData,
this.fieldsArray,
this.entry?.contents || {},
);
changed = changed || (!this.isEditing && this.initialSelectedGroup != this.selectedGroup);
if (!changed) {
if (this.entryId) {
await this.returnToEntryList();
return;
}
// New entry, no changes means no field filled, warn the user.
throw new CoreError(Translate.instant('addon.mod_data.emptyaddform'));
}
const modal = await CoreDomUtils.showModalLoading('core.sending', true);
// Create an ID to assign files.
const entryTemp = this.entryId ? this.entryId : - (new Date().getTime());
let editData: AddonModDataEntryWSField[] = [];
try {
try {
editData = await AddonModDataHelper.getEditDataFromForm(
inputData,
this.fieldsArray,
this.database!.id,
entryTemp,
this.entry?.contents || {},
this.offline,
);
} catch (error) {
if (this.offline) {
throw error;
}
// Cannot submit in online, prepare for offline usage.
this.offline = true;
editData = await AddonModDataHelper.getEditDataFromForm(
inputData,
this.fieldsArray,
this.database!.id,
entryTemp,
this.entry?.contents || {},
this.offline,
);
}
if (editData.length <= 0) {
// No field filled, warn the user.
throw new CoreError(Translate.instant('addon.mod_data.emptyaddform'));
}
let updateEntryResult: AddonModDataEditEntryResult | AddonModDataAddEntryResult | undefined;
if (this.isEditing) {
updateEntryResult = await AddonModData.editEntry(
this.database!.id,
this.entryId!,
this.courseId,
editData,
this.fieldsArray,
this.siteId,
this.offline,
);
} else {
updateEntryResult = await AddonModData.addEntry(
this.database!.id,
entryTemp,
this.courseId,
editData,
this.selectedGroup,
this.fieldsArray,
this.siteId,
this.offline,
);
}
// This is done if entry is updated when editing or creating if not.
if ((this.isEditing && 'updated' in updateEntryResult && updateEntryResult.updated) ||
(!this.isEditing && 'newentryid' in updateEntryResult && updateEntryResult.newentryid)) {
CoreForms.triggerFormSubmittedEvent(this.formElement, updateEntryResult.sent, this.siteId);
const promises: Promise<void>[] = [];
if (updateEntryResult.sent) {
CoreEvents.trigger(CoreEvents.ACTIVITY_DATA_SENT, { module: 'data' });
if (this.isEditing) {
promises.push(AddonModData.invalidateEntryData(this.database!.id, this.entryId!, this.siteId));
}
promises.push(AddonModData.invalidateEntriesData(this.database!.id, this.siteId));
}
try {
await Promise.all(promises);
CoreEvents.trigger(
AddonModDataProvider.ENTRY_CHANGED,
{ dataId: this.database!.id, entryId: this.entryId },
this.siteId,
);
} finally {
this.returnToEntryList();
}
} else {
this.errors = {};
if (updateEntryResult.fieldnotifications) {
updateEntryResult.fieldnotifications.forEach((fieldNotif) => {
const field = this.fieldsArray.find((field) => field.name == fieldNotif.fieldname);
if (field) {
this.errors[field.id] = fieldNotif.notification;
}
});
}
this.jsData!.errors = this.errors;
setTimeout(() => {
this.scrollToFirstError();
});
}
} finally {
modal.dismiss();
}
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'Cannot edit entry', true);
}
}
/**
* Set group to see the database.
*
* @param groupId Group identifier to set.
* @return Resolved when done.
*/
setGroup(groupId: number): Promise<void> {
this.selectedGroup = groupId;
this.loaded = false;
return this.fetchEntryData();
}
/**
* Displays Edit Search Fields.
*
* @return Generated HTML.
*/
protected displayEditFields(): string {
this.jsData = {
fields: this.fields,
contents: CoreUtils.clone(this.entry?.contents) || {},
form: this.editForm,
database: this.database,
errors: this.errors,
};
let template = AddonModDataHelper.getTemplate(this.database!, AddonModDataTemplateType.ADD, this.fieldsArray);
// Replace the fields found on template.
this.fieldsArray.forEach((field) => {
let replace = '[[' + field.name + ']]';
replace = replace.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&');
let replaceRegEx = new RegExp(replace, 'gi');
// 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>';
template = template.replace(replaceRegEx, render);
// Replace the field id tag.
replace = '[[' + field.name + '#id]]';
replace = replace.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&');
replaceRegEx = new RegExp(replace, 'gi');
template = template.replace(replaceRegEx, 'field_' + field.id);
});
// Editing tags is not supported.
const replaceRegEx = new RegExp('##tags##', 'gi');
const message = CoreTag.areTagsAvailableInSite()
? '<p class="item-dimmed">{{ \'addon.mod_data.edittagsnotsupported\' | translate }}</p>'
: '';
template = template.replace(replaceRegEx, message);
return template;
}
/**
* Return to the entry list (previous page) discarding temp data.
*
* @return Resolved when done.
*/
protected async returnToEntryList(): Promise<void> {
const inputData = this.editForm.value;
try {
const files = await AddonModDataHelper.getEditTmpFiles(
inputData,
this.fieldsArray,
this.entry?.contents || {},
);
CoreFileUploader.clearTmpFiles(files);
} finally {
// Go back to entry list.
this.forceLeave = true;
CoreNavigator.back();
}
}
/**
* Scroll to first error or to the top if not found.
*/
protected scrollToFirstError(): void {
if (!CoreDomUtils.scrollToElementBySelector(this.formElement.nativeElement, this.content, '.addon-data-error')) {
this.content?.scrollToTop();
}
}
}

View File

@ -0,0 +1,82 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
</ion-buttons>
<ion-title>
<core-format-text [text]="title" contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
</core-format-text>
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-refresher slot="fixed"
[disabled]="!entryLoaded || !(isPullingToRefresh || !renderingEntry && !loadingRating && !loadingComments)"
(ionRefresh)="refreshDatabase($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="entryLoaded && (isPullingToRefresh || !renderingEntry && !loadingRating && !loadingComments)">
<!-- Database entries found to be synchronized -->
<ion-card class="core-warning-card" *ngIf="entry && entry.hasOffline">
<ion-item>
<ion-icon name="fas-exclamation-triangle" slot="start"></ion-icon>
<ion-label>{{ 'core.hasdatatosync' | translate: {$a: moduleName} }}</ion-label>
</ion-item>
</ion-card>
<ion-item class="ion-text-wrap" *ngIf="groupInfo && (groupInfo.separateGroups || groupInfo.visibleGroups)">
<ion-label id="addon-data-groupslabel">
<ng-container *ngIf="groupInfo.separateGroups">{{ 'core.groupsvisible' | translate }}</ng-container>
<ng-container *ngIf="groupInfo.visibleGroups">{{ 'core.groupsseparate' | translate }}</ng-container>
</ion-label>
<ion-select [(ngModel)]="selectedGroup" (ionChange)="setGroup(selectedGroup)" aria-labelledby="addon-data-groupslabel"
interface="action-sheet">
<ion-select-option *ngFor="let groupOpt of groupInfo.groups" [value]="groupOpt.id">
{{groupOpt.name}}
</ion-select-option>
</ion-select>
</ion-item>
<div class="addon-data-contents addon-data-entries-{{database.id}}" *ngIf="database && entry">
<core-style [css]="database.csstemplate" prefix=".addon-data-entries-{{database.id}}"></core-style>
<core-compile-html [text]="entryHtml" [jsData]="jsData" [extraImports]="extraImports"
(compiling)="setRenderingEntry($event)"></core-compile-html>
</div>
<core-rating-rate *ngIf="database && entry && ratingInfo && (!database.approval || entry.approved)"
[ratingInfo]="ratingInfo" contextLevel="module" [instanceId]="database.coursemodule" [itemId]="entry.id" [itemSetId]="0"
[courseId]="courseId" [aggregateMethod]="database.assessed" [scaleId]="database.scale" [userId]="entry.userid"
(onLoading)="setLoadingRating($event)" (onUpdate)="ratingUpdated()">
</core-rating-rate>
<core-rating-aggregate *ngIf="database && entry && ratingInfo" [ratingInfo]="ratingInfo" contextLevel="module"
[instanceId]="database.coursemodule" [itemId]="entry.id" [courseId]="courseId" [aggregateMethod]="database.assessed"
[scaleId]="database.scale">
</core-rating-aggregate>
<ion-item *ngIf="database && database.comments && entry && entry.id > 0 && commentsEnabled">
<ion-label>
<core-comments contextLevel="module" [instanceId]="database.coursemodule" component="mod_data" [itemId]="entry.id"
area="database_entry" [displaySpinner]="false" [courseId]="courseId" (onLoading)="setLoadingComments($event)">
</core-comments>
</ion-label>
</ion-item>
<ion-grid *ngIf="hasPrevious || hasNext">
<ion-row class="ion-align-items-center">
<ion-col *ngIf="hasPrevious">
<ion-button expand="block" fill="outline" (click)="gotoEntry(offset! -1)">
<ion-icon name="fas-chevron-left" slot="start"></ion-icon>
{{ 'core.previous' | translate }}
</ion-button>
</ion-col>
<ion-col *ngIf="hasNext">
<ion-button expand="block" (click)="gotoEntry(offset! + 1)">
{{ 'core.next' | translate }}
<ion-icon name="fas-chevron-right" slot="end"></ion-icon>
</ion-button>
</ion-col>
</ion-row>
</ion-grid>
</core-loading>
</ion-content>

View File

@ -0,0 +1,414 @@
// (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, OnDestroy, ViewChild, ChangeDetectorRef, OnInit, Type } from '@angular/core';
import { CoreCommentsCommentsComponent } from '@features/comments/components/comments/comments';
import { CoreComments } from '@features/comments/services/comments';
import { CoreCourse } from '@features/course/services/course';
import { CoreCourseModule } from '@features/course/services/course-helper';
import { CoreRatingInfo } from '@features/rating/services/rating';
import { IonContent, IonRefresher } from '@ionic/angular';
import { CoreGroups, CoreGroupInfo } from '@services/groups';
import { CoreNavigator } from '@services/navigator';
import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreUtils } from '@services/utils/utils';
import { CoreEventObserver, CoreEvents } from '@singletons/events';
import { AddonModDataComponentsCompileModule } from '../../components/components-compile.module';
import { AddonModDataProvider,
AddonModData,
AddonModDataData,
AddonModDataGetDataAccessInformationWSResponse,
AddonModDataField,
AddonModDataTemplateType,
AddonModDataTemplateMode,
AddonModDataEntry,
} from '../../services/data';
import { AddonModDataHelper } from '../../services/data-helper';
import { AddonModDataSyncProvider } from '../../services/data-sync';
/**
* Page that displays the view entry page.
*/
@Component({
selector: 'page-addon-mod-data-entry',
templateUrl: 'entry.html',
styleUrls: ['../../data.scss'],
})
export class AddonModDataEntryPage implements OnInit, OnDestroy {
@ViewChild(IonContent) content?: IonContent;
@ViewChild(CoreCommentsCommentsComponent) comments?: CoreCommentsCommentsComponent;
protected entryId?: number;
protected syncObserver: CoreEventObserver; // It will observe the sync auto event.
protected entryChangedObserver: CoreEventObserver; // It will observe the changed entry event.
protected fields: Record<number, AddonModDataField> = {};
protected fieldsArray: AddonModDataField[] = [];
module!: CoreCourseModule;
courseId!: number;
offset?: number;
title = '';
moduleName = 'data';
component = AddonModDataProvider.COMPONENT;
entryLoaded = false;
renderingEntry = false;
loadingComments = false;
loadingRating = false;
selectedGroup = 0;
entry?: AddonModDataEntry;
hasPrevious = false;
hasNext = false;
access?: AddonModDataGetDataAccessInformationWSResponse;
database?: AddonModDataData;
groupInfo?: CoreGroupInfo;
showComments = false;
entryHtml = '';
siteId: string;
extraImports: Type<unknown>[] = [AddonModDataComponentsCompileModule];
jsData? : {
fields: Record<number, AddonModDataField>;
entries: Record<number, AddonModDataEntry>;
database: AddonModDataData;
module: CoreCourseModule;
group: number;
};
ratingInfo?: CoreRatingInfo;
isPullingToRefresh = false; // Whether the last fetching of data was started by a pull-to-refresh action
commentsEnabled = false;
constructor(
private cdr: ChangeDetectorRef,
) {
this.moduleName = CoreCourse.translateModuleName('data');
this.siteId = CoreSites.getCurrentSiteId();
// Refresh data if this discussion is synchronized automatically.
this.syncObserver = CoreEvents.on(AddonModDataSyncProvider.AUTO_SYNCED, (data) => {
if (typeof data.entryId == 'undefined') {
return;
}
if ((data.entryId == this.entryId || data.offlineEntryId == this.entryId) && this.database?.id == data.dataId) {
if (data.deleted) {
// If deleted, go back.
CoreNavigator.back();
} else {
this.entryId = data.entryId;
this.entryLoaded = false;
this.fetchEntryData(true);
}
}
}, this.siteId);
// Refresh entry on change.
this.entryChangedObserver = CoreEvents.on(AddonModDataProvider.ENTRY_CHANGED, (data) => {
if (data.entryId == this.entryId && this.database?.id == data.dataId) {
if (data.deleted) {
// If deleted, go back.
CoreNavigator.back();
} else {
this.entryLoaded = false;
this.fetchEntryData(true);
}
}
}, this.siteId);
}
/**
* @inheritdoc
*/
async ngOnInit(): Promise<void> {
this.module = CoreNavigator.getRouteParam<CoreCourseModule>('module')!;
this.entryId = CoreNavigator.getRouteNumberParam('entryId') || undefined;
this.courseId = CoreNavigator.getRouteNumberParam('courseId')!;
this.selectedGroup = CoreNavigator.getRouteNumberParam('group') || 0;
this.offset = CoreNavigator.getRouteNumberParam('offset');
this.title = this.module.name;
this.commentsEnabled = !CoreComments.areCommentsDisabledInSite();
await this.fetchEntryData();
this.logView();
}
/**
* Fetch the entry data.
*
* @param refresh Whether to refresh the current data or not.
* @param isPtr Whether is a pull to refresh action.
* @return Resolved when done.
*/
protected async fetchEntryData(refresh = false, isPtr = false): Promise<void> {
this.isPullingToRefresh = isPtr;
try {
this.database = await AddonModData.getDatabase(this.courseId, this.module.id);
this.title = this.database.name || this.title;
this.fieldsArray = await AddonModData.getFields(this.database.id, { cmId: this.module.id });
this.fields = CoreUtils.arrayToObject(this.fieldsArray, 'id');
await this.setEntryFromOffset();
this.access = await AddonModData.getDatabaseAccessInformation(this.database.id, { cmId: this.module.id });
this.groupInfo = await CoreGroups.getActivityGroupInfo(this.database.coursemodule);
this.selectedGroup = CoreGroups.validateGroupId(this.selectedGroup, this.groupInfo);
const actions = AddonModDataHelper.getActions(this.database, this.access, this.entry!);
const template = AddonModDataHelper.getTemplate(this.database, AddonModDataTemplateType.SINGLE, this.fieldsArray);
this.entryHtml = AddonModDataHelper.displayShowFields(
template,
this.fieldsArray,
this.entry!,
this.offset,
AddonModDataTemplateMode.SHOW,
actions,
);
this.showComments = actions.comments;
const entries: Record<number, AddonModDataEntry> = {};
entries[this.entryId!] = this.entry!;
// Pass the input data to the component.
this.jsData = {
fields: this.fields,
entries: entries,
database: this.database,
module: this.module,
group: this.selectedGroup,
};
} catch (error) {
if (!refresh) {
// Some call failed, retry without using cache since it might be a new activity.
return this.refreshAllData(isPtr);
}
CoreDomUtils.showErrorModalDefault(error, 'core.course.errorgetmodule', true);
} finally {
this.content?.scrollToTop();
this.entryLoaded = true;
}
}
/**
* Go to selected entry without changing state.
*
* @param offset Entry offset.
* @return Resolved when done.
*/
async gotoEntry(offset: number): Promise<void> {
this.offset = offset;
this.entryId = undefined;
this.entry = undefined;
this.entryLoaded = false;
await this.fetchEntryData();
this.logView();
}
/**
* Refresh all the data.
*
* @param isPtr Whether is a pull to refresh action.
* @return Promise resolved when done.
*/
protected async refreshAllData(isPtr?: boolean): Promise<void> {
const promises: Promise<void>[] = [];
promises.push(AddonModData.invalidateDatabaseData(this.courseId));
if (this.database) {
promises.push(AddonModData.invalidateEntryData(this.database.id, this.entryId!));
promises.push(CoreGroups.invalidateActivityGroupInfo(this.database.coursemodule));
promises.push(AddonModData.invalidateEntriesData(this.database.id));
promises.push(AddonModData.invalidateFieldsData(this.database.id));
if (this.database.comments && this.entry && this.entry.id > 0 && this.commentsEnabled && this.comments) {
// Refresh comments. Don't add it to promises because we don't want the comments fetch to block the entry fetch.
this.comments.doRefresh().catch(() => {
// Ignore errors.
});
}
}
await Promise.all(promises).finally(() =>
this.fetchEntryData(true, isPtr));
}
/**
* Refresh the data.
*
* @param refresher Refresher.
* @return Promise resolved when done.
*/
refreshDatabase(refresher?: IonRefresher): void {
if (!this.entryLoaded) {
return;
}
this.refreshAllData(true).finally(() => {
refresher?.complete();
});
}
/**
* Set group to see the database.
*
* @param groupId Group identifier to set.
* @return Resolved when done.
*/
async setGroup(groupId: number): Promise<void> {
this.selectedGroup = groupId;
this.offset = undefined;
this.entry = undefined;
this.entryId = undefined;
this.entryLoaded = false;
await this.fetchEntryData();
this.logView();
}
/**
* Convenience function to fetch the entry and set next/previous entries.
*
* @return Resolved when done.
*/
protected async setEntryFromOffset(): Promise<void> {
if (typeof this.offset == 'undefined' && typeof this.entryId != 'undefined') {
// Entry id passed as navigation parameter instead of the offset.
// We don't display next/previous buttons in this case.
this.hasNext = false;
this.hasPrevious = false;
const entry = await AddonModDataHelper.fetchEntry(this.database!, this.fieldsArray, this.entryId);
this.entry = entry.entry;
this.ratingInfo = entry.ratinginfo;
return;
}
const perPage = AddonModDataProvider.PER_PAGE;
const page = typeof this.offset != 'undefined' && this.offset >= 0
? Math.floor(this.offset / perPage)
: 0;
const entries = await AddonModDataHelper.fetchEntries(this.database!, this.fieldsArray, {
groupId: this.selectedGroup,
sort: 0,
order: 'DESC',
page,
perPage,
});
const pageEntries = (entries.offlineEntries || []).concat(entries.entries);
// Index of the entry when concatenating offline and online page entries.
let pageIndex = 0;
if (typeof this.offset == 'undefined') {
// No offset passed, display the first entry.
pageIndex = 0;
} else if (this.offset > 0) {
// Online entry.
pageIndex = this.offset % perPage + (entries.offlineEntries?.length || 0);
} else {
// Offline entry.
pageIndex = this.offset + (entries.offlineEntries?.length || 0);
}
this.entry = pageEntries[pageIndex];
this.entryId = this.entry.id;
this.hasPrevious = page > 0 || pageIndex > 0;
if (pageIndex + 1 < pageEntries.length) {
// Not the last entry on the page;
this.hasNext = true;
} else if (pageEntries.length < perPage) {
// Last entry of the last page.
this.hasNext = false;
} else {
// Last entry of the page, check if there are more pages.
const entries = await AddonModData.getEntries(this.database!.id, {
groupId: this.selectedGroup,
page: page + 1,
perPage: perPage,
});
this.hasNext = entries?.entries?.length > 0;
}
if (this.entryId > 0) {
// Online entry, we need to fetch the the rating info.
const entry = await AddonModData.getEntry(this.database!.id, this.entryId, { cmId: this.module.id });
this.ratingInfo = entry.ratinginfo;
}
}
/**
* Function called when entry is being rendered.
*/
setRenderingEntry(rendering: boolean): void {
this.renderingEntry = rendering;
this.cdr.detectChanges();
}
/**
* Function called when comments component is loading data.
*/
setLoadingComments(loading: boolean): void {
this.loadingComments = loading;
this.cdr.detectChanges();
}
/**
* Function called when rate component is loading data.
*/
setLoadingRating(loading: boolean): void {
this.loadingRating = loading;
this.cdr.detectChanges();
}
/**
* Function called when rating is updated online.
*/
ratingUpdated(): void {
AddonModData.invalidateEntryData(this.database!.id, this.entryId!);
}
/**
* Log viewing the activity.
*
* @return Promise resolved when done.
*/
protected async logView(): Promise<void> {
if (!this.database || !this.database.id) {
return;
}
await CoreUtils.ignoreErrors(AddonModData.logView(this.database.id, this.database.name));
}
/**
* Component being destroyed.
*/
ngOnDestroy(): void {
this.syncObserver?.off();
this.entryChangedObserver?.off();
}
}

View File

@ -0,0 +1,23 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
</ion-buttons>
<ion-title>
<core-format-text [text]="title" contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
</core-format-text>
</ion-title>
<ion-buttons slot="end">
<!-- The buttons defined by the component will be added in here. -->
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-refresher slot="fixed" [disabled]="!activityComponent?.loaded" (ionRefresh)="activityComponent?.doRefresh($event)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<addon-mod-data-index [module]="module" [courseId]="courseId" [group]="group" (dataRetrieved)="updateData($event)">
</addon-mod-data-index>
</ion-content>

View File

@ -0,0 +1,41 @@
// (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, OnInit, ViewChild } from '@angular/core';
import { CoreCourseModuleMainActivityPage } from '@features/course/classes/main-activity-page';
import { CoreNavigator } from '@services/navigator';
import { AddonModDataIndexComponent } from '../../components/index/index';
/**
* Page that displays a data.
*/
@Component({
selector: 'page-addon-mod-data-index',
templateUrl: 'index.html',
})
export class AddonModDataIndexPage extends CoreCourseModuleMainActivityPage<AddonModDataIndexComponent> implements OnInit {
@ViewChild(AddonModDataIndexComponent) activityComponent?: AddonModDataIndexComponent;
group = 0;
/**
* Component being initialized.
*/
ngOnInit(): void {
super.ngOnInit();
this.group = CoreNavigator.getRouteNumberParam('group') || 0;
}
}

View File

@ -0,0 +1,267 @@
// (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 { Injectable, Type } from '@angular/core';
import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate';
import { AddonModDataDefaultFieldHandler } from './handlers/default-field';
import { makeSingleton } from '@singletons';
import { AddonModDataEntryField,
AddonModDataField,
AddonModDataSearchEntriesAdvancedFieldFormatted,
AddonModDataSubfieldData,
} from './data';
import { CoreFormFields } from '@singletons/form';
import { CoreWSExternalFile } from '@services/ws';
import { FileEntry } from '@ionic-native/file';
/**
* Interface that all fields handlers must implement.
*/
export interface AddonModDataFieldHandler extends CoreDelegateHandler {
/**
* Name of the type of data field the handler supports. E.g. 'checkbox'.
*/
type: string;
/**
* Return the Component to use to display the plugin data.
* It's recommended to return the class of the component, but you can also return an instance of the component.
*
* @param field The field object.
* @return The component to use, undefined if not found.
*/
getComponent?(plugin: AddonModDataField): Type<unknown> | undefined;
/**
* Get field search data in the input data.
*
* @param field Defines the field to be rendered.
* @param inputData Data entered in the search form.
* @return With name and value of the data to be sent.
*/
getFieldSearchData?(
field: AddonModDataField,
inputData: CoreFormFields,
): AddonModDataSearchEntriesAdvancedFieldFormatted[];
/**
* Get field edit data in the input data.
*
* @param field Defines the field to be rendered.
* @param inputData Data entered in the edit form.
* @return With name and value of the data to be sent.
*/
getFieldEditData?(
field: AddonModDataField,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
inputData: CoreFormFields<any>,
originalFieldData: AddonModDataEntryField,
): AddonModDataSubfieldData[];
/**
* Get field data in changed.
*
* @param field Defines the field to be rendered.
* @param inputData Data entered in the edit form.
* @param originalFieldData Original field entered data.
* @return If the field has changes.
*/
hasFieldDataChanged?(
field: AddonModDataField,
inputData: CoreFormFields,
originalFieldData: AddonModDataEntryField,
): boolean;
/**
* Get field edit files in the input data.
*
* @param field Defines the field..
* @return With name and value of the data to be sent.
*/
getFieldEditFiles?(
field: AddonModDataField,
inputData: CoreFormFields,
originalFieldData: AddonModDataEntryField,
): (CoreWSExternalFile | FileEntry)[];
/**
* Check and get field requeriments.
*
* @param field Defines the field to be rendered.
* @param inputData Data entered in the edit form.
* @return String with the notification or false.
*/
getFieldsNotifications?(field: AddonModDataField, inputData: AddonModDataSubfieldData[]): string | undefined;
/**
* Override field content data with offline submission.
*
* @param originalContent Original data to be overriden.
* @param offlineContent Array with all the offline data to override.
* @param offlineFiles Array with all the offline files in the field.
* @return Data overriden
*/
overrideData?(
originalContent: AddonModDataEntryField,
offlineContent: CoreFormFields,
offlineFiles?: FileEntry[],
): AddonModDataEntryField;
}
/**
* Delegate to register database fields handlers.
*/
@Injectable({ providedIn: 'root' })
export class AddonModDataFieldsDelegateService extends CoreDelegate<AddonModDataFieldHandler> {
protected handlerNameProperty = 'type';
constructor(
protected defaultHandler: AddonModDataDefaultFieldHandler,
) {
super('AddonModDataFieldsDelegate', true);
}
/**
* Get the component to use for a certain field field.
*
* @param field The field object.
* @return Promise resolved with the component to use, undefined if not found.
*/
getComponentForField(field: AddonModDataField): Promise<Type<unknown> | undefined> {
return Promise.resolve(this.executeFunctionOnEnabled(field.type, 'getComponent', [field]));
}
/**
* Get database data in the input data to search.
*
* @param field Defines the field to be rendered.
* @param inputData Data entered in the search form.
* @return Name and data field.
*/
getFieldSearchData(field: AddonModDataField, inputData: CoreFormFields): AddonModDataSearchEntriesAdvancedFieldFormatted[] {
return this.executeFunctionOnEnabled(field.type, 'getFieldSearchData', [field, inputData]) || [];
}
/**
* Get database data in the input data to add or update entry.
*
* @param field Defines the field to be rendered.
* @param inputData Data entered in the search form.
* @param originalFieldData Original field entered data.
* @return Name and data field.
*/
getFieldEditData(
field: AddonModDataField,
inputData: CoreFormFields,
originalFieldData: AddonModDataEntryField,
): AddonModDataSubfieldData[] {
return this.executeFunctionOnEnabled(field.type, 'getFieldEditData', [field, inputData, originalFieldData]) || [];
}
/**
* Get database data in the input files to add or update entry.
*
* @param field Defines the field to be rendered.
* @param inputData Data entered in the search form.
* @param originalFieldData Original field entered data.
* @return Name and data field.
*/
getFieldEditFiles(
field: AddonModDataField,
inputData: CoreFormFields,
originalFieldData: CoreFormFields,
): (CoreWSExternalFile | FileEntry)[] {
return this.executeFunctionOnEnabled(field.type, 'getFieldEditFiles', [field, inputData, originalFieldData]) || [];
}
/**
* Check and get field requeriments.
*
* @param field Defines the field to be rendered.
* @param inputData Data entered in the edit form.
* @return String with the notification or false.
*/
getFieldsNotifications(field: AddonModDataField, inputData: AddonModDataSubfieldData[]): string | undefined {
return this.executeFunctionOnEnabled(field.type, 'getFieldsNotifications', [field, inputData]);
}
/**
* Check if field type manage files or not.
*
* @param field Defines the field to be checked.
* @return If the field type manages files.
*/
hasFiles(field: AddonModDataField): boolean {
return this.hasFunction(field.type, 'getFieldEditFiles');
}
/**
* Check if the data has changed for a certain field.
*
* @param field Defines the field to be rendered.
* @param inputData Data entered in the search form.
* @param originalFieldData Original field entered data.
* @return If the field has changes.
*/
hasFieldDataChanged(
field: AddonModDataField,
inputData: CoreFormFields,
originalFieldData: CoreFormFields,
): boolean {
return !!this.executeFunctionOnEnabled(
field.type,
'hasFieldDataChanged',
[field, inputData, originalFieldData],
);
}
/**
* Check if a field plugin is supported.
*
* @param pluginType Type of the plugin.
* @return True if supported, false otherwise.
*/
isPluginSupported(pluginType: string): boolean {
return this.hasHandler(pluginType, true);
}
/**
* Override field content data with offline submission.
*
* @param field Defines the field to be rendered.
* @param originalContent Original data to be overriden.
* @param offlineContent Array with all the offline data to override.
* @param offlineFiles Array with all the offline files in the field.
* @return Data overriden
*/
overrideData(
field: AddonModDataField,
originalContent: AddonModDataEntryField,
offlineContent: CoreFormFields,
offlineFiles?: FileEntry[],
): AddonModDataEntryField {
originalContent = originalContent || {};
if (!offlineContent) {
return originalContent;
}
return this.executeFunctionOnEnabled(field.type, 'overrideData', [originalContent, offlineContent, offlineFiles]) ||
originalContent;
}
}
export const AddonModDataFieldsDelegate = makeSingleton(AddonModDataFieldsDelegateService);

View File

@ -0,0 +1,790 @@
// (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 { ContextLevel } from '@/core/constants';
import { Injectable } from '@angular/core';
import { CoreCourse } from '@features/course/services/course';
import { CoreFileUploader, CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader';
import { CoreRatingOffline } from '@features/rating/services/rating-offline';
import { FileEntry } from '@ionic-native/file';
import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreFormFields } from '@singletons/form';
import { CoreTextUtils } from '@services/utils/text';
import { CoreUtils } from '@services/utils/utils';
import { CoreWSExternalFile } from '@services/ws';
import { makeSingleton, Translate } from '@singletons';
import { CoreEvents } from '@singletons/events';
import {
AddonModDataEntry,
AddonModData,
AddonModDataProvider,
AddonModDataSearchEntriesOptions,
AddonModDataEntries,
AddonModDataEntryFields,
AddonModDataAction,
AddonModDataGetEntryFormatted,
AddonModDataData,
AddonModDataTemplateType,
AddonModDataGetDataAccessInformationWSResponse,
AddonModDataTemplateMode,
AddonModDataField,
AddonModDataEntryWSField,
} from './data';
import { AddonModDataFieldsDelegate } from './data-fields-delegate';
import { AddonModDataOffline, AddonModDataOfflineAction } from './data-offline';
/**
* Service that provides helper functions for datas.
*/
@Injectable({ providedIn: 'root' })
export class AddonModDataHelperProvider {
/**
* Returns the record with the offline actions applied.
*
* @param record Entry to modify.
* @param offlineActions Offline data with the actions done.
* @param fields Entry defined fields indexed by fieldid.
* @return Promise resolved when done.
*/
protected async applyOfflineActions(
record: AddonModDataEntry,
offlineActions: AddonModDataOfflineAction[],
fields: AddonModDataField[],
): Promise<AddonModDataEntry> {
const promises: Promise<void>[] = [];
offlineActions.forEach((action) => {
record.timemodified = action.timemodified;
record.hasOffline = true;
const offlineContents: Record<number, CoreFormFields> = {};
switch (action.action) {
case AddonModDataAction.APPROVE:
record.approved = true;
break;
case AddonModDataAction.DISAPPROVE:
record.approved = false;
break;
case AddonModDataAction.DELETE:
record.deleted = true;
break;
case AddonModDataAction.ADD:
case AddonModDataAction.EDIT:
record.groupid = action.groupid;
action.fields.forEach((offlineContent) => {
if (typeof offlineContents[offlineContent.fieldid] == 'undefined') {
offlineContents[offlineContent.fieldid] = {};
}
if (offlineContent.subfield) {
offlineContents[offlineContent.fieldid][offlineContent.subfield] =
CoreTextUtils.parseJSON(offlineContent.value);
} else {
offlineContents[offlineContent.fieldid][''] = CoreTextUtils.parseJSON(offlineContent.value);
}
});
// Override field contents.
fields.forEach((field) => {
if (AddonModDataFieldsDelegate.hasFiles(field)) {
promises.push(this.getStoredFiles(record.dataid, record.id, field.id).then((offlineFiles) => {
record.contents[field.id] = AddonModDataFieldsDelegate.overrideData(
field,
record.contents[field.id],
offlineContents[field.id],
offlineFiles,
);
record.contents[field.id].fieldid = field.id;
return;
}));
} else {
record.contents[field.id] = AddonModDataFieldsDelegate.overrideData(
field,
record.contents[field.id],
offlineContents[field.id],
);
record.contents[field.id].fieldid = field.id;
}
});
break;
default:
break;
}
});
await Promise.all(promises);
return record;
}
/**
* Approve or disapprove a database entry.
*
* @param dataId Database ID.
* @param entryId Entry ID.
* @param approve True to approve, false to disapprove.
* @param courseId Course ID. It not defined, it will be fetched.
* @param siteId Site ID. If not defined, current site.
*/
async approveOrDisapproveEntry(
dataId: number,
entryId: number,
approve: boolean,
courseId?: number,
siteId?: string,
): Promise<void> {
siteId = siteId || CoreSites.getCurrentSiteId();
const modal = await CoreDomUtils.showModalLoading('core.sending', true);
try {
courseId = await this.getActivityCourseIdIfNotSet(dataId, courseId, siteId);
try {
// Approve/disapprove entry.
await AddonModData.approveEntry(dataId, entryId, approve, courseId, siteId);
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_data.errorapproving', true);
throw error;
}
const promises: Promise<void>[] = [];
promises.push(AddonModData.invalidateEntryData(dataId, entryId, siteId));
promises.push(AddonModData.invalidateEntriesData(dataId, siteId));
await CoreUtils.ignoreErrors(Promise.all(promises));
CoreEvents.trigger(AddonModDataProvider.ENTRY_CHANGED, { dataId: dataId, entryId: entryId }, siteId);
CoreDomUtils.showToast(approve ? 'addon.mod_data.recordapproved' : 'addon.mod_data.recorddisapproved', true, 3000);
} catch {
// Ignore error, it was already displayed.
} finally {
modal.dismiss();
}
}
/**
* Displays fields for being shown.
*
* @param template Template HMTL.
* @param fields Fields that defines every content in the entry.
* @param entry Entry.
* @param offset Entry offset.
* @param mode Mode list or show.
* @param actions Actions that can be performed to the record.
* @return Generated HTML.
*/
displayShowFields(
template: string,
fields: AddonModDataField[],
entry: AddonModDataEntry,
offset = 0,
mode: AddonModDataTemplateMode,
actions: Record<AddonModDataAction, boolean>,
): string {
if (!template) {
return '';
}
// Replace the fields found on template.
fields.forEach((field) => {
let replace = '[[' + field.name + ']]';
replace = replace.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&');
const replaceRegex = new RegExp(replace, 'gi');
// 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>';
template = template.replace(replaceRegex, render);
});
for (const action in actions) {
const replaceRegex = new RegExp('##' + action + '##', 'gi');
// Is enabled?
if (actions[action]) {
let render = '';
if (action == AddonModDataAction.MOREURL) {
// Render more url directly because it can be part of an HTML attribute.
render = CoreSites.getCurrentSite()!.getURL() + '/mod/data/view.php?d={{database.id}}&rid=' + entry.id;
} else if (action == 'approvalstatus') {
render = Translate.instant('addon.mod_data.' + (entry.approved ? 'approved' : 'notapproved'));
} else {
render = '<addon-mod-data-action action="' + action + '" [entry]="entries[' + entry.id + ']" mode="' + mode +
'" [database]="database" [module]="module" [offset]="' + offset + '" [group]="group" ></addon-mod-data-action>';
}
template = template.replace(replaceRegex, render);
} else {
template = template.replace(replaceRegex, '');
}
}
return template;
}
/**
* Get online and offline entries, or search entries.
*
* @param database Database object.
* @param fields The fields that define the contents.
* @param options Other options.
* @return Promise resolved when the database is retrieved.
*/
async fetchEntries(
database: AddonModDataData,
fields: AddonModDataField[],
options: AddonModDataSearchEntriesOptions = {},
): Promise<AddonModDataEntries> {
const site = await CoreSites.getSite(options.siteId);
options.groupId = options.groupId || 0;
options.page = options.page || 0;
const offlineActions: Record<number, AddonModDataOfflineAction[]> = {};
const result: AddonModDataEntries = {
entries: [],
totalcount: 0,
offlineEntries: [],
};
options.siteId = site.id;
const offlinePromise = AddonModDataOffline.getDatabaseEntries(database.id, site.id).then((actions) => {
result.hasOfflineActions = !!actions.length;
actions.forEach((action) => {
if (typeof offlineActions[action.entryid] == 'undefined') {
offlineActions[action.entryid] = [];
}
offlineActions[action.entryid].push(action);
// We only display new entries in the first page when not searching.
if (action.action == AddonModDataAction.ADD && options.page == 0 && !options.search && !options.advSearch &&
(!action.groupid || !options.groupId || action.groupid == options.groupId)) {
result.offlineEntries!.push({
id: action.entryid,
canmanageentry: true,
approved: !database.approval || database.manageapproved,
dataid: database.id,
groupid: action.groupid,
timecreated: -action.entryid,
timemodified: -action.entryid,
userid: site.getUserId(),
fullname: site.getInfo()?.fullname,
contents: {},
});
}
});
// Sort offline entries by creation time.
result.offlineEntries!.sort((a, b) => b.timecreated - a.timecreated);
return;
});
const ratingsPromise = CoreRatingOffline.hasRatings('mod_data', 'entry', ContextLevel.MODULE, database.coursemodule)
.then((hasRatings) => {
result.hasOfflineRatings = hasRatings;
return;
});
let fetchPromise: Promise<void>;
if (options.search || options.advSearch) {
fetchPromise = AddonModData.searchEntries(database.id, options).then((searchResult) => {
result.entries = searchResult.entries;
result.totalcount = searchResult.totalcount;
result.maxcount = searchResult.maxcount;
return;
});
} else {
fetchPromise = AddonModData.getEntries(database.id, options).then((entriesResult) => {
result.entries = entriesResult.entries;
result.totalcount = entriesResult.totalcount;
return;
});
}
await Promise.all([offlinePromise, ratingsPromise, fetchPromise]);
// Apply offline actions to online and offline entries.
const promises: Promise<AddonModDataEntry>[] = [];
result.entries.forEach((entry) => {
promises.push(this.applyOfflineActions(entry, offlineActions[entry.id] || [], fields));
});
result.offlineEntries!.forEach((entry) => {
promises.push(this.applyOfflineActions(entry, offlineActions[entry.id] || [], fields));
});
await Promise.all(promises);
return result;
}
/**
* Fetch an online or offline entry.
*
* @param database Database.
* @param fields List of database fields.
* @param entryId Entry ID.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with the entry.
*/
async fetchEntry(
database: AddonModDataData,
fields: AddonModDataField[],
entryId: number,
siteId?: string,
): Promise<AddonModDataGetEntryFormatted> {
const site = await CoreSites.getSite(siteId);
const offlineActions = await AddonModDataOffline.getEntryActions(database.id, entryId, site.id);
let response: AddonModDataGetEntryFormatted;
if (entryId > 0) {
// Online entry.
response = await AddonModData.getEntry(database.id, entryId, { cmId: database.coursemodule, siteId: site.id });
} else {
// Offline entry or new entry.
response = {
entry: {
id: entryId,
userid: site.getUserId(),
groupid: 0,
dataid: database.id,
timecreated: -entryId,
timemodified: -entryId,
approved: !database.approval || database.manageapproved,
canmanageentry: true,
fullname: site.getInfo()?.fullname,
contents: {},
},
};
}
await this.applyOfflineActions(response.entry, offlineActions, fields);
return response;
}
/**
* Returns an object with all the actions that the user can do over the record.
*
* @param database Database activity.
* @param accessInfo Access info to the activity.
* @param entry Entry or record where the actions will be performed.
* @return Keyed with the action names and boolean to evalute if it can or cannot be done.
*/
getActions(
database: AddonModDataData,
accessInfo: AddonModDataGetDataAccessInformationWSResponse,
entry: AddonModDataEntry,
): Record<AddonModDataAction, boolean> {
return {
add: false, // Not directly used on entries.
more: true,
moreurl: true,
user: true,
userpicture: true,
timeadded: true,
timemodified: true,
tags: true,
edit: entry.canmanageentry && !entry.deleted, // This already checks capabilities and readonly period.
delete: entry.canmanageentry,
approve: database.approval && accessInfo.canapprove && !entry.approved && !entry.deleted,
disapprove: database.approval && accessInfo.canapprove && entry.approved && !entry.deleted,
approvalstatus: database.approval,
comments: database.comments,
// Unsupported actions.
delcheck: false,
export: false,
};
}
/**
* Convenience function to get the course id of the database.
*
* @param dataId Database id.
* @param courseId Course id, if known.
* @param siteId Site id, if not set, current site will be used.
* @return Resolved with course Id when done.
*/
protected async getActivityCourseIdIfNotSet(dataId: number, courseId?: number, siteId?: string): Promise<number> {
if (courseId) {
return courseId;
}
const module = await CoreCourse.getModuleBasicInfoByInstance(dataId, 'data', siteId);
return module.course;
}
/**
* Returns the default template of a certain type.
*
* Based on Moodle function data_generate_default_template.
*
* @param type Type of template.
* @param fields List of database fields.
* @return Template HTML.
*/
getDefaultTemplate(type: AddonModDataTemplateType, fields: AddonModDataField[]): string {
if (type == AddonModDataTemplateType.LIST_HEADER || type == AddonModDataTemplateType.LIST_FOOTER) {
return '';
}
const html: string[] = [];
if (type == AddonModDataTemplateType.LIST) {
html.push('##delcheck##<br />');
}
html.push(
'<div class="defaulttemplate">',
'<table class="mod-data-default-template ##approvalstatus##">',
'<tbody>',
);
fields.forEach((field) => {
html.push(
'<tr class="">',
'<td class="template-field cell c0" style="">',
field.name,
': </td>',
'<td class="template-token cell c1 lastcol" style="">[[',
field.name,
']]</td>',
'</tr>',
);
});
if (type == AddonModDataTemplateType.LIST) {
html.push(
'<tr class="lastrow">',
'<td class="controls template-field cell c0 lastcol" style="" colspan="2">',
'##edit## ##more## ##delete## ##approve## ##disapprove## ##export##',
'</td>',
'</tr>',
);
} else if (type == AddonModDataTemplateType.SINGLE) {
html.push(
'<tr class="lastrow">',
'<td class="controls template-field cell c0 lastcol" style="" colspan="2">',
'##edit## ##delete## ##approve## ##disapprove## ##export##',
'</td>',
'</tr>',
);
} else if (type == AddonModDataTemplateType.SEARCH) {
html.push(
'<tr class="searchcontrols">',
'<td class="template-field cell c0" style="">Author first name: </td>',
'<td class="template-token cell c1 lastcol" style="">##firstname##</td>',
'</tr>',
'<tr class="searchcontrols lastrow">',
'<td class="template-field cell c0" style="">Author surname: </td>',
'<td class="template-token cell c1 lastcol" style="">##lastname##</td>',
'</tr>',
);
}
html.push(
'</tbody>',
'</table>',
'</div>',
);
if (type == AddonModDataTemplateType.LIST) {
html.push('<hr />');
}
return html.join('');
}
/**
* Retrieve the entered data in the edit form.
* We don't use ng-model because it doesn't detect changes done by JavaScript.
*
* @param inputData Array with the entered form values.
* @param fields Fields that defines every content in the entry.
* @param dataId Database Id. If set, files will be uploaded and itemId set.
* @param entryId Entry Id.
* @param entryContents Original entry contents.
* @param offline True to prepare the data for an offline uploading, false otherwise.
* @param siteId Site ID. If not defined, current site.
* @return That contains object with the answers.
*/
async getEditDataFromForm(
inputData: CoreFormFields,
fields: AddonModDataField[],
dataId: number,
entryId: number,
entryContents: AddonModDataEntryFields,
offline: boolean = false,
siteId?: string,
): Promise<AddonModDataEntryWSField[]> {
if (!inputData) {
return [];
}
siteId = siteId || CoreSites.getCurrentSiteId();
// Filter and translate fields to each field plugin.
const entryFieldDataToSend: AddonModDataEntryWSField[] = [];
const promises = fields.map(async (field) => {
const fieldData = AddonModDataFieldsDelegate.getFieldEditData(field, inputData, entryContents[field.id]);
if (!fieldData) {
return;
}
const proms = fieldData.map(async (fieldSubdata) => {
let value = fieldSubdata.value;
// Upload Files if asked.
if (dataId && fieldSubdata.files) {
value = await this.uploadOrStoreFiles(
dataId,
0,
entryId,
fieldSubdata.fieldid,
fieldSubdata.files,
offline,
siteId,
);
}
// WS wants values in JSON format.
entryFieldDataToSend.push({
fieldid: fieldSubdata.fieldid,
subfield: fieldSubdata.subfield || '',
value: value ? JSON.stringify(value) : '',
});
return;
});
await Promise.all(proms);
});
await Promise.all(promises);
return entryFieldDataToSend;
}
/**
* Retrieve the temp files to be updated.
*
* @param inputData Array with the entered form values.
* @param fields Fields that defines every content in the entry.
* @param entryContents Original entry contents indexed by field id.
* @return That contains object with the files.
*/
async getEditTmpFiles(
inputData: CoreFormFields,
fields: AddonModDataField[],
entryContents: AddonModDataEntryFields,
): Promise<(CoreWSExternalFile | FileEntry)[]> {
if (!inputData) {
return [];
}
// Filter and translate fields to each field plugin.
const promises = fields.map((field) =>
AddonModDataFieldsDelegate.getFieldEditFiles(field, inputData, entryContents[field.id]));
const fieldsFiles = await Promise.all(promises);
return fieldsFiles.reduce((files, fieldFiles) => files.concat(fieldFiles), []);
}
/**
* Get a list of stored attachment files for a new entry. See $mmaModDataHelper#storeFiles.
*
* @param dataId Database ID.
* @param entryId Entry ID or, if creating, timemodified.
* @param fieldId Field ID.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with the files.
*/
async getStoredFiles(dataId: number, entryId: number, fieldId: number, siteId?: string): Promise<FileEntry[]> {
const folderPath = await AddonModDataOffline.getEntryFieldFolder(dataId, entryId, fieldId, siteId);
try {
return CoreFileUploader.getStoredFiles(folderPath);
} catch {
// Ignore not found files.
return [];
}
}
/**
* Returns the template of a certain type.
*
* @param data Database object.
* @param type Type of template.
* @param fields List of database fields.
* @return Template HTML.
*/
getTemplate(data: AddonModDataData, type: AddonModDataTemplateType, fields: AddonModDataField[]): string {
let template = data[type] || this.getDefaultTemplate(type, fields);
if (type != AddonModDataTemplateType.LIST_HEADER && type != AddonModDataTemplateType.LIST_FOOTER) {
// Try to fix syntax errors so the template can be parsed by Angular.
template = CoreDomUtils.fixHtml(template);
}
// Add core-link directive to links.
template = template.replace(
/<a ([^>]*href="[^>]*)>/ig,
(match, attributes) => '<a core-link capture="true" ' + attributes + '>',
);
return template;
}
/**
* Check if data has been changed by the user.
*
* @param inputData Object with the entered form values.
* @param fields Fields that defines every content in the entry.
* @param dataId Database Id. If set, fils will be uploaded and itemId set.
* @param entryContents Original entry contents indexed by field id.
* @return True if changed, false if not.
*/
hasEditDataChanged(
inputData: CoreFormFields,
fields: AddonModDataField[],
entryContents: AddonModDataEntryFields,
): boolean {
return fields.some((field) =>
AddonModDataFieldsDelegate.hasFieldDataChanged(field, inputData, entryContents[field.id]));
}
/**
* Displays a confirmation modal for deleting an entry.
*
* @param dataId Database ID.
* @param entryId Entry ID.
* @param courseId Course ID. It not defined, it will be fetched.
* @param siteId Site ID. If not defined, current site.
*/
async showDeleteEntryModal(dataId: number, entryId: number, courseId?: number, siteId?: string): Promise<void> {
siteId = siteId || CoreSites.getCurrentSiteId();
try {
await CoreDomUtils.showDeleteConfirm('addon.mod_data.confirmdeleterecord');
const modal = await CoreDomUtils.showModalLoading();
try {
if (entryId > 0) {
courseId = await this.getActivityCourseIdIfNotSet(dataId, courseId, siteId);
}
AddonModData.deleteEntry(dataId, entryId, courseId!, siteId);
} catch (message) {
CoreDomUtils.showErrorModalDefault(message, 'addon.mod_data.errordeleting', true);
modal.dismiss();
return;
}
try {
await AddonModData.invalidateEntryData(dataId, entryId, siteId);
await AddonModData.invalidateEntriesData(dataId, siteId);
} catch (error) {
// Ignore errors.
}
CoreEvents.trigger(AddonModDataProvider.ENTRY_CHANGED, { dataId, entryId, deleted: true }, siteId);
CoreDomUtils.showToast('addon.mod_data.recorddeleted', true, 3000);
modal.dismiss();
} catch {
// Ignore error, it was already displayed.
}
}
/**
* Given a list of files (either online files or local files), store the local files in a local folder
* to be submitted later.
*
* @param dataId Database ID.
* @param entryId Entry ID or, if creating, timemodified.
* @param fieldId Field ID.
* @param files List of files.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved if success, rejected otherwise.
*/
async storeFiles(
dataId: number,
entryId: number,
fieldId: number,
files: (CoreWSExternalFile | FileEntry)[],
siteId?: string,
): Promise<CoreFileUploaderStoreFilesResult> {
// Get the folder where to store the files.
const folderPath = await AddonModDataOffline.getEntryFieldFolder(dataId, entryId, fieldId, siteId);
return CoreFileUploader.storeFilesToUpload(folderPath, files);
}
/**
* Upload or store some files, depending if the user is offline or not.
*
* @param dataId Database ID.
* @param itemId Draft ID to use. Undefined or 0 to create a new draft ID.
* @param entryId Entry ID or, if creating, timemodified.
* @param fieldId Field ID.
* @param files List of files.
* @param offline True if files sould be stored for offline, false to upload them.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with the itemId for the uploaded file/s.
*/
async uploadOrStoreFiles(
dataId: number,
itemId: number = 0,
entryId: number,
fieldId: number,
files: (CoreWSExternalFile | FileEntry)[],
offline: boolean,
siteId?: string,
): Promise<number | CoreFileUploaderStoreFilesResult> {
if (!files.length) {
return 0;
}
if (offline) {
return this.storeFiles(dataId, entryId, fieldId, files, siteId);
}
return CoreFileUploader.uploadOrReuploadFiles(files, AddonModDataProvider.COMPONENT, itemId, siteId);
}
}
export const AddonModDataHelper = makeSingleton(AddonModDataHelperProvider);

View File

@ -0,0 +1,290 @@
// (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 { Injectable } from '@angular/core';
import { CoreFileUploader, CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader';
import { CoreFile } from '@services/file';
import { CoreSites } from '@services/sites';
import { CoreTextUtils } from '@services/utils/text';
import { CoreUtils } from '@services/utils/utils';
import { makeSingleton } from '@singletons';
import { AddonModDataAction, AddonModDataEntryWSField } from './data';
import { AddonModDataEntryDBRecord, DATA_ENTRY_TABLE } from './database/data';
/**
* Service to handle Offline data.
*/
@Injectable({ providedIn: 'root' })
export class AddonModDataOfflineProvider {
/**
* Delete all the actions of an entry.
*
* @param dataId Database ID.
* @param entryId Database entry ID.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved if deleted, rejected if failure.
*/
async deleteAllEntryActions(dataId: number, entryId: number, siteId?: string): Promise<void> {
const actions = await this.getEntryActions(dataId, entryId, siteId);
const promises = actions.map((action) => {
this.deleteEntry(dataId, entryId, action.action, siteId);
});
await Promise.all(promises);
}
/**
* Delete an stored entry.
*
* @param dataId Database ID.
* @param entryId Database entry Id.
* @param action Action to be done
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved if deleted, rejected if failure.
*/
async deleteEntry(dataId: number, entryId: number, action: AddonModDataAction, siteId?: string): Promise<void> {
const site = await CoreSites.getSite(siteId);
await this.deleteEntryFiles(dataId, entryId, action, site.id);
await site.getDb().deleteRecords(DATA_ENTRY_TABLE, {
dataid: dataId,
entryid: entryId,
action,
});
}
/**
* Delete entry offline files.
*
* @param dataId Database ID.
* @param entryId Database entry ID.
* @param action Action to be done.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved if deleted, rejected if failure.
*/
protected async deleteEntryFiles(dataId: number, entryId: number, action: AddonModDataAction, siteId?: string): Promise<void> {
const site = await CoreSites.getSite(siteId);
const entry = await CoreUtils.ignoreErrors(this.getEntry(dataId, entryId, action, site.id));
if (!entry || !entry.fields) {
// Entry not found or no fields, ignore.
return;
}
const promises: Promise<void>[] = [];
entry.fields.forEach((field) => {
const value = CoreTextUtils.parseJSON<CoreFileUploaderStoreFilesResult>(field.value);
if (!value.offline) {
return;
}
const promise = this.getEntryFieldFolder(dataId, entryId, field.fieldid, site.id).then((folderPath) =>
CoreFileUploader.getStoredFiles(folderPath)).then((files) =>
CoreFileUploader.clearTmpFiles(files)).catch(() => { // Files not found, ignore.
});
promises.push(promise);
});
await Promise.all(promises);
}
/**
* Get all the stored entry data from all the databases.
*
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with entries.
*/
async getAllEntries(siteId?: string): Promise<AddonModDataOfflineAction[]> {
const site = await CoreSites.getSite(siteId);
const entries = await site.getDb().getAllRecords<AddonModDataEntryDBRecord>(DATA_ENTRY_TABLE);
return entries.map(this.parseRecord.bind(this));
}
/**
* Get all the stored entry actions from a certain database, sorted by modification time.
*
* @param dataId Database ID.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with entries.
*/
async getDatabaseEntries(dataId: number, siteId?: string): Promise<AddonModDataOfflineAction[]> {
const site = await CoreSites.getSite(siteId);
const entries = await site.getDb().getRecords<AddonModDataEntryDBRecord>(
DATA_ENTRY_TABLE,
{ dataid: dataId },
'timemodified',
);
return entries.map(this.parseRecord.bind(this));
}
/**
* Get an stored entry data.
*
* @param dataId Database ID.
* @param entryId Database entry Id.
* @param action Action to be done
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with entry.
*/
async getEntry(
dataId: number,
entryId: number,
action: AddonModDataAction,
siteId?: string,
): Promise<AddonModDataOfflineAction> {
const site = await CoreSites.getSite(siteId);
const entry = await site.getDb().getRecord<AddonModDataEntryDBRecord>(DATA_ENTRY_TABLE, {
dataid: dataId, entryid: entryId,
action,
});
return this.parseRecord(entry);
}
/**
* Get an all stored entry actions data.
*
* @param dataId Database ID.
* @param entryId Database entry Id.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with entry actions.
*/
async getEntryActions(dataId: number, entryId: number, siteId?: string): Promise<AddonModDataOfflineAction[]> {
const site = await CoreSites.getSite(siteId);
const entries = await site.getDb().getRecords<AddonModDataEntryDBRecord>(
DATA_ENTRY_TABLE,
{ dataid: dataId, entryid: entryId },
);
return entries.map(this.parseRecord.bind(this));
}
/**
* Check if there are offline entries to send.
*
* @param dataId Database ID.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with boolean: true if has offline answers, false otherwise.
*/
async hasOfflineData(dataId: number, siteId?: string): Promise<boolean> {
const site = await CoreSites.getSite(siteId);
return CoreUtils.promiseWorks(
site.getDb().recordExists(DATA_ENTRY_TABLE, { dataid: dataId }),
);
}
/**
* Get the path to the folder where to store files for offline files in a database.
*
* @param dataId Database ID.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with the path.
*/
protected async getDatabaseFolder(dataId: number, siteId?: string): Promise<string> {
const site = await CoreSites.getSite(siteId);
const siteFolderPath = CoreFile.getSiteFolder(site.getId());
const folderPath = 'offlinedatabase/' + dataId;
return CoreTextUtils.concatenatePaths(siteFolderPath, folderPath);
}
/**
* Get the path to the folder where to store files for a new offline entry.
*
* @param dataId Database ID.
* @param entryId The ID of the entry.
* @param fieldId Field ID.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with the path.
*/
async getEntryFieldFolder(dataId: number, entryId: number, fieldId: number, siteId?: string): Promise<string> {
const folderPath = await this.getDatabaseFolder(dataId, siteId);
return CoreTextUtils.concatenatePaths(folderPath, entryId + '_' + fieldId);
}
/**
* Parse "fields" of an offline record.
*
* @param record Record object
* @return Record object with columns parsed.
*/
protected parseRecord(record: AddonModDataEntryDBRecord): AddonModDataOfflineAction {
return Object.assign(record, {
fields: CoreTextUtils.parseJSON<AddonModDataEntryWSField[]>(record.fields),
});
}
/**
* Save an entry data to be sent later.
*
* @param dataId Database ID.
* @param entryId Database entry Id. If action is add entryId should be 0 and -timemodified will be used.
* @param action Action to be done to the entry: [add, edit, delete, approve, disapprove]
* @param courseId Course ID of the database.
* @param groupId Group ID. Only provided when adding.
* @param fields Array of field data of the entry if needed.
* @param timemodified The time the entry was modified. If not defined, current time.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved if stored, rejected if failure.
*/
async saveEntry(
dataId: number,
entryId: number,
action: AddonModDataAction,
courseId: number,
groupId = 0,
fields?: AddonModDataEntryWSField[],
timemodified?: number,
siteId?: string,
): Promise<AddonModDataEntryDBRecord> {
const site = await CoreSites.getSite(siteId);
timemodified = timemodified || new Date().getTime();
entryId = typeof entryId == 'undefined' || entryId === null ? -timemodified : entryId;
const entry: AddonModDataEntryDBRecord = {
dataid: dataId,
courseid: courseId,
groupid: groupId,
action,
entryid: entryId,
fields: JSON.stringify(fields || []),
timemodified,
};
await site.getDb().insertRecord(DATA_ENTRY_TABLE, entry);
return entry;
}
}
export const AddonModDataOffline = makeSingleton(AddonModDataOfflineProvider);
/**
* Entry action stored offline.
*/
export type AddonModDataOfflineAction = Omit<AddonModDataEntryDBRecord, 'fields'> & {
fields: AddonModDataEntryWSField[];
};

View File

@ -0,0 +1,499 @@
// (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 { ContextLevel } from '@/core/constants';
import { Injectable } from '@angular/core';
import { CoreSyncBlockedError } from '@classes/base-sync';
import { CoreNetworkError } from '@classes/errors/network-error';
import { CoreCourseActivitySyncBaseProvider } from '@features/course/classes/activity-sync';
import { CoreCourseCommonModWSOptions } from '@features/course/services/course';
import { CoreCourseLogHelper } from '@features/course/services/log-helper';
import { CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader';
import { CoreRatingSync } from '@features/rating/services/rating-sync';
import { FileEntry } from '@ionic-native/file';
import { CoreApp } from '@services/app';
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
import { CoreSync } from '@services/sync';
import { CoreTextUtils } from '@services/utils/text';
import { CoreUtils } from '@services/utils/utils';
import { CoreWSExternalFile } from '@services/ws';
import { Translate, makeSingleton } from '@singletons';
import { CoreEvents } from '@singletons/events';
import { AddonModDataProvider, AddonModData, AddonModDataData, AddonModDataAction } from './data';
import { AddonModDataHelper } from './data-helper';
import { AddonModDataOffline, AddonModDataOfflineAction } from './data-offline';
/**
* Service to sync databases.
*/
@Injectable({ providedIn: 'root' })
export class AddonModDataSyncProvider extends CoreCourseActivitySyncBaseProvider<AddonModDataSyncResult> {
static readonly AUTO_SYNCED = 'addon_mod_data_autom_synced';
protected componentTranslatableString = 'data';
constructor() {
super('AddonModDataSyncProvider');
}
/**
* Check if a database has data to synchronize.
*
* @param dataId Database ID.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with boolean: true if has data to sync, false otherwise.
*/
hasDataToSync(dataId: number, siteId?: string): Promise<boolean> {
return AddonModDataOffline.hasOfflineData(dataId, siteId);
}
/**
* Try to synchronize all the databases in a certain site or in all sites.
*
* @param siteId Site ID to sync. If not defined, sync all sites.
* @param force Wether to force sync not depending on last execution.
* @return Promise resolved if sync is successful, rejected if sync fails.
*/
syncAllDatabases(siteId?: string, force?: boolean): Promise<void> {
return this.syncOnSites('all databases', this.syncAllDatabasesFunc.bind(this, !!force), siteId);
}
/**
* Sync all pending databases on a site.
*
* @param force Wether to force sync not depending on last execution.
* @param siteId Site ID to sync. If not defined, sync all sites.
* @param Promise resolved if sync is successful, rejected if sync fails.
*/
protected async syncAllDatabasesFunc(force: boolean, siteId: string): Promise<void> {
const promises: Promise<unknown>[] = [];
// Get all data answers pending to be sent in the site.
promises.push(AddonModDataOffline.getAllEntries(siteId).then(async (offlineActions) => {
// Get data id.
let dataIds: number[] = offlineActions.map((action) => action.dataid);
// Get unique values.
dataIds = dataIds.filter((id, pos) => dataIds.indexOf(id) == pos);
const entriesPromises = dataIds.map(async (dataId) => {
const result = force
? await this.syncDatabase(dataId, siteId)
: await this.syncDatabaseIfNeeded(dataId, siteId);
if (result && result.updated) {
// Sync done. Send event.
CoreEvents.trigger(AddonModDataSyncProvider.AUTO_SYNCED, {
dataId: dataId,
warnings: result.warnings,
}, siteId);
}
});
await Promise.all(entriesPromises);
return;
}));
promises.push(this.syncRatings(undefined, force, siteId));
await Promise.all(promises);
}
/**
* Sync a database only if a certain time has passed since the last time.
*
* @param dataId Database ID.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when the data is synced or if it doesn't need to be synced.
*/
async syncDatabaseIfNeeded(dataId: number, siteId?: string): Promise<AddonModDataSyncResult | undefined> {
const needed = await this.isSyncNeeded(dataId, siteId);
if (needed) {
return this.syncDatabase(dataId, siteId);
}
}
/**
* Synchronize a data.
*
* @param dataId Data ID.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved if sync is successful, rejected otherwise.
*/
syncDatabase(dataId: number, siteId?: string): Promise<AddonModDataSyncResult> {
siteId = siteId || CoreSites.getCurrentSiteId();
if (this.isSyncing(dataId, siteId)) {
// There's already a sync ongoing for this database, return the promise.
return this.getOngoingSync(dataId, siteId)!;
}
// Verify that database isn't blocked.
if (CoreSync.isBlocked(AddonModDataProvider.COMPONENT, dataId, siteId)) {
this.logger.debug(`Cannot sync database '${dataId}' because it is blocked.`);
throw new CoreSyncBlockedError(Translate.instant('core.errorsyncblocked', { $a: this.componentTranslate }));
}
this.logger.debug(`Try to sync data '${dataId}' in site ${siteId}'`);
const syncPromise = this.performSyncDatabase(dataId, siteId);
return this.addOngoingSync(dataId, syncPromise, siteId);
}
/**
* Perform the database syncronization.
*
* @param dataId Data ID.
* @param siteId Site ID.
* @return Promise resolved if sync is successful, rejected otherwise.
*/
protected async performSyncDatabase(dataId: number, siteId: string): Promise<AddonModDataSyncResult> {
// Sync offline logs.
await CoreUtils.ignoreErrors(
CoreCourseLogHelper.syncActivity(AddonModDataProvider.COMPONENT, dataId, siteId),
);
const result: AddonModDataSyncResult = {
warnings: [],
updated: false,
};
// Get answers to be sent.
const offlineActions: AddonModDataOfflineAction[] =
await CoreUtils.ignoreErrors(AddonModDataOffline.getDatabaseEntries(dataId, siteId), []);
if (!offlineActions.length) {
// Nothing to sync.
await CoreUtils.ignoreErrors(this.setSyncTime(dataId, siteId));
return result;
}
if (!CoreApp.isOnline()) {
// Cannot sync in offline.
throw new CoreNetworkError();
}
const courseId = offlineActions[0].courseid;
// Send the answers.
const database = await AddonModData.getDatabaseById(courseId, dataId, { siteId });
const offlineEntries: Record<number, AddonModDataOfflineAction[]> = {};
offlineActions.forEach((entry) => {
if (typeof offlineEntries[entry.entryid] == 'undefined') {
offlineEntries[entry.entryid] = [];
}
offlineEntries[entry.entryid].push(entry);
});
const promises = CoreUtils.objectToArray(offlineEntries).map((entryActions) =>
this.syncEntry(database, entryActions, result, siteId));
await Promise.all(promises);
if (result.updated) {
// Data has been sent to server. Now invalidate the WS calls.
await CoreUtils.ignoreErrors(AddonModData.invalidateContent(database.coursemodule, courseId, siteId));
}
// Sync finished, set sync time.
await CoreUtils.ignoreErrors(this.setSyncTime(dataId, siteId));
return result;
}
/**
* Synchronize an entry.
*
* @param database Database.
* @param entryActions Entry actions.
* @param result Object with the result of the sync.
* @param siteId Site ID.
* @return Promise resolved if success, rejected otherwise.
*/
protected async syncEntry(
database: AddonModDataData,
entryActions: AddonModDataOfflineAction[],
result: AddonModDataSyncResult,
siteId: string,
): Promise<void> {
const synEntryResult = await this.performSyncEntry(database, entryActions, result, siteId);
if (synEntryResult.discardError) {
// Submission was discarded, add a warning.
const message = Translate.instant('core.warningofflinedatadeleted', {
component: this.componentTranslate,
name: database.name,
error: synEntryResult.discardError,
});
if (result.warnings.indexOf(message) == -1) {
result.warnings.push(message);
}
}
// Sync done. Send event.
CoreEvents.trigger(AddonModDataSyncProvider.AUTO_SYNCED, {
dataId: database.id,
entryId: synEntryResult.entryId,
offlineEntryId: synEntryResult.offlineId,
warnings: result.warnings,
deleted: synEntryResult.deleted,
}, siteId);
}
/**
* Perform the synchronization of an entry.
*
* @param database Database.
* @param entryActions Entry actions.
* @param result Object with the result of the sync.
* @param siteId Site ID.
* @return Promise resolved if success, rejected otherwise.
*/
protected async performSyncEntry(
database: AddonModDataData,
entryActions: AddonModDataOfflineAction[],
result: AddonModDataSyncResult,
siteId: string,
): Promise<AddonModDataSyncEntryResult> {
let entryId = entryActions[0].entryid;
const entryResult: AddonModDataSyncEntryResult = {
deleted: false,
entryId: entryId,
};
const editAction = entryActions.find((action) =>
action.action == AddonModDataAction.ADD || action.action == AddonModDataAction.EDIT);
const approveAction = entryActions.find((action) =>
action.action == AddonModDataAction.APPROVE || action.action == AddonModDataAction.DISAPPROVE);
const deleteAction = entryActions.find((action) => action.action == AddonModDataAction.DELETE);
const options: CoreCourseCommonModWSOptions = {
cmId: database.coursemodule,
readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
siteId,
};
let timemodified = 0;
if (entryId > 0) {
try {
const entry = await AddonModData.getEntry(database.id, entryId, options);
timemodified = entry.entry.timemodified;
} catch (error) {
if (error && CoreUtils.isWebServiceError(error)) {
// The WebService has thrown an error, this means the entry has been deleted.
timemodified = -1;
} else {
throw error;
}
}
} else if (editAction) {
// New entry.
entryResult.offlineId = entryId;
timemodified = 0;
} else {
// New entry but the add action is missing, discard.
timemodified = -1;
}
if (timemodified < 0 || timemodified >= entryActions[0].timemodified) {
// The entry was not found in Moodle or the entry has been modified, discard the action.
result.updated = true;
entryResult.discardError = Translate.instant('addon.mod_data.warningsubmissionmodified');
await AddonModDataOffline.deleteAllEntryActions(database.id, entryId, siteId);
return entryResult;
}
if (deleteAction) {
try {
await AddonModData.deleteEntryOnline(entryId, siteId);
entryResult.deleted = true;
} catch (error) {
if (error && CoreUtils.isWebServiceError(error)) {
// The WebService has thrown an error, this means it cannot be performed. Discard.
entryResult.discardError = CoreTextUtils.getErrorMessageFromError(error);
} else {
// Couldn't connect to server, reject.
throw error;
}
}
// Delete the offline data.
result.updated = true;
await AddonModDataOffline.deleteAllEntryActions(deleteAction.dataid, deleteAction.entryid, siteId);
return entryResult;
}
if (editAction) {
try {
await Promise.all(editAction.fields.map(async (field) => {
// Upload Files if asked.
const value = CoreTextUtils.parseJSON<CoreFileUploaderStoreFilesResult>(field.value || '');
if (value.online || value.offline) {
let files: (CoreWSExternalFile | FileEntry)[] = value.online || [];
const offlineFiles = value.offline
? await AddonModDataHelper.getStoredFiles(editAction.dataid, entryId, field.fieldid)
: [];
files = files.concat(offlineFiles);
const filesResult = await AddonModDataHelper.uploadOrStoreFiles(
editAction.dataid,
0,
entryId,
field.fieldid,
files,
false,
siteId,
);
field.value = JSON.stringify(filesResult);
}
}));
if (editAction.action == AddonModDataAction.ADD) {
const result = await AddonModData.addEntryOnline(
editAction.dataid,
editAction.fields,
editAction.groupid,
siteId,
);
entryId = result.newentryid;
entryResult.entryId = entryId;
} else {
await AddonModData.editEntryOnline(entryId, editAction.fields, siteId);
}
} catch (error) {
if (error && CoreUtils.isWebServiceError(error)) {
// The WebService has thrown an error, this means it cannot be performed. Discard.
entryResult.discardError = CoreTextUtils.getErrorMessageFromError(error);
} else {
// Couldn't connect to server, reject.
throw error;
}
}
// Delete the offline data.
result.updated = true;
await AddonModDataOffline.deleteEntry(editAction.dataid, editAction.entryid, editAction.action, siteId);
}
if (approveAction) {
try {
await AddonModData.approveEntryOnline(entryId, approveAction.action == AddonModDataAction.APPROVE, siteId);
} catch (error) {
if (error && CoreUtils.isWebServiceError(error)) {
// The WebService has thrown an error, this means it cannot be performed. Discard.
entryResult.discardError = CoreTextUtils.getErrorMessageFromError(error);
} else {
// Couldn't connect to server, reject.
throw error;
}
}
// Delete the offline data.
result.updated = true;
await AddonModDataOffline.deleteEntry(approveAction.dataid, approveAction.entryid, approveAction.action, siteId);
}
return entryResult;
}
/**
* Synchronize offline ratings.
*
* @param cmId Course module to be synced. If not defined, sync all databases.
* @param force Wether to force sync not depending on last execution.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved if sync is successful, rejected otherwise.
*/
async syncRatings(cmId?: number, force?: boolean, siteId?: string): Promise<AddonModDataSyncResult> {
siteId = siteId || CoreSites.getCurrentSiteId();
const results = await CoreRatingSync.syncRatings('mod_data', 'entry', ContextLevel.MODULE, cmId, 0, force, siteId);
let updated = false;
const warnings = [];
const promises = results.map((result) =>
AddonModData.getDatabase(result.itemSet.courseId, result.itemSet.instanceId, { siteId })
.then((database) => {
const subPromises: Promise<void>[] = [];
if (result.updated.length) {
updated = true;
// Invalidate entry of updated ratings.
result.updated.forEach((itemId) => {
subPromises.push(AddonModData.invalidateEntryData(database.id, itemId, siteId));
});
}
if (result.warnings.length) {
result.warnings.forEach((warning) => {
this.addOfflineDataDeletedWarning(warnings, database.name, warning);
});
}
return CoreUtils.allPromises(subPromises);
}));
await Promise.all(promises);
return ({ updated, warnings });
}
}
export const AddonModDataSync = makeSingleton(AddonModDataSyncProvider);
/**
* Data returned by a database sync.
*/
export type AddonModDataSyncEntryResult = {
discardError?: string;
offlineId?: number;
entryId: number;
deleted: boolean;
};
/**
* 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 AddonModDataAutoSyncData = {
dataId: number;
warnings: string[];
entryId?: number;
offlineEntryId?: number;
deleted?: boolean;
};

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,83 @@
// (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 { SQLiteDB } from '@classes/sqlitedb';
import { CoreSiteSchema } from '@services/sites';
import { AddonModDataAction } from '../data';
/**
* Database variables for AddonModDataOfflineProvider.
*/
export const DATA_ENTRY_TABLE = 'addon_mod_data_entry_1';
export const ADDON_MOD_DATA_OFFLINE_SITE_SCHEMA: CoreSiteSchema = {
name: 'AddonModDataOfflineProvider',
version: 1,
tables: [
{
name: DATA_ENTRY_TABLE,
columns: [
{
name: 'dataid',
type: 'INTEGER',
},
{
name: 'courseid',
type: 'INTEGER',
},
{
name: 'groupid',
type: 'INTEGER',
},
{
name: 'action',
type: 'TEXT',
},
{
name: 'entryid',
type: 'INTEGER',
},
{
name: 'fields',
type: 'TEXT',
},
{
name: 'timemodified',
type: 'INTEGER',
},
],
primaryKeys: ['dataid', 'entryid', 'action'],
},
],
async migrate(db: SQLiteDB, oldVersion: number): Promise<void> {
if (oldVersion > 0) {
return;
}
// Move the records from the old table.
await db.migrateTable('addon_mod_data_entry', DATA_ENTRY_TABLE);
},
};
/**
* Data about data entries to sync.
*/
export type AddonModDataEntryDBRecord = {
dataid: number; // Primary key.
entryid: number; // Primary key. Negative for offline entries.
action: AddonModDataAction; // Primary key.
courseid: number;
groupid: number;
fields: string;
timemodified: number;
};

View File

@ -0,0 +1,63 @@
// (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 { Injectable } from '@angular/core';
import { Params } from '@angular/router';
import { CoreContentLinksHandlerBase } from '@features/contentlinks/classes/base-handler';
import { CoreContentLinksAction } from '@features/contentlinks/services/contentlinks-delegate';
import { makeSingleton } from '@singletons';
import { AddonModData } from '../data';
import { AddonModDataHelper } from '../data-helper';
/**
* Content links handler for database approve/disapprove entry.
* Match mod/data/view.php?d=6&approve=5 with a valid data id and entryid.
*/
@Injectable({ providedIn: 'root' })
export class AddonModDataApproveLinkHandlerService extends CoreContentLinksHandlerBase {
name = 'AddonModDataApproveLinkHandler';
featureName = 'CoreCourseModuleDelegate_AddonModData';
pattern = /\/mod\/data\/view\.php.*([?&](d|approve|disapprove)=\d+)/;
priority = 50; // Higher priority than the default link handler for view.php.
/**
* @inheritdoc
*/
getActions(siteIds: string[], url: string, params: Params, courseId?: number): CoreContentLinksAction[] {
return [{
action: (siteId): void => {
const dataId = parseInt(params.d, 10);
const entryId = parseInt(params.approve, 10) || parseInt(params.disapprove, 10);
const approve = parseInt(params.approve, 10) ? true : false;
AddonModDataHelper.approveOrDisapproveEntry(dataId, entryId, approve, courseId, siteId);
},
}];
}
/**
* @inheritdoc
*/
async isEnabled(siteId: string, url: string, params: Params): Promise<boolean> {
if (typeof params.d == 'undefined' || (typeof params.approve == 'undefined' && typeof params.disapprove == 'undefined')) {
// Required fields not defined. Cannot treat the URL.
return false;
}
return AddonModData.isPluginEnabled(siteId);
}
}
export const AddonModDataApproveLinkHandler = makeSingleton(AddonModDataApproveLinkHandlerService);

View File

@ -0,0 +1,78 @@
// (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 { Injectable } from '@angular/core';
import { FileEntry } from '@ionic-native/file';
import { CoreWSExternalFile } from '@services/ws';
import { AddonModDataEntryField, AddonModDataSearchEntriesAdvancedFieldFormatted, AddonModDataSubfieldData } from '../data';
import { AddonModDataFieldHandler } from '../data-fields-delegate';
/**
* Default handler used when a field plugin doesn't have a specific implementation.
*/
@Injectable({ providedIn: 'root' })
export class AddonModDataDefaultFieldHandler implements AddonModDataFieldHandler {
name = 'AddonModDataDefaultFieldHandler';
type = 'default';
/**
* @inheritdoc
*/
getFieldSearchData(): AddonModDataSearchEntriesAdvancedFieldFormatted[] {
return [];
}
/**
* @inheritdoc
*/
getFieldEditData(): AddonModDataSubfieldData[] {
return [];
}
/**
* @inheritdoc
*/
hasFieldDataChanged(): boolean {
return false;
}
/**
* @inheritdoc
*/
getFieldEditFiles(): (CoreWSExternalFile | FileEntry)[] {
return [];
}
/**
* @inheritdoc
*/
getFieldsNotifications(): undefined {
return;
}
/**
* @inheritdoc
*/
overrideData(originalContent: AddonModDataEntryField): AddonModDataEntryField {
return originalContent;
}
/**
* @inheritdoc
*/
async isEnabled(): Promise<boolean> {
return true;
}
}

View File

@ -0,0 +1,61 @@
// (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 { Injectable } from '@angular/core';
import { Params } from '@angular/router';
import { CoreContentLinksHandlerBase } from '@features/contentlinks/classes/base-handler';
import { CoreContentLinksAction } from '@features/contentlinks/services/contentlinks-delegate';
import { makeSingleton } from '@singletons';
import { AddonModData } from '../data';
import { AddonModDataHelper } from '../data-helper';
/**
* Content links handler for database delete entry.
* Match mod/data/view.php?d=6&delete=5 with a valid data id and entryid.
*/
@Injectable({ providedIn: 'root' })
export class AddonModDataDeleteLinkHandlerService extends CoreContentLinksHandlerBase {
name = 'AddonModDataDeleteLinkHandler';
featureName = 'CoreCourseModuleDelegate_AddonModData';
pattern = /\/mod\/data\/view\.php.*([?&](d|delete)=\d+)/;
/**
* @inheritdoc
*/
getActions(siteIds: string[], url: string, params: Params, courseId?: number): CoreContentLinksAction[] {
return [{
action: (siteId): void => {
const dataId = parseInt(params.d, 10);
const entryId = parseInt(params.delete, 10);
AddonModDataHelper.showDeleteEntryModal(dataId, entryId, courseId, siteId);
},
}];
}
/**
* @inheritdoc
*/
async isEnabled(siteId: string, url: string, params: Params): Promise<boolean> {
if (typeof params.d == 'undefined' || typeof params.delete == 'undefined') {
// Required fields not defined. Cannot treat the URL.
return false;
}
return AddonModData.isPluginEnabled(siteId);
}
}
export const AddonModDataDeleteLinkHandler = makeSingleton(AddonModDataDeleteLinkHandlerService);

View File

@ -0,0 +1,79 @@
// (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 { Injectable } from '@angular/core';
import { Params } from '@angular/router';
import { CoreContentLinksHandlerBase } from '@features/contentlinks/classes/base-handler';
import { CoreContentLinksAction } from '@features/contentlinks/services/contentlinks-delegate';
import { CoreCourse } from '@features/course/services/course';
import { CoreNavigator } from '@services/navigator';
import { CoreDomUtils } from '@services/utils/dom';
import { makeSingleton } from '@singletons';
import { AddonModData } from '../data';
import { AddonModDataModuleHandlerService } from './module';
/**
* Content links handler for database add or edit entry.
* Match mod/data/edit.php?d=6&rid=6 with a valid data and optional record id.
*/
@Injectable({ providedIn: 'root' })
export class AddonModDataEditLinkHandlerService extends CoreContentLinksHandlerBase {
name = 'AddonModDataEditLinkHandler';
featureName = 'CoreCourseModuleDelegate_AddonModData';
pattern = /\/mod\/data\/edit\.php.*([?&](d|rid)=\d+)/;
/**
* @inheritdoc
*/
getActions(siteIds: string[], url: string, params: Params): CoreContentLinksAction[] {
return [{
action: async (siteId): Promise<void> => {
const modal = await CoreDomUtils.showModalLoading();
const dataId = parseInt(params.d, 10);
const rId = params.rid || '';
try {
const module = await CoreCourse.getModuleBasicInfoByInstance(dataId, 'data', siteId);
const pageParams: Params = {
module,
courseId: module.course,
};
CoreNavigator.navigateToSitePath(
`${AddonModDataModuleHandlerService.PAGE_NAME}/${module.course}/${module.id}/edit/${rId}`,
{ siteId, params: pageParams },
);
} finally {
// Just in case. In fact we need to dismiss the modal before showing a toast or error message.
modal.dismiss();
}
},
}];
}
/**
* @inheritdoc
*/
async isEnabled(siteId: string, url: string, params: Params): Promise<boolean> {
if (typeof params.d == 'undefined') {
// Id not defined. Cannot treat the URL.
return false;
}
return AddonModData.isPluginEnabled(siteId);
}
}
export const AddonModDataEditLinkHandler = makeSingleton(AddonModDataEditLinkHandlerService);

View File

@ -0,0 +1,40 @@
// (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 { Injectable } from '@angular/core';
import { CoreContentLinksModuleIndexHandler } from '@features/contentlinks/classes/module-index-handler';
import { makeSingleton } from '@singletons';
import { AddonModData } from '../data';
/**
* Handler to treat links to data.
*/
@Injectable({ providedIn: 'root' })
export class AddonModDataIndexLinkHandlerService extends CoreContentLinksModuleIndexHandler {
name = 'AddonModDataLinkHandler';
constructor() {
super('AddonModData', 'data', 'd');
}
/**
* @inheritdoc
*/
isEnabled(siteId: string): Promise<boolean> {
return AddonModData.isPluginEnabled(siteId);
}
}
export const AddonModDataIndexLinkHandler = makeSingleton(AddonModDataIndexLinkHandlerService);

View File

@ -0,0 +1,40 @@
// (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 { Injectable } from '@angular/core';
import { CoreContentLinksModuleListHandler } from '@features/contentlinks/classes/module-list-handler';
import { makeSingleton } from '@singletons';
import { AddonModData } from '../data';
/**
* Handler to treat links to data list page.
*/
@Injectable({ providedIn: 'root' })
export class AddonModDataListLinkHandlerService extends CoreContentLinksModuleListHandler {
name = 'AddonModDataListLinkHandler';
constructor() {
super('AddonModData', 'data');
}
/**
* @inheritdoc
*/
isEnabled(siteId?: string): Promise<boolean> {
return AddonModData.isPluginEnabled(siteId);
}
}
export const AddonModDataListLinkHandler = makeSingleton(AddonModDataListLinkHandlerService);

View File

@ -0,0 +1,85 @@
// (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 { CoreConstants } from '@/core/constants';
import { Injectable, Type } from '@angular/core';
import { CoreCourse, CoreCourseAnyModuleData } from '@features/course/services/course';
import { CoreCourseModule } from '@features/course/services/course-helper';
import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate';
import { CoreNavigationOptions, CoreNavigator } from '@services/navigator';
import { makeSingleton } from '@singletons';
import { AddonModDataIndexComponent } from '../../components/index';
import { AddonModData } from '../data';
/**
* Handler to support data modules.
*/
@Injectable({ providedIn: 'root' })
export class AddonModDataModuleHandlerService implements CoreCourseModuleHandler {
static readonly PAGE_NAME = 'mod_data';
name = 'AddonModData';
modName = 'data';
supportedFeatures = {
[CoreConstants.FEATURE_GROUPS]: true,
[CoreConstants.FEATURE_GROUPINGS]: true,
[CoreConstants.FEATURE_MOD_INTRO]: true,
[CoreConstants.FEATURE_COMPLETION_TRACKS_VIEWS]: true,
[CoreConstants.FEATURE_COMPLETION_HAS_RULES]: true,
[CoreConstants.FEATURE_GRADE_HAS_GRADE]: true,
[CoreConstants.FEATURE_GRADE_OUTCOMES]: true,
[CoreConstants.FEATURE_BACKUP_MOODLE2]: true,
[CoreConstants.FEATURE_SHOW_DESCRIPTION]: true,
[CoreConstants.FEATURE_RATE]: true,
[CoreConstants.FEATURE_COMMENT]: true,
};
/**
* @inheritdoc
*/
isEnabled(): Promise<boolean> {
return AddonModData.isPluginEnabled();
}
/**
* @inheritdoc
*/
getData(module: CoreCourseAnyModuleData): CoreCourseModuleHandlerData {
return {
icon: CoreCourse.getModuleIconSrc(this.modName, 'modicon' in module ? module.modicon : undefined),
title: module.name,
class: 'addon-mod_data-handler',
showDownloadButton: true,
action(event: Event, module: CoreCourseModule, courseId: number, options?: CoreNavigationOptions): void {
options = options || {};
options.params = options.params || {};
Object.assign(options.params, { module });
const routeParams = '/' + courseId + '/' + module.id;
CoreNavigator.navigateToSitePath(AddonModDataModuleHandlerService.PAGE_NAME + routeParams, options);
},
};
}
/**
* @inheritdoc
*/
async getMainComponent(): Promise<Type<unknown>> {
return AddonModDataIndexComponent;
}
}
export const AddonModDataModuleHandler = makeSingleton(AddonModDataModuleHandlerService);

View File

@ -0,0 +1,300 @@
// (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 { Injectable } from '@angular/core';
import { CoreComments } from '@features/comments/services/comments';
import { CoreCourseActivityPrefetchHandlerBase } from '@features/course/classes/activity-prefetch-handler';
import { CoreCourseCommonModWSOptions, CoreCourse, CoreCourseAnyModuleData } from '@features/course/services/course';
import { CoreFilepool } from '@services/filepool';
import { CoreGroup, CoreGroups } from '@services/groups';
import { CoreSitesCommonWSOptions, CoreSites, CoreSitesReadingStrategy } from '@services/sites';
import { CoreTimeUtils } from '@services/utils/time';
import { CoreUtils } from '@services/utils/utils';
import { CoreWSExternalFile } from '@services/ws';
import { makeSingleton } from '@singletons';
import { AddonModDataProvider, AddonModDataEntry, AddonModData, AddonModDataData } from '../data';
import { AddonModDataSync, AddonModDataSyncResult } from '../data-sync';
/**
* Handler to prefetch databases.
*/
@Injectable({ providedIn: 'root' })
export class AddonModDataPrefetchHandlerService extends CoreCourseActivityPrefetchHandlerBase {
name = 'AddonModData';
modName = 'data';
component = AddonModDataProvider.COMPONENT;
updatesNames = /^configuration$|^.*files$|^entries$|^gradeitems$|^outcomes$|^comments$|^ratings/;
/**
* Retrieves all the entries for all the groups and then returns only unique entries.
*
* @param dataId Database Id.
* @param groups Array of groups in the activity.
* @param options Other options.
* @return All unique entries.
*/
protected async getAllUniqueEntries(
dataId: number,
groups: CoreGroup[],
options: CoreSitesCommonWSOptions = {},
): Promise<AddonModDataEntry[]> {
const promises = groups.map((group) => AddonModData.fetchAllEntries(dataId, {
groupId: group.id,
...options, // Include all options.
}));
const responses = await Promise.all(promises);
const uniqueEntries: Record<number, AddonModDataEntry> = {};
responses.forEach((groupEntries) => {
groupEntries.forEach((entry) => {
uniqueEntries[entry.id] = entry;
});
});
return CoreUtils.objectToArray(uniqueEntries);
}
/**
* Helper function to get all database info just once.
*
* @param module Module to get the files.
* @param courseId Course ID the module belongs to.
* @param omitFail True to always return even if fails. Default false.
* @param options Other options.
* @return Promise resolved with the info fetched.
*/
protected async getDatabaseInfoHelper(
module: CoreCourseAnyModuleData,
courseId: number,
omitFail: boolean,
options: CoreCourseCommonModWSOptions = {},
): Promise<{ database: AddonModDataData; groups: CoreGroup[]; entries: AddonModDataEntry[]; files: CoreWSExternalFile[]}> {
let groups: CoreGroup[] = [];
let entries: AddonModDataEntry[] = [];
let files: CoreWSExternalFile[] = [];
options.cmId = options.cmId || module.id;
options.siteId = options.siteId || CoreSites.getCurrentSiteId();
const database = await AddonModData.getDatabase(courseId, module.id, options);
try {
files = this.getIntroFilesFromInstance(module, database);
const groupInfo = await CoreGroups.getActivityGroupInfo(module.id, false, undefined, options.siteId);
if (!groupInfo.groups || groupInfo.groups.length == 0) {
groupInfo.groups = [{ id: 0, name: '' }];
}
groups = groupInfo.groups || [];
entries = await this.getAllUniqueEntries(database.id, groups, options);
files = files.concat(this.getEntriesFiles(entries));
return {
database,
groups,
entries,
files,
};
} catch (error) {
if (omitFail) {
// Any error, return the info we have.
return {
database,
groups,
entries,
files,
};
}
throw error;
}
}
/**
* Returns the file contained in the entries.
*
* @param entries List of entries to get files from.
* @return List of files.
*/
protected getEntriesFiles(entries: AddonModDataEntry[]): CoreWSExternalFile[] {
let files: CoreWSExternalFile[] = [];
entries.forEach((entry) => {
CoreUtils.objectToArray(entry.contents).forEach((content) => {
files = files.concat(<CoreWSExternalFile[]>content.files);
});
});
return files;
}
/**
* @inheritdoc
*/
async getFiles(module: CoreCourseAnyModuleData, courseId: number): Promise<CoreWSExternalFile[]> {
return this.getDatabaseInfoHelper(module, courseId, true).then((info) => info.files);
}
/**
* @inheritdoc
*/
async getIntroFiles(module: CoreCourseAnyModuleData, courseId: number): Promise<CoreWSExternalFile[]> {
const data = await CoreUtils.ignoreErrors(AddonModData.getDatabase(courseId, module.id));
return this.getIntroFilesFromInstance(module, data);
}
/**
* @inheritdoc
*/
async invalidateContent(moduleId: number, courseId: number): Promise<void> {
await AddonModData.invalidateContent(moduleId, courseId);
}
/**
* @inheritdoc
*/
async invalidateModule(module: CoreCourseAnyModuleData, courseId: number): Promise<void> {
const promises: Promise<void>[] = [];
promises.push(AddonModData.invalidateDatabaseData(courseId));
promises.push(AddonModData.invalidateDatabaseAccessInformationData(module.instance!));
await Promise.all(promises);
}
/**
* @inheritdoc
*/
async isDownloadable(module: CoreCourseAnyModuleData, courseId: number): Promise<boolean> {
const database = await AddonModData.getDatabase(courseId, module.id, {
readingStrategy: CoreSitesReadingStrategy.PreferCache,
});
const accessData = await AddonModData.getDatabaseAccessInformation(database.id, { cmId: module.id });
// Check if database is restricted by time.
if (!accessData.timeavailable) {
const time = CoreTimeUtils.timestamp();
// It is restricted, checking times.
if (database.timeavailablefrom && time < database.timeavailablefrom) {
return false;
}
if (database.timeavailableto && time > database.timeavailableto) {
return false;
}
}
return true;
}
/**
* @inheritdoc
*/
async isEnabled(): Promise<boolean> {
return AddonModData.isPluginEnabled();
}
/**
* @inheritdoc
*/
prefetch(module: CoreCourseAnyModuleData, courseId?: number): Promise<void> {
return this.prefetchPackage(module, courseId, this.prefetchDatabase.bind(this, module, courseId));
}
/**
* Prefetch a database.
*
* @param module Module.
* @param courseId Course ID the module belongs to.
* @return Promise resolved when done.
*/
protected async prefetchDatabase(module: CoreCourseAnyModuleData, courseId?: number): Promise<void> {
const siteId = CoreSites.getCurrentSiteId();
courseId = courseId || module.course || CoreSites.getCurrentSiteHomeId();
const options = {
cmId: module.id,
readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
siteId,
};
const info = await this.getDatabaseInfoHelper(module, courseId, false, options);
// Prefetch the database data.
const database = info.database;
const commentsEnabled = !CoreComments.areCommentsDisabledInSite();
const promises: Promise<unknown>[] = [];
promises.push(AddonModData.getFields(database.id, options));
promises.push(CoreFilepool.addFilesToQueue(siteId, info.files, this.component, module.id));
info.groups.forEach((group) => {
promises.push(AddonModData.getDatabaseAccessInformation(database.id, {
groupId: group.id,
...options, // Include all options.
}));
});
info.entries.forEach((entry) => {
promises.push(AddonModData.getEntry(database.id, entry.id, options));
if (commentsEnabled && database.comments) {
promises.push(CoreComments.getComments(
'module',
database.coursemodule,
'mod_data',
entry.id,
'database_entry',
0,
siteId,
));
}
});
// Add Basic Info to manage links.
promises.push(CoreCourse.getModuleBasicInfoByInstance(database.id, 'data', siteId));
await Promise.all(promises);
}
/**
* Sync a module.
*
* @param module Module.
* @param courseId Course ID the module belongs to
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
async sync(module: CoreCourseAnyModuleData, courseId: number, siteId?: string): Promise<AddonModDataSyncResult> {
const promises = [
AddonModDataSync.syncDatabase(module.instance!, siteId),
AddonModDataSync.syncRatings(module.id, true, siteId),
];
const results = await Promise.all(promises);
return results.reduce((a, b) => ({
updated: a.updated || b.updated,
warnings: (a.warnings || []).concat(b.warnings || []),
}), { updated: false , warnings: [] });
}
}
export const AddonModDataPrefetchHandler = makeSingleton(AddonModDataPrefetchHandlerService);

View File

@ -0,0 +1,94 @@
// (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 { Injectable } from '@angular/core';
import { Params } from '@angular/router';
import { CoreContentLinksHandlerBase } from '@features/contentlinks/classes/base-handler';
import { CoreContentLinksAction } from '@features/contentlinks/services/contentlinks-delegate';
import { CoreCourse } from '@features/course/services/course';
import { CoreNavigator } from '@services/navigator';
import { CoreDomUtils } from '@services/utils/dom';
import { makeSingleton } from '@singletons';
import { AddonModData } from '../data';
import { AddonModDataModuleHandlerService } from './module';
/**
* Content links handler for database show entry.
* Match mod/data/view.php?d=6&rid=5 with a valid data id and entryid.
*/
@Injectable({ providedIn: 'root' })
export class AddonModDataShowLinkHandlerService extends CoreContentLinksHandlerBase {
name = 'AddonModDataShowLinkHandler';
featureName = 'CoreCourseModuleDelegate_AddonModData';
pattern = /\/mod\/data\/view\.php.*([?&](d|rid|page|group|mode)=\d+)/;
priority = 50; // Higher priority than the default link handler for view.php.
/**
* @inheritdoc
*/
getActions(siteIds: string[], url: string, params: Params): CoreContentLinksAction[] {
return [{
action: async (siteId): Promise<void> => {
const modal = await CoreDomUtils.showModalLoading();
const dataId = parseInt(params.d, 10);
const rId = params.rid || '';
const group = parseInt(params.group, 10) || false;
const page = parseInt(params.page, 10) || false;
try {
const module = await CoreCourse.getModuleBasicInfoByInstance(dataId, 'data', siteId);
const pageParams: Params = {
module: module,
courseId: module.course,
};
if (group) {
pageParams.group = group;
}
if (params.mode && params.mode == 'single') {
pageParams.offset = page || 0;
}
CoreNavigator.navigateToSitePath(
`${AddonModDataModuleHandlerService.PAGE_NAME}/${module.course}/${module.id}/${rId}`,
{ siteId, params: pageParams },
);
} finally {
// Just in case. In fact we need to dismiss the modal before showing a toast or error message.
modal.dismiss();
}
},
}];
}
/**
* @inheritdoc
*/
async isEnabled(siteId: string, url: string, params: Params): Promise<boolean> {
if (typeof params.d == 'undefined') {
// Id not defined. Cannot treat the URL.
return false;
}
if ((!params.mode || params.mode != 'single') && typeof params.rid == 'undefined') {
return false;
}
return AddonModData.isPluginEnabled(siteId);
}
}
export const AddonModDataShowLinkHandler = makeSingleton(AddonModDataShowLinkHandlerService);

View File

@ -0,0 +1,43 @@
// (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 { Injectable } from '@angular/core';
import { CoreCronHandler } from '@services/cron';
import { makeSingleton } from '@singletons';
import { AddonModDataSync } from '../data-sync';
/**
* Synchronization cron handler.
*/
@Injectable({ providedIn: 'root' })
export class AddonModDataSyncCronHandlerService implements CoreCronHandler {
name = 'AddonModDataSyncCronHandler';
/**
* @inheritdoc
*/
execute(siteId?: string, force?: boolean): Promise<void> {
return AddonModDataSync.syncAllDatabases(siteId, force);
}
/**
* @inheritdoc
*/
getInterval(): number {
return AddonModDataSync.syncInterval;
}
}
export const AddonModDataSyncCronHandler = makeSingleton(AddonModDataSyncCronHandlerService);

View File

@ -0,0 +1,53 @@
// (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 { Injectable, Type } from '@angular/core';
import { CoreTagFeedComponent } from '@features/tag/components/feed/feed';
import { CoreTagAreaHandler } from '@features/tag/services/tag-area-delegate';
import { CoreTagFeedElement, CoreTagHelper } from '@features/tag/services/tag-helper';
import { makeSingleton } from '@singletons';
import { AddonModData } from '../data';
/**
* Handler to support tags.
*/
@Injectable({ providedIn: 'root' })
export class AddonModDataTagAreaHandlerService implements CoreTagAreaHandler {
name = 'AddonModDataTagAreaHandler';
type = 'mod_data/data_records';
/**
* @inheritdoc
*/
async isEnabled(): Promise<boolean> {
return AddonModData.isPluginEnabled();
}
/**
* @inheritdoc
*/
parseContent(content: string): CoreTagFeedElement[] {
return CoreTagHelper.parseFeedContent(content);
}
/**
* @inheritdoc
*/
getComponent(): Type<unknown> {
return CoreTagFeedComponent;
}
}
export const AddonModDataTagAreaHandler = makeSingleton(AddonModDataTagAreaHandlerService);

View File

@ -20,6 +20,7 @@ import { CoreDomUtils } from '@services/utils/dom';
import { ModalController, Translate } from '@singletons';
import { AddonModForumData, AddonModForumPost, AddonModForumReply } from '@addons/mod/forum/services/forum';
import { AddonModForumHelper } from '@addons/mod/forum/services/forum-helper';
import { CoreForms } from '@singletons/form';
/**
* Page that displays a form to edit discussion post.
@ -93,9 +94,9 @@ export class AddonModForumEditPostComponent implements OnInit {
}
if (data) {
CoreDomUtils.triggerFormSubmittedEvent(this.formElement, false, CoreSites.getCurrentSiteId());
CoreForms.triggerFormSubmittedEvent(this.formElement, false, CoreSites.getCurrentSiteId());
} else {
CoreDomUtils.triggerFormCancelledEvent(this.formElement, CoreSites.getCurrentSiteId());
CoreForms.triggerFormCancelledEvent(this.formElement, CoreSites.getCurrentSiteId());
}
ModalController.dismiss(data);

View File

@ -53,6 +53,7 @@ import { CoreUtils } from '@services/utils/utils';
import { AddonModForumPostOptionsMenuComponent } from '../post-options-menu/post-options-menu';
import { AddonModForumEditPostComponent } from '../edit-post/edit-post';
import { CoreRatingInfo } from '@features/rating/services/rating';
import { CoreForms } from '@singletons/form';
/**
* Components that shows a discussion post, its attachments and the action buttons allowed (reply, etc.).
@ -129,7 +130,7 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges
ngOnChanges(changes: {[name: string]: SimpleChange}): void {
if (changes.leavingPage && this.leavingPage) {
// Download all courses is enabled now, initialize it.
CoreDomUtils.triggerFormCancelledEvent(this.formElement, CoreSites.getCurrentSiteId());
CoreForms.triggerFormCancelledEvent(this.formElement, CoreSites.getCurrentSiteId());
}
}
@ -498,7 +499,7 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges
this.onPostChange.emit();
CoreDomUtils.triggerFormSubmittedEvent(this.formElement, sent, CoreSites.getCurrentSiteId());
CoreForms.triggerFormSubmittedEvent(this.formElement, sent, CoreSites.getCurrentSiteId());
if (this.syncId) {
CoreSync.unblockOperation(AddonModForumProvider.COMPONENT, this.syncId);
@ -520,7 +521,7 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges
// Reset data.
this.setReplyFormData();
CoreDomUtils.triggerFormCancelledEvent(this.formElement, CoreSites.getCurrentSiteId());
CoreForms.triggerFormCancelledEvent(this.formElement, CoreSites.getCurrentSiteId());
if (this.syncId) {
CoreSync.unblockOperation(AddonModForumProvider.COMPONENT, this.syncId);

View File

@ -39,6 +39,7 @@ import { CoreFileUploader } from '@features/fileuploader/services/fileuploader';
import { CoreTextUtils } from '@services/utils/text';
import { CanLeave } from '@guards/can-leave';
import { CoreSplitViewComponent } from '@components/split-view/split-view';
import { CoreForms } from '@singletons/form';
type NewDiscussionData = {
subject: string;
@ -519,7 +520,7 @@ export class AddonModForumNewDiscussionPage implements OnInit, OnDestroy, CanLea
CoreDomUtils.showErrorModalDefault(null, 'addon.mod_forum.errorposttoallgroups', true);
}
CoreDomUtils.triggerFormSubmittedEvent(
CoreForms.triggerFormSubmittedEvent(
this.formElement,
!!discussionIds,
CoreSites.getCurrentSiteId(),
@ -551,7 +552,7 @@ export class AddonModForumNewDiscussionPage implements OnInit, OnDestroy, CanLea
await Promise.all(promises);
CoreDomUtils.triggerFormCancelledEvent(this.formElement, CoreSites.getCurrentSiteId());
CoreForms.triggerFormCancelledEvent(this.formElement, CoreSites.getCurrentSiteId());
this.returnToDiscussions();
} catch (error) {
@ -585,7 +586,7 @@ export class AddonModForumNewDiscussionPage implements OnInit, OnDestroy, CanLea
CoreFileUploader.clearTmpFiles(this.newDiscussion.files);
if (this.formElement) {
CoreDomUtils.triggerFormCancelledEvent(this.formElement, CoreSites.getCurrentSiteId());
CoreForms.triggerFormCancelledEvent(this.formElement, CoreSites.getCurrentSiteId());
}
return true;

Some files were not shown because too many files have changed in this diff Show More