commit
a9df9c6411
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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']);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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> {
|
||||
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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>
|
|
@ -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 {}
|
|
@ -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 {}
|
|
@ -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>
|
|
@ -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>;
|
||||
};
|
|
@ -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>
|
|
@ -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[];
|
||||
};
|
|
@ -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>
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 {}
|
|
@ -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 {}
|
|
@ -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;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 {}
|
|
@ -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>
|
|
@ -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>');
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
|
@ -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>
|
|
@ -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()));
|
||||
}
|
||||
|
||||
}
|
|
@ -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 {}
|
|
@ -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);
|
|
@ -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 { }
|
|
@ -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>
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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 {}
|
|
@ -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);
|
|
@ -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>
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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 {}
|
|
@ -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);
|
|
@ -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>
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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 {}
|
|
@ -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);
|
|
@ -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>
|
|
@ -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>');
|
||||
}
|
||||
|
||||
}
|
|
@ -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 {}
|
|
@ -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);
|
|
@ -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>
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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 {}
|
|
@ -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);
|
|
@ -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/>
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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 {}
|
|
@ -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);
|
|
@ -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>
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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 {}
|
|
@ -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);
|
|
@ -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>
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
|
@ -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 {}
|
|
@ -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>
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
|
@ -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 {}
|
|
@ -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>
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
|
@ -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 {}
|
|
@ -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."
|
||||
}
|
|
@ -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>
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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>
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
|
@ -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);
|
|
@ -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[];
|
||||
};
|
|
@ -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
|
@ -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;
|
||||
};
|
|
@ -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);
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue