forked from CIT/Vmeda.Online
		
	MOBILE-3640 database: Add actions and pages
This commit is contained in:
		
							parent
							
								
									8febbe3ea7
								
							
						
					
					
						commit
						1363951920
					
				
							
								
								
									
										140
									
								
								src/addons/mod/data/components/action/action.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								src/addons/mod/data/components/action/action.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,140 @@
 | 
			
		||||
// (C) Copyright 2015 Moodle Pty Ltd.
 | 
			
		||||
//
 | 
			
		||||
// Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
// you may not use this file except in compliance with the License.
 | 
			
		||||
// You may obtain a copy of the License at
 | 
			
		||||
//
 | 
			
		||||
//     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
//
 | 
			
		||||
// Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
// distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { Component, OnInit, Input } from '@angular/core';
 | 
			
		||||
import { Params } from '@angular/router';
 | 
			
		||||
import { CoreCourseModule } from '@features/course/services/course-helper';
 | 
			
		||||
import { CoreTag } from '@features/tag/services/tag';
 | 
			
		||||
import { CoreUser } from '@features/user/services/user';
 | 
			
		||||
import { CoreNavigator } from '@services/navigator';
 | 
			
		||||
import { CoreSites } from '@services/sites';
 | 
			
		||||
import { CoreEvents } from '@singletons/events';
 | 
			
		||||
import {
 | 
			
		||||
    AddonModDataAction,
 | 
			
		||||
    AddonModDataData,
 | 
			
		||||
    AddonModDataEntry,
 | 
			
		||||
    AddonModDataProvider,
 | 
			
		||||
    AddonModDataTemplateMode,
 | 
			
		||||
} from '../../services/data';
 | 
			
		||||
import { AddonModDataHelper } from '../../services/data-helper';
 | 
			
		||||
import { AddonModDataOffline } from '../../services/data-offline';
 | 
			
		||||
import { AddonModDataModuleHandlerService } from '../../services/handlers/module';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Component that displays a database action.
 | 
			
		||||
 */
 | 
			
		||||
@Component({
 | 
			
		||||
    selector: 'addon-mod-data-action',
 | 
			
		||||
    templateUrl: 'addon-mod-data-action.html',
 | 
			
		||||
})
 | 
			
		||||
export class AddonModDataActionComponent implements OnInit {
 | 
			
		||||
 | 
			
		||||
    @Input() mode!: AddonModDataTemplateMode; // The render mode.
 | 
			
		||||
    @Input() action!: AddonModDataAction; // The field to render.
 | 
			
		||||
    @Input() entry!: AddonModDataEntry; // The value of the field.
 | 
			
		||||
    @Input() database!: AddonModDataData; // Database object.
 | 
			
		||||
    @Input() module!: CoreCourseModule; // Module object.
 | 
			
		||||
    @Input() group = 0; // Module object.
 | 
			
		||||
    @Input() offset?: number; // Offset of the entry.
 | 
			
		||||
 | 
			
		||||
    siteId: string;
 | 
			
		||||
    userPicture?: string;
 | 
			
		||||
    tagsEnabled = false;
 | 
			
		||||
 | 
			
		||||
    constructor() {
 | 
			
		||||
        this.siteId = CoreSites.getCurrentSiteId();
 | 
			
		||||
        this.tagsEnabled = CoreTag.areTagsAvailableInSite();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Component being initialized.
 | 
			
		||||
     */
 | 
			
		||||
    async ngOnInit(): Promise<void> {
 | 
			
		||||
        if (this.action == AddonModDataAction.USERPICTURE) {
 | 
			
		||||
            const profile = await CoreUser.getProfile(this.entry.userid, this.database.course);
 | 
			
		||||
            this.userPicture = profile.profileimageurl;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Approve the entry.
 | 
			
		||||
     */
 | 
			
		||||
    approveEntry(): void {
 | 
			
		||||
        AddonModDataHelper.approveOrDisapproveEntry(this.database.id, this.entry.id, true, this.database.course);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Show confirmation modal for deleting the entry.
 | 
			
		||||
     */
 | 
			
		||||
    deleteEntry(): void {
 | 
			
		||||
        AddonModDataHelper.showDeleteEntryModal(this.database.id, this.entry.id, this.database.course);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Disapprove the entry.
 | 
			
		||||
     */
 | 
			
		||||
    disapproveEntry(): void {
 | 
			
		||||
        AddonModDataHelper.approveOrDisapproveEntry(this.database.id, this.entry.id, false, this.database.course);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Go to the edit page of the entry.
 | 
			
		||||
     */
 | 
			
		||||
    editEntry(): void {
 | 
			
		||||
        const params = {
 | 
			
		||||
            courseId: this.database.course,
 | 
			
		||||
            module: this.module,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        CoreNavigator.navigateToSitePath(
 | 
			
		||||
            `${AddonModDataModuleHandlerService.PAGE_NAME}/${this.module.course}/${this.module.id}/edit/${this.entry.id}`,
 | 
			
		||||
            { params },
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Go to the view page of the entry.
 | 
			
		||||
     */
 | 
			
		||||
    viewEntry(): void {
 | 
			
		||||
        const params: Params = {
 | 
			
		||||
            courseId: this.database.course,
 | 
			
		||||
            module: this.module,
 | 
			
		||||
            entryId: this.entry.id,
 | 
			
		||||
            group: this.group,
 | 
			
		||||
            offset: this.offset,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        CoreNavigator.navigateToSitePath(
 | 
			
		||||
            `${AddonModDataModuleHandlerService.PAGE_NAME}/${this.module.course}/${this.module.id}/${this.entry.id}`,
 | 
			
		||||
            { params },
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Undo delete action.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Solved when done.
 | 
			
		||||
     */
 | 
			
		||||
    async undoDelete(): Promise<void> {
 | 
			
		||||
        const dataId = this.database.id;
 | 
			
		||||
        const entryId = this.entry.id;
 | 
			
		||||
 | 
			
		||||
        await AddonModDataOffline.getEntry(dataId, entryId, AddonModDataAction.DELETE, this.siteId);
 | 
			
		||||
 | 
			
		||||
        // Found. Just delete the action.
 | 
			
		||||
        await AddonModDataOffline.deleteEntry(dataId, entryId, AddonModDataAction.DELETE, this.siteId);
 | 
			
		||||
        CoreEvents.trigger(AddonModDataProvider.ENTRY_CHANGED, { dataId: dataId, entryId: entryId }, this.siteId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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>
 | 
			
		||||
							
								
								
									
										38
									
								
								src/addons/mod/data/components/components-compile.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								src/addons/mod/data/components/components-compile.module.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,38 @@
 | 
			
		||||
// (C) Copyright 2015 Moodle Pty Ltd.
 | 
			
		||||
//
 | 
			
		||||
// Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
// you may not use this file except in compliance with the License.
 | 
			
		||||
// You may obtain a copy of the License at
 | 
			
		||||
//
 | 
			
		||||
//     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
//
 | 
			
		||||
// Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
// distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { NgModule } from '@angular/core';
 | 
			
		||||
import { AddonModDataFieldPluginComponent } from './field-plugin/field-plugin';
 | 
			
		||||
import { AddonModDataActionComponent } from './action/action';
 | 
			
		||||
import { CoreSharedModule } from '@/core/shared.module';
 | 
			
		||||
import { CoreCommentsComponentsModule } from '@features/comments/components/components.module';
 | 
			
		||||
import { CoreTagComponentsModule } from '@features/tag/components/components.module';
 | 
			
		||||
 | 
			
		||||
// This module is intended to be passed to the compiler in order to avoid circular depencencies.
 | 
			
		||||
@NgModule({
 | 
			
		||||
    declarations: [
 | 
			
		||||
        AddonModDataFieldPluginComponent,
 | 
			
		||||
        AddonModDataActionComponent,
 | 
			
		||||
    ],
 | 
			
		||||
    imports: [
 | 
			
		||||
        CoreSharedModule,
 | 
			
		||||
        CoreCommentsComponentsModule,
 | 
			
		||||
        CoreTagComponentsModule,
 | 
			
		||||
    ],
 | 
			
		||||
    exports: [
 | 
			
		||||
        AddonModDataActionComponent,
 | 
			
		||||
        AddonModDataFieldPluginComponent,
 | 
			
		||||
    ],
 | 
			
		||||
})
 | 
			
		||||
export class AddonModDataComponentsCompileModule {}
 | 
			
		||||
							
								
								
									
										37
									
								
								src/addons/mod/data/components/components.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								src/addons/mod/data/components/components.module.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,37 @@
 | 
			
		||||
// (C) Copyright 2015 Moodle Pty Ltd.
 | 
			
		||||
//
 | 
			
		||||
// Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
// you may not use this file except in compliance with the License.
 | 
			
		||||
// You may obtain a copy of the License at
 | 
			
		||||
//
 | 
			
		||||
//     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
//
 | 
			
		||||
// Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
// distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { NgModule } from '@angular/core';
 | 
			
		||||
import { CoreSharedModule } from '@/core/shared.module';
 | 
			
		||||
import { CoreCourseComponentsModule } from '@features/course/components/components.module';
 | 
			
		||||
import { AddonModDataIndexComponent } from './index';
 | 
			
		||||
import { AddonModDataSearchComponent } from './search/search';
 | 
			
		||||
import { CoreCompileHtmlComponentModule } from '@features/compile/components/compile-html/compile-html.module';
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
    declarations: [
 | 
			
		||||
        AddonModDataIndexComponent,
 | 
			
		||||
        AddonModDataSearchComponent,
 | 
			
		||||
    ],
 | 
			
		||||
    imports: [
 | 
			
		||||
        CoreSharedModule,
 | 
			
		||||
        CoreCourseComponentsModule,
 | 
			
		||||
        CoreCompileHtmlComponentModule,
 | 
			
		||||
    ],
 | 
			
		||||
    exports: [
 | 
			
		||||
        AddonModDataIndexComponent,
 | 
			
		||||
        AddonModDataSearchComponent,
 | 
			
		||||
    ],
 | 
			
		||||
})
 | 
			
		||||
export class AddonModDataComponentsModule {}
 | 
			
		||||
							
								
								
									
										154
									
								
								src/addons/mod/data/components/index/addon-mod-data-index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										154
									
								
								src/addons/mod/data/components/index/addon-mod-data-index.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,154 @@
 | 
			
		||||
<!-- Buttons to add to the header. -->
 | 
			
		||||
<core-navbar-buttons slot="end">
 | 
			
		||||
    <ion-button *ngIf="canSearch" (click)="showSearch()" [attr.aria-label]="'addon.mod_data.search' | translate">
 | 
			
		||||
        <ion-icon name="fas-search" slot="icon-only"></ion-icon>
 | 
			
		||||
    </ion-button>
 | 
			
		||||
    <core-context-menu>
 | 
			
		||||
        <core-context-menu-item *ngIf="externalUrl" [priority]="900" [content]="'core.openinbrowser' | translate"
 | 
			
		||||
            [href]="externalUrl" iconAction="fas-external-link-alt">
 | 
			
		||||
        </core-context-menu-item>
 | 
			
		||||
        <core-context-menu-item *ngIf="description" [priority]="800" [content]="'core.moduleintro' | translate"
 | 
			
		||||
            (action)="expandDescription()" iconAction="fas-arrow-right">
 | 
			
		||||
        </core-context-menu-item>
 | 
			
		||||
        <core-context-menu-item *ngIf="blog" [priority]="750" content="{{'addon.blog.blog' | translate}}"
 | 
			
		||||
            iconAction="far-newspaper" (action)="gotoBlog()">
 | 
			
		||||
        </core-context-menu-item>
 | 
			
		||||
        <core-context-menu-item *ngIf="loaded && !(hasOffline || hasOfflineRatings) && isOnline" [priority]="700"
 | 
			
		||||
            [content]="'core.refresh' | translate" (action)="doRefresh(null, $event)" [iconAction]="refreshIcon"
 | 
			
		||||
            [closeOnClick]="false">
 | 
			
		||||
        </core-context-menu-item>
 | 
			
		||||
        <core-context-menu-item *ngIf="loaded && (hasOffline || hasOfflineRatings) && isOnline"  [priority]="600"
 | 
			
		||||
            [content]="'core.settings.synchronizenow' | translate" (action)="doRefresh(null, $event, true)" [iconAction]="syncIcon"
 | 
			
		||||
            [closeOnClick]="false">
 | 
			
		||||
        </core-context-menu-item>
 | 
			
		||||
        <core-context-menu-item [priority]="500" *ngIf="canAdd" [content]="'addon.mod_data.addentries' | translate"
 | 
			
		||||
            iconAction="fas-plus" (action)="gotoAddEntries()">
 | 
			
		||||
        </core-context-menu-item>
 | 
			
		||||
        <core-context-menu-item [priority]="400" *ngIf="firstEntry" [content]="'addon.mod_data.single' | translate"
 | 
			
		||||
            iconAction="fas-file" (action)="gotoEntry(firstEntry)">
 | 
			
		||||
        </core-context-menu-item>
 | 
			
		||||
        <core-context-menu-item *ngIf="prefetchStatusIcon" [priority]="300" [content]="prefetchText" (action)="prefetch($event)"
 | 
			
		||||
            [iconAction]="prefetchStatusIcon" [closeOnClick]="false">
 | 
			
		||||
        </core-context-menu-item>
 | 
			
		||||
        <core-context-menu-item *ngIf="size" [priority]="200" [content]="'core.clearstoreddata' | translate:{$a: size}"
 | 
			
		||||
            iconDescription="fas-archive" (action)="removeFiles($event)" iconAction="fas-trash" [closeOnClick]="false">
 | 
			
		||||
        </core-context-menu-item>
 | 
			
		||||
    </core-context-menu>
 | 
			
		||||
</core-navbar-buttons>
 | 
			
		||||
 | 
			
		||||
<!-- Content. -->
 | 
			
		||||
 | 
			
		||||
<ion-content>
 | 
			
		||||
    <core-loading [hideUntil]="loaded" class="core-loading-center">
 | 
			
		||||
 | 
			
		||||
        <core-course-module-description [description]="description" [component]="component" [componentId]="componentId"
 | 
			
		||||
            contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
 | 
			
		||||
        </core-course-module-description>
 | 
			
		||||
 | 
			
		||||
        <!-- Data done in offline but not synchronized -->
 | 
			
		||||
        <ion-card class="core-warning-card" *ngIf="hasOffline || hasOfflineRatings">
 | 
			
		||||
            <ion-item>
 | 
			
		||||
                <ion-icon name="fas-exclamation-triangle" slot="start"></ion-icon>
 | 
			
		||||
                <ion-label>{{ 'core.hasdatatosync' | translate: {$a: moduleName} }}</ion-label>
 | 
			
		||||
            </ion-item>
 | 
			
		||||
        </ion-card>
 | 
			
		||||
 | 
			
		||||
        <ion-item class="ion-text-wrap" *ngIf="groupInfo && (groupInfo.separateGroups || groupInfo.visibleGroups)">
 | 
			
		||||
            <ion-label id="addon-data-groupslabel">
 | 
			
		||||
                <ng-container *ngIf="groupInfo.separateGroups">{{'core.groupsseparate' | translate }}</ng-container>
 | 
			
		||||
                <ng-container *ngIf="groupInfo.visibleGroups">{{'core.groupsvisible' | translate }}</ng-container>
 | 
			
		||||
            </ion-label>
 | 
			
		||||
            <ion-select [(ngModel)]="selectedGroup" (ionChange)="setGroup(selectedGroup)" aria-labelledby="addon-data-groupslabel"
 | 
			
		||||
                interface="action-sheet">
 | 
			
		||||
                <ion-select-option *ngFor="let groupOpt of groupInfo.groups" [value]="groupOpt.id">
 | 
			
		||||
                    {{groupOpt.name}}
 | 
			
		||||
                </ion-select-option>
 | 
			
		||||
            </ion-select>
 | 
			
		||||
        </ion-item>
 | 
			
		||||
 | 
			
		||||
        <ion-card class="core-info-card" *ngIf="!access?.timeavailable && timeAvailableFrom">
 | 
			
		||||
            <ion-item>
 | 
			
		||||
                <ion-icon name="fas-info-circle" slot="start"></ion-icon>
 | 
			
		||||
                <ion-label>{{ 'addon.mod_data.notopenyet' | translate:{$a: timeAvailableFromReadable} }}</ion-label>
 | 
			
		||||
            </ion-item>
 | 
			
		||||
        </ion-card>
 | 
			
		||||
 | 
			
		||||
        <ion-card class="core-info-card" *ngIf="!access?.timeavailable && timeAvailableTo">
 | 
			
		||||
            <ion-item>
 | 
			
		||||
                <ion-icon name="fas-info-circle" slot="start"></ion-icon>
 | 
			
		||||
                <ion-label>{{ 'addon.mod_data.expired' | translate:{$a: timeAvailableToReadable} }}</ion-label>
 | 
			
		||||
            </ion-item>
 | 
			
		||||
        </ion-card>
 | 
			
		||||
 | 
			
		||||
        <ion-card class="core-info-card" *ngIf="access && access.entrieslefttoview">>
 | 
			
		||||
            <ion-item>
 | 
			
		||||
                <ion-icon name="fas-info-circle" slot="start"></ion-icon>
 | 
			
		||||
                <ion-label>
 | 
			
		||||
                    {{ 'addon.mod_data.entrieslefttoaddtoview' | translate:{$a: {entrieslefttoview: access.entrieslefttoview} } }}
 | 
			
		||||
                </ion-label>
 | 
			
		||||
            </ion-item>
 | 
			
		||||
        </ion-card>
 | 
			
		||||
 | 
			
		||||
        <ion-card class="core-info-card" *ngIf="access && access.entrieslefttoadd">>
 | 
			
		||||
            <ion-item>
 | 
			
		||||
                <ion-icon name="fas-info-circle" slot="start"></ion-icon>
 | 
			
		||||
                <ion-label>
 | 
			
		||||
                    {{ 'addon.mod_data.entrieslefttoadd' | translate:{$a: {entriesleft: access.entrieslefttoadd} } }}
 | 
			
		||||
                </ion-label>
 | 
			
		||||
            </ion-item>
 | 
			
		||||
        </ion-card>
 | 
			
		||||
 | 
			
		||||
        <!-- Reset search. -->
 | 
			
		||||
        <ng-container *ngIf="search.searching && !isEmpty">
 | 
			
		||||
            <ion-item *ngIf="!foundRecordsTranslationData">
 | 
			
		||||
                <ion-label>
 | 
			
		||||
                    <a (click)="searchReset()">{{ 'addon.mod_data.resetsettings' | translate}}</a>
 | 
			
		||||
                </ion-label>
 | 
			
		||||
            </ion-item>
 | 
			
		||||
 | 
			
		||||
            <ion-card class="core-success-card" *ngIf="foundRecordsTranslationData" (click)="searchReset()">
 | 
			
		||||
                <ion-item><ion-label>
 | 
			
		||||
                <p [innerHTML]="'addon.mod_data.foundrecords' | translate:{$a: foundRecordsTranslationData}"></p>
 | 
			
		||||
                </ion-label></ion-item>
 | 
			
		||||
            </ion-card>
 | 
			
		||||
        </ng-container>
 | 
			
		||||
 | 
			
		||||
        <div class="addon-data-contents addon-data-entries-{{database.id}} ion-padding-horizontal" *ngIf="!isEmpty && database">
 | 
			
		||||
            <core-style [css]="database.csstemplate" prefix=".addon-data-entries-{{database.id}}"></core-style>
 | 
			
		||||
 | 
			
		||||
            <core-compile-html [text]="entriesRendered" [jsData]="jsData" [extraImports]="extraImports"></core-compile-html>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <ion-grid *ngIf="search.page > 0 || hasNextPage">
 | 
			
		||||
            <ion-row class="ion-align-items-center">
 | 
			
		||||
                <ion-col *ngIf="search.page > 0">
 | 
			
		||||
                    <ion-button expand="block" fill="outline" (click)="searchEntries(search.page - 1)">
 | 
			
		||||
                        <ion-icon name="fas-chevron-left" slot="start"></ion-icon>
 | 
			
		||||
                        {{ 'core.previous' | translate }}
 | 
			
		||||
                    </ion-button>
 | 
			
		||||
                </ion-col>
 | 
			
		||||
                <ion-col *ngIf="hasNextPage">
 | 
			
		||||
                    <ion-button expand="block" (click)="searchEntries(search.page + 1)">
 | 
			
		||||
                        {{ 'core.next' | translate }}
 | 
			
		||||
                        <ion-icon name="fas-chevron-right" slot="end"></ion-icon>
 | 
			
		||||
                    </ion-button>
 | 
			
		||||
                </ion-col>
 | 
			
		||||
            </ion-row>
 | 
			
		||||
        </ion-grid>
 | 
			
		||||
 | 
			
		||||
        <core-empty-box *ngIf="isEmpty && !search.searching" icon="fas-database" [message]="'addon.mod_data.norecords' | translate">
 | 
			
		||||
        </core-empty-box>
 | 
			
		||||
 | 
			
		||||
        <core-empty-box *ngIf="isEmpty && search.searching" icon="fas-database" [message]="'addon.mod_data.nomatch' | translate"
 | 
			
		||||
            class="core-empty-box-clickable">
 | 
			
		||||
            <a (click)="searchReset()">{{ 'addon.mod_data.resetsettings' | translate}}</a>
 | 
			
		||||
        </core-empty-box>
 | 
			
		||||
 | 
			
		||||
    </core-loading>
 | 
			
		||||
 | 
			
		||||
    <ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end" *ngIf="canAdd">
 | 
			
		||||
        <ion-fab-button (click)="gotoAddEntries()" [attr.aria-label]="'addon.mod_data.addentries' | translate">
 | 
			
		||||
            <ion-icon name="fas-plus"></ion-icon>
 | 
			
		||||
        </ion-fab-button>
 | 
			
		||||
    </ion-fab>
 | 
			
		||||
</ion-content>
 | 
			
		||||
							
								
								
									
										556
									
								
								src/addons/mod/data/components/index/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										556
									
								
								src/addons/mod/data/components/index/index.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,556 @@
 | 
			
		||||
// (C) Copyright 2015 Moodle Pty Ltd.
 | 
			
		||||
//
 | 
			
		||||
// Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
// you may not use this file except in compliance with the License.
 | 
			
		||||
// You may obtain a copy of the License at
 | 
			
		||||
//
 | 
			
		||||
//     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
//
 | 
			
		||||
// Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
// distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { ContextLevel } from '@/core/constants';
 | 
			
		||||
import { Component, OnDestroy, OnInit, Optional, Type } from '@angular/core';
 | 
			
		||||
import { Params } from '@angular/router';
 | 
			
		||||
import { CoreCommentsProvider } from '@features/comments/services/comments';
 | 
			
		||||
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
 | 
			
		||||
import { CoreCourseModule } from '@features/course/course.module';
 | 
			
		||||
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
 | 
			
		||||
import { CoreCourse } from '@features/course/services/course';
 | 
			
		||||
import { CoreRatingProvider } from '@features/rating/services/rating';
 | 
			
		||||
import { CoreRatingSyncProvider } from '@features/rating/services/rating-sync';
 | 
			
		||||
import { IonContent } from '@ionic/angular';
 | 
			
		||||
import { CoreGroupInfo, CoreGroups } from '@services/groups';
 | 
			
		||||
import { CoreNavigator } from '@services/navigator';
 | 
			
		||||
import { CoreSites } from '@services/sites';
 | 
			
		||||
import { CoreDomUtils } from '@services/utils/dom';
 | 
			
		||||
import { CoreTimeUtils } from '@services/utils/time';
 | 
			
		||||
import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
import { ModalController } from '@singletons';
 | 
			
		||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
 | 
			
		||||
import {
 | 
			
		||||
    AddonModDataProvider,
 | 
			
		||||
    AddonModData,
 | 
			
		||||
    AddonModDataEntry,
 | 
			
		||||
    AddonModDataTemplateType,
 | 
			
		||||
    AddonModDataTemplateMode,
 | 
			
		||||
    AddonModDataField,
 | 
			
		||||
    AddonModDataGetDataAccessInformationWSResponse,
 | 
			
		||||
    AddonModDataData,
 | 
			
		||||
    AddonModDataSearchEntriesAdvancedField,
 | 
			
		||||
} from '../../services/data';
 | 
			
		||||
import { AddonModDataHelper } from '../../services/data-helper';
 | 
			
		||||
import { AddonModDataAutoSyncData, AddonModDataSyncProvider, AddonModDataSyncResult } from '../../services/data-sync';
 | 
			
		||||
import { AddonModDataPrefetchHandler } from '../../services/handlers/prefetch';
 | 
			
		||||
import { AddonModDataComponentsCompileModule } from '../components-compile.module';
 | 
			
		||||
import { AddonModDataSearchComponent } from '../search/search';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Component that displays a data index page.
 | 
			
		||||
 */
 | 
			
		||||
@Component({
 | 
			
		||||
    selector: 'addon-mod-data-index',
 | 
			
		||||
    templateUrl: 'addon-mod-data-index.html',
 | 
			
		||||
    styleUrls: ['../../data.scss'],
 | 
			
		||||
})
 | 
			
		||||
export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComponent implements OnInit, OnDestroy {
 | 
			
		||||
 | 
			
		||||
    component = AddonModDataProvider.COMPONENT;
 | 
			
		||||
    moduleName = 'data';
 | 
			
		||||
 | 
			
		||||
    access?: AddonModDataGetDataAccessInformationWSResponse;
 | 
			
		||||
    database?: AddonModDataData;
 | 
			
		||||
    fields: Record<number, AddonModDataField> = {};
 | 
			
		||||
    selectedGroup = 0;
 | 
			
		||||
    timeAvailableFrom?: number;
 | 
			
		||||
    timeAvailableFromReadable?: string;
 | 
			
		||||
    timeAvailableTo?: number;
 | 
			
		||||
    timeAvailableToReadable?: string;
 | 
			
		||||
    isEmpty = true;
 | 
			
		||||
    groupInfo?: CoreGroupInfo;
 | 
			
		||||
    entries: AddonModDataEntry[] = [];
 | 
			
		||||
    firstEntry?: number;
 | 
			
		||||
    canAdd = false;
 | 
			
		||||
    canSearch = false;
 | 
			
		||||
    search: AddonModDataSearchDataParams = {
 | 
			
		||||
        sortBy: '0',
 | 
			
		||||
        sortDirection: 'DESC',
 | 
			
		||||
        page: 0,
 | 
			
		||||
        text: '',
 | 
			
		||||
        searching: false,
 | 
			
		||||
        searchingAdvanced: false,
 | 
			
		||||
        advanced: [],
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    hasNextPage = false;
 | 
			
		||||
    entriesRendered = '';
 | 
			
		||||
    extraImports: Type<unknown>[]  = [AddonModDataComponentsCompileModule];
 | 
			
		||||
 | 
			
		||||
    jsData? : {
 | 
			
		||||
        fields: Record<number, AddonModDataField>;
 | 
			
		||||
        entries: Record<number, AddonModDataEntry>;
 | 
			
		||||
        database: AddonModDataData;
 | 
			
		||||
        module: CoreCourseModule;
 | 
			
		||||
        group: number;
 | 
			
		||||
        gotoEntry: (a: number) => void;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // Data for found records translation.
 | 
			
		||||
    foundRecordsTranslationData? : {
 | 
			
		||||
        num: number;
 | 
			
		||||
        max: number;
 | 
			
		||||
        reseturl: string;
 | 
			
		||||
    };;
 | 
			
		||||
 | 
			
		||||
    hasOfflineRatings = false;
 | 
			
		||||
 | 
			
		||||
    protected syncEventName = AddonModDataSyncProvider.AUTO_SYNCED;
 | 
			
		||||
    protected hasComments = false;
 | 
			
		||||
    protected fieldsArray: AddonModDataField[] = [];
 | 
			
		||||
    protected entryChangedObserver?: CoreEventObserver;
 | 
			
		||||
    protected ratingOfflineObserver?: CoreEventObserver;
 | 
			
		||||
    protected ratingSyncObserver?: CoreEventObserver;
 | 
			
		||||
 | 
			
		||||
    constructor(
 | 
			
		||||
        protected content?: IonContent,
 | 
			
		||||
        @Optional() courseContentsPage?: CoreCourseContentsPage,
 | 
			
		||||
    ) {
 | 
			
		||||
        super('AddonModDataIndexComponent', content, courseContentsPage);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    async ngOnInit(): Promise<void> {
 | 
			
		||||
        await super.ngOnInit();
 | 
			
		||||
 | 
			
		||||
        this.selectedGroup = this.group || 0;
 | 
			
		||||
 | 
			
		||||
        // Refresh entries on change.
 | 
			
		||||
        this.entryChangedObserver = CoreEvents.on(AddonModDataProvider.ENTRY_CHANGED, (eventData) => {
 | 
			
		||||
            if (this.database?.id == eventData.dataId) {
 | 
			
		||||
                this.loaded = false;
 | 
			
		||||
 | 
			
		||||
                return this.loadContent(true);
 | 
			
		||||
            }
 | 
			
		||||
        }, this.siteId);
 | 
			
		||||
 | 
			
		||||
        // Listen for offline ratings saved and synced.
 | 
			
		||||
        this.ratingOfflineObserver = CoreEvents.on(CoreRatingProvider.RATING_SAVED_EVENT, (data) => {
 | 
			
		||||
            if (data.component == 'mod_data' && data.ratingArea == 'entry' && data.contextLevel == ContextLevel.MODULE
 | 
			
		||||
                    && data.instanceId == this.database?.coursemodule) {
 | 
			
		||||
                this.hasOfflineRatings = true;
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        this.ratingSyncObserver = CoreEvents.on(CoreRatingSyncProvider.SYNCED_EVENT, (data) => {
 | 
			
		||||
            if (data.component == 'mod_data' && data.ratingArea == 'entry' && data.contextLevel == ContextLevel.MODULE
 | 
			
		||||
                    && data.instanceId == this.database?.coursemodule) {
 | 
			
		||||
                this.hasOfflineRatings = false;
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        await this.loadContent(false, true);
 | 
			
		||||
        await this.logView(true);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Perform the invalidate content function.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async invalidateContent(): Promise<void> {
 | 
			
		||||
        const promises: Promise<void>[] = [];
 | 
			
		||||
 | 
			
		||||
        promises.push(AddonModData.invalidateDatabaseData(this.courseId));
 | 
			
		||||
        if (this.database) {
 | 
			
		||||
            promises.push(AddonModData.invalidateDatabaseAccessInformationData(this.database.id));
 | 
			
		||||
            promises.push(CoreGroups.invalidateActivityGroupInfo(this.database.coursemodule));
 | 
			
		||||
            promises.push(AddonModData.invalidateEntriesData(this.database.id));
 | 
			
		||||
            promises.push(AddonModData.invalidateFieldsData(this.database.id));
 | 
			
		||||
 | 
			
		||||
            if (this.hasComments) {
 | 
			
		||||
                CoreEvents.trigger(CoreCommentsProvider.REFRESH_COMMENTS_EVENT, {
 | 
			
		||||
                    contextLevel: ContextLevel.MODULE,
 | 
			
		||||
                    instanceId: this.database.coursemodule,
 | 
			
		||||
                }, CoreSites.getCurrentSiteId());
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await Promise.all(promises);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Compares sync event data with current data to check if refresh content is needed.
 | 
			
		||||
     *
 | 
			
		||||
     * @param syncEventData Data receiven on sync observer.
 | 
			
		||||
     * @return True if refresh is needed, false otherwise.
 | 
			
		||||
     */
 | 
			
		||||
    protected isRefreshSyncNeeded(syncEventData: AddonModDataAutoSyncData): boolean {
 | 
			
		||||
        if (this.database && syncEventData.dataId == this.database.id && typeof syncEventData.entryId == 'undefined') {
 | 
			
		||||
            this.loaded = false;
 | 
			
		||||
            // Refresh the data.
 | 
			
		||||
            this.content?.scrollToTop();
 | 
			
		||||
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Download data contents.
 | 
			
		||||
     *
 | 
			
		||||
     * @param refresh If it's refreshing content.
 | 
			
		||||
     * @param sync If it should try to sync.
 | 
			
		||||
     * @param showErrors If show errors to the user of hide them.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise<void> {
 | 
			
		||||
        let canAdd = false;
 | 
			
		||||
        let canSearch = false;
 | 
			
		||||
 | 
			
		||||
        this.database = await AddonModData.getDatabase(this.courseId, this.module.id);
 | 
			
		||||
        this.hasComments = this.database.comments;
 | 
			
		||||
 | 
			
		||||
        this.description = this.database.intro;
 | 
			
		||||
        this.dataRetrieved.emit(this.database);
 | 
			
		||||
 | 
			
		||||
        if (sync) {
 | 
			
		||||
            // Try to synchronize the data.
 | 
			
		||||
            await CoreUtils.ignoreErrors(this.syncActivity(showErrors));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.groupInfo = await CoreGroups.getActivityGroupInfo(this.database.coursemodule);
 | 
			
		||||
        this.selectedGroup = CoreGroups.validateGroupId(this.selectedGroup, this.groupInfo);
 | 
			
		||||
 | 
			
		||||
        this.access = await AddonModData.getDatabaseAccessInformation(this.database.id, {
 | 
			
		||||
            cmId: this.module.id,
 | 
			
		||||
            groupId: this.selectedGroup,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        if (!this.access.timeavailable) {
 | 
			
		||||
            const time = CoreTimeUtils.timestamp();
 | 
			
		||||
 | 
			
		||||
            this.timeAvailableFrom = this.database.timeavailablefrom && time < this.database.timeavailablefrom
 | 
			
		||||
                ? this.database.timeavailablefrom * 1000
 | 
			
		||||
                : undefined;
 | 
			
		||||
            this.timeAvailableFromReadable = this.timeAvailableFrom
 | 
			
		||||
                ? CoreTimeUtils.userDate(this.timeAvailableFrom)
 | 
			
		||||
                : undefined;
 | 
			
		||||
            this.timeAvailableTo = this.database.timeavailableto && time > this.database.timeavailableto
 | 
			
		||||
                ? this.database.timeavailableto * 1000
 | 
			
		||||
                : undefined;
 | 
			
		||||
            this.timeAvailableToReadable = this.timeAvailableTo
 | 
			
		||||
                ? CoreTimeUtils.userDate(this.timeAvailableTo)
 | 
			
		||||
                : undefined;
 | 
			
		||||
 | 
			
		||||
            this.isEmpty = true;
 | 
			
		||||
            this.groupInfo = undefined;
 | 
			
		||||
        } else {
 | 
			
		||||
            canSearch = true;
 | 
			
		||||
            canAdd = this.access.canaddentry;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const fields = await AddonModData.getFields(this.database.id, { cmId: this.module.id });
 | 
			
		||||
        this.search.advanced = [];
 | 
			
		||||
 | 
			
		||||
        this.fields = CoreUtils.arrayToObject(fields, 'id');
 | 
			
		||||
        this.fieldsArray = CoreUtils.objectToArray(this.fields);
 | 
			
		||||
        if (this.fieldsArray.length == 0) {
 | 
			
		||||
            canSearch = false;
 | 
			
		||||
            canAdd = false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            await this.fetchEntriesData();
 | 
			
		||||
        } finally {
 | 
			
		||||
            this.canAdd = canAdd;
 | 
			
		||||
            this.canSearch = canSearch;
 | 
			
		||||
            this.fillContextMenu(refresh);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Fetch current database entries.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Resolved then done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async fetchEntriesData(): Promise<void> {
 | 
			
		||||
 | 
			
		||||
        const search = this.search.searching && !this.search.searchingAdvanced ? this.search.text : undefined;
 | 
			
		||||
        const advSearch = this.search.searching && this.search.searchingAdvanced ? this.search.advanced : undefined;
 | 
			
		||||
 | 
			
		||||
        const entries = await AddonModDataHelper.fetchEntries(this.database!, this.fieldsArray, {
 | 
			
		||||
            groupId: this.selectedGroup,
 | 
			
		||||
            search,
 | 
			
		||||
            advSearch,
 | 
			
		||||
            sort: Number(this.search.sortBy),
 | 
			
		||||
            order: this.search.sortDirection,
 | 
			
		||||
            page: this.search.page,
 | 
			
		||||
            cmId: this.module.id,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        const numEntries = entries.entries.length;
 | 
			
		||||
        const numOfflineEntries = entries.offlineEntries?.length || 0;
 | 
			
		||||
 | 
			
		||||
        this.isEmpty = !numEntries && !numOfflineEntries;
 | 
			
		||||
 | 
			
		||||
        this.hasNextPage = numEntries >= AddonModDataProvider.PER_PAGE && ((this.search.page + 1) *
 | 
			
		||||
            AddonModDataProvider.PER_PAGE) < entries.totalcount;
 | 
			
		||||
 | 
			
		||||
        this.hasOffline = entries.hasOfflineActions;
 | 
			
		||||
 | 
			
		||||
        this.hasOfflineRatings = !!entries.hasOfflineRatings;
 | 
			
		||||
 | 
			
		||||
        this.entriesRendered = '';
 | 
			
		||||
 | 
			
		||||
        this.foundRecordsTranslationData = typeof entries.maxcount != 'undefined'
 | 
			
		||||
            ? {
 | 
			
		||||
                num: entries.totalcount,
 | 
			
		||||
                max: entries.maxcount,
 | 
			
		||||
                reseturl: '#',
 | 
			
		||||
            }
 | 
			
		||||
            : undefined;
 | 
			
		||||
 | 
			
		||||
        if (!this.isEmpty) {
 | 
			
		||||
            this.entries = (entries.offlineEntries || []).concat(entries.entries);
 | 
			
		||||
 | 
			
		||||
            let entriesHTML = AddonModDataHelper.getTemplate(
 | 
			
		||||
                this.database!,
 | 
			
		||||
                AddonModDataTemplateType.LIST_HEADER,
 | 
			
		||||
                this.fieldsArray,
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            // Get first entry from the whole list.
 | 
			
		||||
            if (!this.search.searching || !this.firstEntry) {
 | 
			
		||||
                this.firstEntry = this.entries[0].id;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const template = AddonModDataHelper.getTemplate(this.database!, AddonModDataTemplateType.LIST, this.fieldsArray);
 | 
			
		||||
 | 
			
		||||
            const entriesById: Record<number, AddonModDataEntry> = {};
 | 
			
		||||
            this.entries.forEach((entry, index) => {
 | 
			
		||||
                entriesById[entry.id] = entry;
 | 
			
		||||
 | 
			
		||||
                const actions = AddonModDataHelper.getActions(this.database!, this.access!, entry);
 | 
			
		||||
                const offset = this.search.searching
 | 
			
		||||
                    ? 0
 | 
			
		||||
                    : this.search.page * AddonModDataProvider.PER_PAGE + index - numOfflineEntries;
 | 
			
		||||
 | 
			
		||||
                entriesHTML += AddonModDataHelper.displayShowFields(
 | 
			
		||||
                    template,
 | 
			
		||||
                    this.fieldsArray,
 | 
			
		||||
                    entry,
 | 
			
		||||
                    offset,
 | 
			
		||||
                    AddonModDataTemplateMode.LIST,
 | 
			
		||||
                    actions,
 | 
			
		||||
                );
 | 
			
		||||
            });
 | 
			
		||||
            entriesHTML += AddonModDataHelper.getTemplate(this.database!, AddonModDataTemplateType.LIST_FOOTER, this.fieldsArray);
 | 
			
		||||
 | 
			
		||||
            this.entriesRendered = CoreDomUtils.fixHtml(entriesHTML);
 | 
			
		||||
 | 
			
		||||
            // Pass the input data to the component.
 | 
			
		||||
            this.jsData = {
 | 
			
		||||
                fields: this.fields,
 | 
			
		||||
                entries: entriesById,
 | 
			
		||||
                database: this.database!,
 | 
			
		||||
                module: this.module,
 | 
			
		||||
                group: this.selectedGroup,
 | 
			
		||||
                gotoEntry: this.gotoEntry.bind(this),
 | 
			
		||||
            };
 | 
			
		||||
        } else if (!this.search.searching) {
 | 
			
		||||
            // Empty and no searching.
 | 
			
		||||
            this.canSearch = false;
 | 
			
		||||
            this.firstEntry = undefined;
 | 
			
		||||
        } else {
 | 
			
		||||
            this.firstEntry = undefined;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Display the chat users modal.
 | 
			
		||||
     */
 | 
			
		||||
    async showSearch(): Promise<void> {
 | 
			
		||||
        const modal = await ModalController.create({
 | 
			
		||||
            component: AddonModDataSearchComponent,
 | 
			
		||||
            componentProps: {
 | 
			
		||||
                search: this.search,
 | 
			
		||||
                fields: this.fields,
 | 
			
		||||
                database: this.database,
 | 
			
		||||
            },
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        await modal.present();
 | 
			
		||||
 | 
			
		||||
        const result = await modal.onDidDismiss();
 | 
			
		||||
        // Add data to search object.
 | 
			
		||||
        if (result.data) {
 | 
			
		||||
            this.search = result.data;
 | 
			
		||||
            this.searchEntries(0);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Performs the search and closes the modal.
 | 
			
		||||
     *
 | 
			
		||||
     * @param page Page number.
 | 
			
		||||
     * @return Resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    async searchEntries(page: number): Promise<void> {
 | 
			
		||||
        this.loaded = false;
 | 
			
		||||
        this.search.page = page;
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            await this.fetchEntriesData();
 | 
			
		||||
            // Log activity view for coherence with Moodle web.
 | 
			
		||||
            await this.logView();
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            CoreDomUtils.showErrorModalDefault(error, 'core.course.errorgetmodule', true);
 | 
			
		||||
        } finally {
 | 
			
		||||
            this.loaded = true;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Reset all search filters and closes the modal.
 | 
			
		||||
     */
 | 
			
		||||
    searchReset(): void {
 | 
			
		||||
        this.search.sortBy = '0';
 | 
			
		||||
        this.search.sortDirection = 'DESC';
 | 
			
		||||
        this.search.text = '';
 | 
			
		||||
        this.search.advanced = [];
 | 
			
		||||
        this.search.searchingAdvanced = false;
 | 
			
		||||
        this.search.searching = false;
 | 
			
		||||
        this.searchEntries(0);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Set group to see the database.
 | 
			
		||||
     *
 | 
			
		||||
     * @param groupId Group ID.
 | 
			
		||||
     * @return Resolved when new group is selected or rejected if not.
 | 
			
		||||
     */
 | 
			
		||||
    async setGroup(groupId: number): Promise<void> {
 | 
			
		||||
        this.selectedGroup = groupId;
 | 
			
		||||
        this.search.page = 0;
 | 
			
		||||
 | 
			
		||||
        // Only update canAdd if there's any field, otheerwise, canAdd will remain false.
 | 
			
		||||
        if (this.fieldsArray.length > 0) {
 | 
			
		||||
            // Update values for current group.
 | 
			
		||||
            this.access = await AddonModData.getDatabaseAccessInformation(this.database!.id, {
 | 
			
		||||
                groupId: this.selectedGroup,
 | 
			
		||||
                cmId: this.module.id,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            this.canAdd = this.access.canaddentry;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            await this.fetchEntriesData();
 | 
			
		||||
 | 
			
		||||
            // Log activity view for coherence with Moodle web.
 | 
			
		||||
            return this.logView();
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            CoreDomUtils.showErrorModalDefault(error, 'core.course.errorgetmodule', true);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Opens add entries form.
 | 
			
		||||
     */
 | 
			
		||||
    gotoAddEntries(): void {
 | 
			
		||||
        const params: Params = {
 | 
			
		||||
            module: this.module,
 | 
			
		||||
            courseId: this.courseId,
 | 
			
		||||
            group: this.selectedGroup,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        CoreNavigator.navigate('edit', { params });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Goto the selected entry.
 | 
			
		||||
     *
 | 
			
		||||
     * @param entryId Entry ID.
 | 
			
		||||
     */
 | 
			
		||||
    gotoEntry(entryId: number): void {
 | 
			
		||||
        const params: Params = {
 | 
			
		||||
            module: this.module,
 | 
			
		||||
            courseId: this.courseId,
 | 
			
		||||
            group: this.selectedGroup,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // Try to find page number and offset of the entry.
 | 
			
		||||
        const pageXOffset = this.entries.findIndex((entry) => entry.id == entryId);
 | 
			
		||||
        if (pageXOffset >= 0) {
 | 
			
		||||
            params.offset = this.search.page * AddonModDataProvider.PER_PAGE + pageXOffset;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        CoreNavigator.navigate(String(entryId), { params });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Performs the sync of the activity.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async sync(): Promise<void> {
 | 
			
		||||
        await AddonModDataPrefetchHandler.sync(this.module, this.courseId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Checks if sync has succeed from result sync data.
 | 
			
		||||
     *
 | 
			
		||||
     * @param result Data returned on the sync function.
 | 
			
		||||
     * @return If suceed or not.
 | 
			
		||||
     */
 | 
			
		||||
    protected hasSyncSucceed(result: AddonModDataSyncResult): boolean {
 | 
			
		||||
        return result.updated;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Log viewing the activity.
 | 
			
		||||
     *
 | 
			
		||||
     * @param checkCompletion Whether to check completion.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async logView(checkCompletion = false): Promise<void> {
 | 
			
		||||
        if (!this.database || !this.database.id) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            await AddonModData.logView(this.database.id, this.database.name);
 | 
			
		||||
            if (checkCompletion) {
 | 
			
		||||
                CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
 | 
			
		||||
            }
 | 
			
		||||
        } catch {
 | 
			
		||||
            // Ignore errors, the user could be offline.
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    ngOnDestroy(): void {
 | 
			
		||||
        super.ngOnDestroy();
 | 
			
		||||
        this.entryChangedObserver?.off();
 | 
			
		||||
        this.ratingOfflineObserver?.off();
 | 
			
		||||
        this.ratingSyncObserver?.off();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type AddonModDataSearchDataParams = {
 | 
			
		||||
    sortBy: string;
 | 
			
		||||
    sortDirection: string;
 | 
			
		||||
    page: number;
 | 
			
		||||
    text: string;
 | 
			
		||||
    searching: boolean;
 | 
			
		||||
    searchingAdvanced: boolean;
 | 
			
		||||
    advanced?: AddonModDataSearchEntriesAdvancedField[];
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										68
									
								
								src/addons/mod/data/components/search/search.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								src/addons/mod/data/components/search/search.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,68 @@
 | 
			
		||||
<ion-header>
 | 
			
		||||
    <ion-toolbar>
 | 
			
		||||
        <ion-buttons slot="start">
 | 
			
		||||
            <ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
 | 
			
		||||
        </ion-buttons>
 | 
			
		||||
        <ion-title>{{ 'addon.mod_data.search' | translate }}</ion-title>
 | 
			
		||||
        <ion-buttons slot="end">
 | 
			
		||||
            <ion-button (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
 | 
			
		||||
                <ion-icon name="fas-times" slot="icon-only"></ion-icon>
 | 
			
		||||
            </ion-button>
 | 
			
		||||
        </ion-buttons>
 | 
			
		||||
    </ion-toolbar>
 | 
			
		||||
</ion-header>
 | 
			
		||||
<ion-content>
 | 
			
		||||
    <ion-item>
 | 
			
		||||
        <ion-label>{{ 'addon.mod_data.advancedsearch' | translate }}</ion-label>
 | 
			
		||||
        <ion-toggle [(ngModel)]="search.searchingAdvanced"></ion-toggle>
 | 
			
		||||
    </ion-item>
 | 
			
		||||
    <form (ngSubmit)="searchEntries($event)" [formGroup]="searchForm" #searchFormEl>
 | 
			
		||||
        <ion-list  class="ion-no-margin">
 | 
			
		||||
            <ion-item [hidden]="search.searchingAdvanced">
 | 
			
		||||
                <ion-label></ion-label>
 | 
			
		||||
                <ion-input type="text" placeholder="{{ 'addon.mod_data.search' | translate}}"
 | 
			
		||||
                    [(ngModel)]="search.text" name="text" formControlName="text">
 | 
			
		||||
                </ion-input>
 | 
			
		||||
            </ion-item>
 | 
			
		||||
            <ion-item class="ion-text-wrap">
 | 
			
		||||
                <ion-label position="stacked">{{ 'core.sortby' | translate }}</ion-label>
 | 
			
		||||
                <ion-select interface="action-sheet" name="sortBy" formControlName="sortBy"
 | 
			
		||||
                    [placeholder]="'core.sortby' | translate">
 | 
			
		||||
                    <optgroup *ngIf="fieldsArray.length" label="{{ 'addon.mod_data.fields' | translate }}">
 | 
			
		||||
                        <ion-select-option *ngFor="let field of fieldsArray" [value]="field.id">{{field.name}}</ion-select-option>
 | 
			
		||||
                    </optgroup>
 | 
			
		||||
                    <optgroup label="{{ 'addon.mod_data.other' | translate }}">
 | 
			
		||||
                        <ion-select-option value="0">{{ 'addon.mod_data.timeadded' | translate }}</ion-select-option>
 | 
			
		||||
                        <ion-select-option value="-4">{{ 'addon.mod_data.timemodified' | translate }}</ion-select-option>
 | 
			
		||||
                        <ion-select-option value="-1">{{ 'addon.mod_data.authorfirstname' | translate }}</ion-select-option>
 | 
			
		||||
                        <ion-select-option value="-2">{{ 'addon.mod_data.authorlastname' | translate }}</ion-select-option>
 | 
			
		||||
                        <ion-select-option value="-3" *ngIf="database.approval">
 | 
			
		||||
                            {{ 'addon.mod_data.approved' | translate }}
 | 
			
		||||
                        </ion-select-option>
 | 
			
		||||
                    </optgroup>
 | 
			
		||||
                </ion-select>
 | 
			
		||||
            </ion-item>
 | 
			
		||||
            <ion-list >
 | 
			
		||||
                <ion-radio-group [(ngModel)]="search.sortDirection" name="sortDirection" formControlName="sortDirection">
 | 
			
		||||
                    <ion-item>
 | 
			
		||||
                        <ion-label>{{ 'addon.mod_data.ascending' | translate }}</ion-label>
 | 
			
		||||
                        <ion-radio slot="start" value="ASC"></ion-radio>
 | 
			
		||||
                    </ion-item>
 | 
			
		||||
                    <ion-item>
 | 
			
		||||
                        <ion-label>{{ 'addon.mod_data.descending' | translate }}</ion-label>
 | 
			
		||||
                        <ion-radio slot="start" value="DESC"></ion-radio>
 | 
			
		||||
                    </ion-item>
 | 
			
		||||
                </ion-radio-group>
 | 
			
		||||
            </ion-list>
 | 
			
		||||
            <div class="ion-padding addon-data-advanced-search" [hidden]="!advancedSearch || !search.searchingAdvanced">
 | 
			
		||||
                <core-compile-html [text]="advancedSearch" [jsData]="jsData" [extraImports]="extraImports"></core-compile-html>
 | 
			
		||||
            </div>
 | 
			
		||||
        </ion-list>
 | 
			
		||||
        <div class="ion-padding">
 | 
			
		||||
            <ion-button expand="block" type="submit">
 | 
			
		||||
                <ion-icon name="fas-search" slot="start"></ion-icon>
 | 
			
		||||
                {{ 'addon.mod_data.search' | translate }}
 | 
			
		||||
            </ion-button>
 | 
			
		||||
        </div>
 | 
			
		||||
    </form>
 | 
			
		||||
</ion-content>
 | 
			
		||||
							
								
								
									
										216
									
								
								src/addons/mod/data/components/search/search.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										216
									
								
								src/addons/mod/data/components/search/search.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,216 @@
 | 
			
		||||
// (C) Copyright 2015 Moodle Pty Ltd.
 | 
			
		||||
//
 | 
			
		||||
// Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
// you may not use this file except in compliance with the License.
 | 
			
		||||
// You may obtain a copy of the License at
 | 
			
		||||
//
 | 
			
		||||
//     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
//
 | 
			
		||||
// Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
// distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { Component, ElementRef, Input, OnInit, Type, ViewChild } from '@angular/core';
 | 
			
		||||
import { FormGroup, FormBuilder } from '@angular/forms';
 | 
			
		||||
import { CoreTag } from '@features/tag/services/tag';
 | 
			
		||||
import { CoreSites } from '@services/sites';
 | 
			
		||||
import { CoreFormFields, CoreForms } from '@singletons/form';
 | 
			
		||||
import { CoreTextUtils } from '@services/utils/text';
 | 
			
		||||
import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
import { ModalController } from '@singletons';
 | 
			
		||||
import {
 | 
			
		||||
    AddonModDataField,
 | 
			
		||||
    AddonModDataData,
 | 
			
		||||
    AddonModDataTemplateType,
 | 
			
		||||
    AddonModDataSearchEntriesAdvancedField,
 | 
			
		||||
} from '../../services/data';
 | 
			
		||||
import { AddonModDataFieldsDelegate } from '../../services/data-fields-delegate';
 | 
			
		||||
import { AddonModDataHelper } from '../../services/data-helper';
 | 
			
		||||
import { AddonModDataComponentsCompileModule } from '../components-compile.module';
 | 
			
		||||
import { AddonModDataSearchDataParams } from '../index';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Page that displays the search modal.
 | 
			
		||||
 */
 | 
			
		||||
@Component({
 | 
			
		||||
    selector: 'addon-mod-data-search-modal',
 | 
			
		||||
    templateUrl: 'search.html',
 | 
			
		||||
    styleUrls: ['../../data.scss', '../../data-forms.scss'],
 | 
			
		||||
})
 | 
			
		||||
export class AddonModDataSearchComponent implements OnInit {
 | 
			
		||||
 | 
			
		||||
    @ViewChild('searchFormEl') formElement!: ElementRef;
 | 
			
		||||
 | 
			
		||||
    @Input() search!: AddonModDataSearchDataParams;
 | 
			
		||||
    @Input() fields!: Record<number, AddonModDataField>;
 | 
			
		||||
    @Input() database!: AddonModDataData;
 | 
			
		||||
 | 
			
		||||
    advancedSearch = '';
 | 
			
		||||
    advancedIndexed: CoreFormFields = {};
 | 
			
		||||
    extraImports: Type<unknown>[]  = [AddonModDataComponentsCompileModule];
 | 
			
		||||
 | 
			
		||||
    searchForm: FormGroup;
 | 
			
		||||
    jsData? : {
 | 
			
		||||
        fields: Record<number, AddonModDataField>;
 | 
			
		||||
        form: FormGroup;
 | 
			
		||||
        search: CoreFormFields;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    fieldsArray: AddonModDataField[] = [];
 | 
			
		||||
 | 
			
		||||
    constructor(
 | 
			
		||||
        protected fb: FormBuilder,
 | 
			
		||||
    ) {
 | 
			
		||||
        this.searchForm = new FormGroup({});
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ngOnInit(): void {
 | 
			
		||||
        this.advancedIndexed = {};
 | 
			
		||||
        this.search.advanced?.forEach((field) => {
 | 
			
		||||
            if (typeof field != 'undefined') {
 | 
			
		||||
                this.advancedIndexed[field.name] = field.value
 | 
			
		||||
                    ? CoreTextUtils.parseJSON(field.value)
 | 
			
		||||
                    : '';
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        this.searchForm.addControl('text', this.fb.control(this.search.text || ''));
 | 
			
		||||
        this.searchForm.addControl('sortBy', this.fb.control(this.search.sortBy || '0'));
 | 
			
		||||
        this.searchForm.addControl('sortDirection', this.fb.control(this.search.sortDirection || 'DESC'));
 | 
			
		||||
        this.searchForm.addControl('firstname', this.fb.control(this.advancedIndexed['firstname'] || ''));
 | 
			
		||||
        this.searchForm.addControl('lastname', this.fb.control(this.advancedIndexed['lastname'] || ''));
 | 
			
		||||
 | 
			
		||||
        this.fieldsArray = CoreUtils.objectToArray(this.fields);
 | 
			
		||||
        this.advancedSearch = this.renderAdvancedSearchFields();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Displays Advanced Search Fields.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Generated HTML.
 | 
			
		||||
     */
 | 
			
		||||
    protected renderAdvancedSearchFields(): string {
 | 
			
		||||
        this.jsData = {
 | 
			
		||||
            fields: this.fields,
 | 
			
		||||
            form: this.searchForm,
 | 
			
		||||
            search: this.advancedIndexed,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        let template = AddonModDataHelper.getTemplate(this.database, AddonModDataTemplateType.SEARCH, this.fieldsArray);
 | 
			
		||||
 | 
			
		||||
        // Replace the fields found on template.
 | 
			
		||||
        this.fieldsArray.forEach((field) => {
 | 
			
		||||
            let replace = '[[' + field.name + ']]';
 | 
			
		||||
            replace = replace.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&');
 | 
			
		||||
            const replaceRegex = new RegExp(replace, 'gi');
 | 
			
		||||
 | 
			
		||||
            // Replace field by a generic directive.
 | 
			
		||||
            const render = '<addon-mod-data-field-plugin mode="search" [field]="fields[' + field.id +
 | 
			
		||||
                ']" [form]="form" [searchFields]="search"></addon-mod-data-field-plugin>';
 | 
			
		||||
            template = template.replace(replaceRegex, render);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Not pluginable other search elements.
 | 
			
		||||
        // Replace firstname field by the text input.
 | 
			
		||||
        let replaceRegex = new RegExp('##firstname##', 'gi');
 | 
			
		||||
        let render = '<span [formGroup]="form"><ion-input type="text" name="firstname" \
 | 
			
		||||
        [placeholder]="\'addon.mod_data.authorfirstname\' | translate" formControlName="firstname"></ion-input></span>';
 | 
			
		||||
        template = template.replace(replaceRegex, render);
 | 
			
		||||
 | 
			
		||||
        // Replace lastname field by the text input.
 | 
			
		||||
        replaceRegex = new RegExp('##lastname##', 'gi');
 | 
			
		||||
        render = '<span [formGroup]="form"><ion-input type="text" name="lastname" \
 | 
			
		||||
        [placeholder]="\'addon.mod_data.authorlastname\' | translate" formControlName="lastname"></ion-input></span>';
 | 
			
		||||
        template = template.replace(replaceRegex, render);
 | 
			
		||||
 | 
			
		||||
        // Searching by tags is not supported.
 | 
			
		||||
        replaceRegex = new RegExp('##tags##', 'gi');
 | 
			
		||||
        const message = CoreTag.areTagsAvailableInSite() ?
 | 
			
		||||
            '<p class="item-dimmed">{{ \'addon.mod_data.searchbytagsnotsupported\' | translate }}</p>'
 | 
			
		||||
            : '';
 | 
			
		||||
        template = template.replace(replaceRegex, message);
 | 
			
		||||
 | 
			
		||||
        return template;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Retrieve the entered data in search in a form.
 | 
			
		||||
     *
 | 
			
		||||
     * @param searchedData Array with the entered form values.
 | 
			
		||||
     * @return Array with the answers.
 | 
			
		||||
     */
 | 
			
		||||
    getSearchDataFromForm(searchedData: CoreFormFields): AddonModDataSearchEntriesAdvancedField[] {
 | 
			
		||||
        const advancedSearch: AddonModDataSearchEntriesAdvancedField[] = [];
 | 
			
		||||
 | 
			
		||||
        // Filter and translate fields to each field plugin.
 | 
			
		||||
        this.fieldsArray.forEach((field) => {
 | 
			
		||||
            const fieldData = AddonModDataFieldsDelegate.getFieldSearchData(field, searchedData);
 | 
			
		||||
 | 
			
		||||
            fieldData.forEach((data) => {
 | 
			
		||||
                // WS wants values in Json format.
 | 
			
		||||
                advancedSearch.push({
 | 
			
		||||
                    name: data.name,
 | 
			
		||||
                    value: JSON.stringify(data.value),
 | 
			
		||||
                });
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Not pluginable other search elements.
 | 
			
		||||
        if (searchedData.firstname) {
 | 
			
		||||
            // WS wants values in Json format.
 | 
			
		||||
            advancedSearch.push({
 | 
			
		||||
                name: 'firstname',
 | 
			
		||||
                value: JSON.stringify(searchedData.firstname),
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (searchedData.lastname) {
 | 
			
		||||
            // WS wants values in Json format.
 | 
			
		||||
            advancedSearch.push({
 | 
			
		||||
                name: 'lastname',
 | 
			
		||||
                value: JSON.stringify(searchedData.lastname),
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return advancedSearch;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Close modal.
 | 
			
		||||
     */
 | 
			
		||||
    closeModal(): void {
 | 
			
		||||
        CoreForms.triggerFormCancelledEvent(this.formElement, CoreSites.getCurrentSiteId());
 | 
			
		||||
 | 
			
		||||
        ModalController.dismiss();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Done editing.
 | 
			
		||||
     *
 | 
			
		||||
     * @param e Event.
 | 
			
		||||
     */
 | 
			
		||||
    searchEntries(e: Event): void {
 | 
			
		||||
        e.preventDefault();
 | 
			
		||||
        e.stopPropagation();
 | 
			
		||||
 | 
			
		||||
        const searchedData = this.searchForm.value;
 | 
			
		||||
 | 
			
		||||
        if (this.search.searchingAdvanced) {
 | 
			
		||||
            this.search.advanced = this.getSearchDataFromForm(searchedData);
 | 
			
		||||
            this.search.searching = this.search.advanced.length > 0;
 | 
			
		||||
        } else {
 | 
			
		||||
            this.search.text = searchedData.text;
 | 
			
		||||
            this.search.searching = this.search.text.length > 0;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.search.sortBy = searchedData.sortBy;
 | 
			
		||||
        this.search.sortDirection = searchedData.sortDirection;
 | 
			
		||||
 | 
			
		||||
        CoreForms.triggerFormSubmittedEvent(this.formElement, false, CoreSites.getCurrentSiteId());
 | 
			
		||||
 | 
			
		||||
        ModalController.dismiss(this.search);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										106
									
								
								src/addons/mod/data/data-forms.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								src/addons/mod/data/data-forms.scss
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,106 @@
 | 
			
		||||
@import "~theme/globals";
 | 
			
		||||
 | 
			
		||||
// Edit and search modal.
 | 
			
		||||
:host {
 | 
			
		||||
    --input-border-color: var(--gray);
 | 
			
		||||
    --input-border-width: 1px;
 | 
			
		||||
    --select-border-width: 0;
 | 
			
		||||
 | 
			
		||||
    ::ng-deep {
 | 
			
		||||
        table {
 | 
			
		||||
            width: 100%;
 | 
			
		||||
        }
 | 
			
		||||
        td {
 | 
			
		||||
            vertical-align: top;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .addon-data-latlong {
 | 
			
		||||
            display: flex;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .addon-data-advanced-search {
 | 
			
		||||
        padding: 16px;
 | 
			
		||||
        width: 100%;
 | 
			
		||||
        // @todo check if needed
 | 
			
		||||
        // @include safe-area-padding-horizontal(16px !important, 16px !important);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .addon-data-contents form,
 | 
			
		||||
    form .addon-data-advanced-search {
 | 
			
		||||
        background-color: var(--ion-item-background);
 | 
			
		||||
 | 
			
		||||
        ::ng-deep {
 | 
			
		||||
 | 
			
		||||
            ion-input {
 | 
			
		||||
                border-bottom: var(--input-border-width) solid var(--input-border-color);
 | 
			
		||||
                &.has-focus,
 | 
			
		||||
                &.has-focus.ion-valid,
 | 
			
		||||
                &.ion-touched.ion-invalid {
 | 
			
		||||
                    --input-border-width: 2px;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                &.has-focus {
 | 
			
		||||
                    --input-border-color: var(--core-color);
 | 
			
		||||
                }
 | 
			
		||||
                &.has-focus.ion-valid {
 | 
			
		||||
                    --input-border-color: var(--success);
 | 
			
		||||
                }
 | 
			
		||||
                &.ion-touched.ion-invalid {
 | 
			
		||||
                    --input-border-color: var(--danger);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            core-rich-text-editor {
 | 
			
		||||
                border-bottom: var(--select-border-width) solid var(--input-border-color);
 | 
			
		||||
 | 
			
		||||
                &.ion-touched.ng-valid,
 | 
			
		||||
                &.ion-touched.ng-invalid {
 | 
			
		||||
                    --select-border-width: 2px;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                &.ion-touched.ng-valid {
 | 
			
		||||
                    --input-border-color: var(--success);
 | 
			
		||||
                }
 | 
			
		||||
                &.ion-touched.ng-invalid {
 | 
			
		||||
                    --input-border-color: var(--danger);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            ion-select {
 | 
			
		||||
                border-bottom: var(--select-border-width) solid var(--input-border-color);
 | 
			
		||||
 | 
			
		||||
                &.ion-touched.ion-valid,
 | 
			
		||||
                &.ion-touched.ion-invalid {
 | 
			
		||||
                    --select-border-width: 2px;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                &.ion-touched.ion-valid {
 | 
			
		||||
                    --input-border-color: var(--success);
 | 
			
		||||
                }
 | 
			
		||||
                &.ion-touched.ion-invalid {
 | 
			
		||||
                    --input-border-color: var(--danger);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            .has-errors ion-input.ion-invalid {
 | 
			
		||||
                --input-border-width: 2px;
 | 
			
		||||
                --input-border-color: var(--danger);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            .has-errors ion-select.ion-invalid,
 | 
			
		||||
            .has-errors core-rich-text-editor.ng-invalid {
 | 
			
		||||
                --select-border-width: 2px;
 | 
			
		||||
                --input-border-color: var(--danger);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            .core-mark-required {
 | 
			
		||||
                @include float(end);
 | 
			
		||||
 | 
			
		||||
                + ion-input,
 | 
			
		||||
                + ion-select {
 | 
			
		||||
                    @include padding(null, 20px, null, null);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										65
									
								
								src/addons/mod/data/data-lazy.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								src/addons/mod/data/data-lazy.module.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,65 @@
 | 
			
		||||
// (C) Copyright 2015 Moodle Pty Ltd.
 | 
			
		||||
//
 | 
			
		||||
// Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
// you may not use this file except in compliance with the License.
 | 
			
		||||
// You may obtain a copy of the License at
 | 
			
		||||
//
 | 
			
		||||
//     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
//
 | 
			
		||||
// Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
// distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { CoreSharedModule } from '@/core/shared.module';
 | 
			
		||||
import { NgModule } from '@angular/core';
 | 
			
		||||
import { RouterModule, Routes } from '@angular/router';
 | 
			
		||||
import { CoreCommentsComponentsModule } from '@features/comments/components/components.module';
 | 
			
		||||
import { CoreCompileHtmlComponentModule } from '@features/compile/components/compile-html/compile-html.module';
 | 
			
		||||
import { CoreRatingComponentsModule } from '@features/rating/components/components.module';
 | 
			
		||||
import { CanLeaveGuard } from '@guards/can-leave';
 | 
			
		||||
import { AddonModDataComponentsCompileModule } from './components/components-compile.module';
 | 
			
		||||
import { AddonModDataComponentsModule } from './components/components.module';
 | 
			
		||||
import { AddonModDataEditPage } from './pages/edit/edit';
 | 
			
		||||
import { AddonModDataEntryPage } from './pages/entry/entry';
 | 
			
		||||
import { AddonModDataIndexPage } from './pages/index/index';
 | 
			
		||||
 | 
			
		||||
const routes: Routes = [
 | 
			
		||||
    {
 | 
			
		||||
        path: ':courseId/:cmId',
 | 
			
		||||
        component: AddonModDataIndexPage,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        path: ':courseId/:cmId/edit',
 | 
			
		||||
        component: AddonModDataEditPage,
 | 
			
		||||
        canDeactivate: [CanLeaveGuard],
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        path: ':courseId/:cmId/edit/:entryId',
 | 
			
		||||
        component: AddonModDataEditPage,
 | 
			
		||||
        canDeactivate: [CanLeaveGuard],
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        path: ':courseId/:cmId/:entryId',
 | 
			
		||||
        component: AddonModDataEntryPage,
 | 
			
		||||
    },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
    imports: [
 | 
			
		||||
        RouterModule.forChild(routes),
 | 
			
		||||
        CoreSharedModule,
 | 
			
		||||
        AddonModDataComponentsModule,
 | 
			
		||||
        AddonModDataComponentsCompileModule,
 | 
			
		||||
        CoreCommentsComponentsModule,
 | 
			
		||||
        CoreRatingComponentsModule,
 | 
			
		||||
        CoreCompileHtmlComponentModule,
 | 
			
		||||
    ],
 | 
			
		||||
    declarations: [
 | 
			
		||||
        AddonModDataIndexPage,
 | 
			
		||||
        AddonModDataEntryPage,
 | 
			
		||||
        AddonModDataEditPage,
 | 
			
		||||
    ],
 | 
			
		||||
})
 | 
			
		||||
export class AddonModDataLazyModule {}
 | 
			
		||||
							
								
								
									
										88
									
								
								src/addons/mod/data/data.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								src/addons/mod/data/data.module.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,88 @@
 | 
			
		||||
// (C) Copyright 2015 Moodle Pty Ltd.
 | 
			
		||||
//
 | 
			
		||||
// Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
// you may not use this file except in compliance with the License.
 | 
			
		||||
// You may obtain a copy of the License at
 | 
			
		||||
//
 | 
			
		||||
//     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
//
 | 
			
		||||
// Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
// distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { APP_INITIALIZER, NgModule, Type } from '@angular/core';
 | 
			
		||||
import { Routes } from '@angular/router';
 | 
			
		||||
import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate';
 | 
			
		||||
import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
 | 
			
		||||
import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate';
 | 
			
		||||
import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module';
 | 
			
		||||
import { CoreTagAreaDelegate } from '@features/tag/services/tag-area-delegate';
 | 
			
		||||
import { CoreCronDelegate } from '@services/cron';
 | 
			
		||||
import { CORE_SITE_SCHEMAS } from '@services/sites';
 | 
			
		||||
import { AddonModDataProvider } from './services/data';
 | 
			
		||||
import { AddonModDataFieldsDelegateService } from './services/data-fields-delegate';
 | 
			
		||||
import { AddonModDataHelperProvider } from './services/data-helper';
 | 
			
		||||
import { AddonModDataOfflineProvider } from './services/data-offline';
 | 
			
		||||
import { AddonModDataSyncProvider } from './services/data-sync';
 | 
			
		||||
import { ADDON_MOD_DATA_OFFLINE_SITE_SCHEMA } from './services/database/data';
 | 
			
		||||
import { AddonModDataApproveLinkHandler } from './services/handlers/approve-link';
 | 
			
		||||
import { AddonModDataDeleteLinkHandler } from './services/handlers/delete-link';
 | 
			
		||||
import { AddonModDataEditLinkHandler } from './services/handlers/edit-link';
 | 
			
		||||
import { AddonModDataIndexLinkHandler } from './services/handlers/index-link';
 | 
			
		||||
import { AddonModDataListLinkHandler } from './services/handlers/list-link';
 | 
			
		||||
import { AddonModDataModuleHandler, AddonModDataModuleHandlerService } from './services/handlers/module';
 | 
			
		||||
import { AddonModDataPrefetchHandler } from './services/handlers/prefetch';
 | 
			
		||||
import { AddonModDataShowLinkHandler } from './services/handlers/show-link';
 | 
			
		||||
import { AddonModDataSyncCronHandler } from './services/handlers/sync-cron';
 | 
			
		||||
import { AddonModDataTagAreaHandler } from './services/handlers/tag-area';
 | 
			
		||||
import { AddonModDataFieldModule } from './fields/field.module';
 | 
			
		||||
 | 
			
		||||
// List of providers (without handlers).
 | 
			
		||||
export const ADDON_MOD_DATA_SERVICES: Type<unknown>[] = [
 | 
			
		||||
    AddonModDataProvider,
 | 
			
		||||
    AddonModDataHelperProvider,
 | 
			
		||||
    AddonModDataSyncProvider,
 | 
			
		||||
    AddonModDataOfflineProvider,
 | 
			
		||||
    AddonModDataFieldsDelegateService,
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const routes: Routes = [
 | 
			
		||||
    {
 | 
			
		||||
        path: AddonModDataModuleHandlerService.PAGE_NAME,
 | 
			
		||||
        loadChildren: () => import('./data-lazy.module').then(m => m.AddonModDataLazyModule),
 | 
			
		||||
    },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
    imports: [
 | 
			
		||||
        CoreMainMenuTabRoutingModule.forChild(routes),
 | 
			
		||||
        AddonModDataFieldModule,
 | 
			
		||||
    ],
 | 
			
		||||
    providers: [
 | 
			
		||||
        {
 | 
			
		||||
            provide: CORE_SITE_SCHEMAS,
 | 
			
		||||
            useValue: [ADDON_MOD_DATA_OFFLINE_SITE_SCHEMA],
 | 
			
		||||
            multi: true,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            provide: APP_INITIALIZER,
 | 
			
		||||
            multi: true,
 | 
			
		||||
            deps: [],
 | 
			
		||||
            useFactory: () => () => {
 | 
			
		||||
                CoreCourseModuleDelegate.registerHandler(AddonModDataModuleHandler.instance);
 | 
			
		||||
                CoreCourseModulePrefetchDelegate.registerHandler(AddonModDataPrefetchHandler.instance);
 | 
			
		||||
                CoreCronDelegate.register(AddonModDataSyncCronHandler.instance);
 | 
			
		||||
                CoreContentLinksDelegate.registerHandler(AddonModDataIndexLinkHandler.instance);
 | 
			
		||||
                CoreContentLinksDelegate.registerHandler(AddonModDataListLinkHandler.instance);
 | 
			
		||||
                CoreContentLinksDelegate.registerHandler(AddonModDataApproveLinkHandler.instance);
 | 
			
		||||
                CoreContentLinksDelegate.registerHandler(AddonModDataDeleteLinkHandler.instance);
 | 
			
		||||
                CoreContentLinksDelegate.registerHandler(AddonModDataShowLinkHandler.instance);
 | 
			
		||||
                CoreContentLinksDelegate.registerHandler(AddonModDataEditLinkHandler.instance);
 | 
			
		||||
                CoreTagAreaDelegate.registerHandler(AddonModDataTagAreaHandler.instance);
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
    ],
 | 
			
		||||
})
 | 
			
		||||
export class AddonModDataModule {}
 | 
			
		||||
							
								
								
									
										70
									
								
								src/addons/mod/data/data.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								src/addons/mod/data/data.scss
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,70 @@
 | 
			
		||||
@import "~theme/globals";
 | 
			
		||||
 | 
			
		||||
/// @prop - The padding for the grid column
 | 
			
		||||
$grid-column-padding:        var(--ion-grid-column-padding, 5px) !default;
 | 
			
		||||
 | 
			
		||||
/// @prop - The padding for the column at different breakpoints
 | 
			
		||||
$grid-column-paddings: (
 | 
			
		||||
  xs: var(--ion-grid-column-padding-xs, $grid-column-padding),
 | 
			
		||||
  sm: var(--ion-grid-column-padding-sm, $grid-column-padding),
 | 
			
		||||
  md: var(--ion-grid-column-padding-md, $grid-column-padding),
 | 
			
		||||
  lg: var(--ion-grid-column-padding-lg, $grid-column-padding),
 | 
			
		||||
  xl: var(--ion-grid-column-padding-xl, $grid-column-padding)
 | 
			
		||||
) !default;
 | 
			
		||||
 | 
			
		||||
.addon-data-contents {
 | 
			
		||||
    overflow: visible;
 | 
			
		||||
    white-space: normal;
 | 
			
		||||
    word-break: break-word;
 | 
			
		||||
    padding: 16px;
 | 
			
		||||
    // @todo check if needed
 | 
			
		||||
    // @include safe-area-padding-horizontal(16px !important, 16px !important);
 | 
			
		||||
 | 
			
		||||
    background-color: var(--ion-item-background);
 | 
			
		||||
    border-width: 1px 0;
 | 
			
		||||
    border-style: solid;
 | 
			
		||||
    border-color: var(--gray-dark);
 | 
			
		||||
 | 
			
		||||
    ::ng-deep {
 | 
			
		||||
        table, tbody {
 | 
			
		||||
            display: block;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        tr {
 | 
			
		||||
            // Imported form ion-row;
 | 
			
		||||
            display: flex;
 | 
			
		||||
            flex-wrap: wrap;
 | 
			
		||||
 | 
			
		||||
            padding: 0;
 | 
			
		||||
            @include media-breakpoint-down(sm) {
 | 
			
		||||
                flex-direction: column;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        td, th {
 | 
			
		||||
            // Imported form ion-col;
 | 
			
		||||
            @include make-breakpoint-padding($grid-column-paddings);
 | 
			
		||||
            @include margin(0);
 | 
			
		||||
            box-sizing: border-box;
 | 
			
		||||
            position: relative;
 | 
			
		||||
            flex-basis: 0;
 | 
			
		||||
            flex-grow: 1;
 | 
			
		||||
            width: 100%;
 | 
			
		||||
            max-width: 100%;
 | 
			
		||||
            min-height: auto;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Do not let block elements to define widths or heights.
 | 
			
		||||
        address, article, aside, blockquote, canvas, dd, div, dl, dt, fieldset, figcaption, figure, footer, form,
 | 
			
		||||
        h1, h2, h3, h4, h5, h6,
 | 
			
		||||
        header, hr, li, main, nav, noscript, ol, p, pre, section, table, tfoot, ul, video {
 | 
			
		||||
            width: auto !important;
 | 
			
		||||
            height: auto !important;
 | 
			
		||||
            min-width: auto !important;
 | 
			
		||||
            min-height: auto !important;
 | 
			
		||||
            // Avoid having one entry over another.
 | 
			
		||||
            max-height: none !important;
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										40
									
								
								src/addons/mod/data/pages/edit/edit.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								src/addons/mod/data/pages/edit/edit.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,40 @@
 | 
			
		||||
<ion-header>
 | 
			
		||||
    <ion-toolbar>
 | 
			
		||||
        <ion-buttons slot="start">
 | 
			
		||||
            <ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
 | 
			
		||||
        </ion-buttons>
 | 
			
		||||
        <ion-title>
 | 
			
		||||
            <core-format-text [text]="title" contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
 | 
			
		||||
            </core-format-text>
 | 
			
		||||
        </ion-title>
 | 
			
		||||
        <ion-buttons slot="end">
 | 
			
		||||
            <ion-button *ngIf="entry" fill="clear" (click)="save($event)" [attr.aria-label]="'core.save' | translate">
 | 
			
		||||
                {{ 'core.save' | translate }}
 | 
			
		||||
            </ion-button>
 | 
			
		||||
        </ion-buttons>
 | 
			
		||||
    </ion-toolbar>
 | 
			
		||||
</ion-header>
 | 
			
		||||
<ion-content>
 | 
			
		||||
    <core-loading [hideUntil]="loaded">
 | 
			
		||||
        <ion-item class="ion-text-wrap" *ngIf="groupInfo && (groupInfo.separateGroups || groupInfo.visibleGroups)">
 | 
			
		||||
            <ion-label id="addon-data-groupslabel">
 | 
			
		||||
                <ng-container *ngIf="groupInfo.separateGroups">{{ 'core.groupsvisible' | translate }}</ng-container>
 | 
			
		||||
                <ng-container *ngIf="groupInfo.visibleGroups">{{ 'core.groupsseparate' | translate }}</ng-container>
 | 
			
		||||
            </ion-label>
 | 
			
		||||
            <ion-select [(ngModel)]="selectedGroup" (ionChange)="setGroup(selectedGroup)" aria-labelledby="addon-data-groupslabel"
 | 
			
		||||
                interface="action-sheet">
 | 
			
		||||
                <ion-select-option *ngFor="let groupOpt of groupInfo.groups" [value]="groupOpt.id">
 | 
			
		||||
                    {{groupOpt.name}}
 | 
			
		||||
                </ion-select-option>
 | 
			
		||||
            </ion-select>
 | 
			
		||||
        </ion-item>
 | 
			
		||||
 | 
			
		||||
        <div class="addon-data-contents {{cssClass}}" *ngIf="database">
 | 
			
		||||
            <core-style [css]="database.csstemplate" prefix=".{{cssClass}}"></core-style>
 | 
			
		||||
 | 
			
		||||
            <form (ngSubmit)="save($event)" [formGroup]="editForm" #editFormEl>
 | 
			
		||||
                <core-compile-html [text]="editFormRender" [jsData]="jsData" [extraImports]="extraImports"></core-compile-html>
 | 
			
		||||
            </form>
 | 
			
		||||
        </div>
 | 
			
		||||
    </core-loading>
 | 
			
		||||
</ion-content>
 | 
			
		||||
							
								
								
									
										451
									
								
								src/addons/mod/data/pages/edit/edit.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										451
									
								
								src/addons/mod/data/pages/edit/edit.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,451 @@
 | 
			
		||||
// (C) Copyright 2015 Moodle Pty Ltd.
 | 
			
		||||
//
 | 
			
		||||
// Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
// you may not use this file except in compliance with the License.
 | 
			
		||||
// You may obtain a copy of the License at
 | 
			
		||||
//
 | 
			
		||||
//     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
//
 | 
			
		||||
// Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
// distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { Component, OnInit, ViewChild, ElementRef, Type } from '@angular/core';
 | 
			
		||||
import { FormGroup } from '@angular/forms';
 | 
			
		||||
import { CoreError } from '@classes/errors/error';
 | 
			
		||||
import { CoreCourseModule } from '@features/course/services/course-helper';
 | 
			
		||||
import { CoreFileUploader } from '@features/fileuploader/services/fileuploader';
 | 
			
		||||
import { CoreTag } from '@features/tag/services/tag';
 | 
			
		||||
import { IonContent } from '@ionic/angular';
 | 
			
		||||
import { CoreGroupInfo, CoreGroups } from '@services/groups';
 | 
			
		||||
import { CoreNavigator } from '@services/navigator';
 | 
			
		||||
import { CoreSites } from '@services/sites';
 | 
			
		||||
import { CoreDomUtils } from '@services/utils/dom';
 | 
			
		||||
import { CoreForms } from '@singletons/form';
 | 
			
		||||
import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
import { Translate } from '@singletons';
 | 
			
		||||
import { CoreEvents } from '@singletons/events';
 | 
			
		||||
import { AddonModDataComponentsCompileModule } from '../../components/components-compile.module';
 | 
			
		||||
import {
 | 
			
		||||
    AddonModDataData,
 | 
			
		||||
    AddonModDataField,
 | 
			
		||||
    AddonModDataProvider,
 | 
			
		||||
    AddonModData,
 | 
			
		||||
    AddonModDataTemplateType,
 | 
			
		||||
    AddonModDataEntry,
 | 
			
		||||
    AddonModDataEntryFields,
 | 
			
		||||
    AddonModDataEditEntryResult,
 | 
			
		||||
    AddonModDataAddEntryResult,
 | 
			
		||||
    AddonModDataEntryWSField,
 | 
			
		||||
} from '../../services/data';
 | 
			
		||||
import { AddonModDataHelper } from '../../services/data-helper';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Page that displays the view edit page.
 | 
			
		||||
 */
 | 
			
		||||
@Component({
 | 
			
		||||
    selector: 'page-addon-mod-data-edit',
 | 
			
		||||
    templateUrl: 'edit.html',
 | 
			
		||||
    styleUrls: ['../../data.scss', '../../data-forms.scss'],
 | 
			
		||||
})
 | 
			
		||||
export class AddonModDataEditPage implements OnInit {
 | 
			
		||||
 | 
			
		||||
    @ViewChild(IonContent) content?: IonContent;
 | 
			
		||||
    @ViewChild('editFormEl') formElement!: ElementRef;
 | 
			
		||||
 | 
			
		||||
    protected entryId?: number;
 | 
			
		||||
    protected fieldsArray: AddonModDataField[] = [];
 | 
			
		||||
    protected siteId: string;
 | 
			
		||||
    protected offline = false;
 | 
			
		||||
    protected forceLeave = false; // To allow leaving the page without checking for changes.
 | 
			
		||||
    protected initialSelectedGroup?: number;
 | 
			
		||||
    protected isEditing = false;
 | 
			
		||||
 | 
			
		||||
    entry?: AddonModDataEntry;
 | 
			
		||||
    fields: Record<number, AddonModDataField> = {};
 | 
			
		||||
    courseId!: number;
 | 
			
		||||
    module!: CoreCourseModule;
 | 
			
		||||
    database?: AddonModDataData;
 | 
			
		||||
    title = '';
 | 
			
		||||
    component = AddonModDataProvider.COMPONENT;
 | 
			
		||||
    loaded = false;
 | 
			
		||||
    selectedGroup = 0;
 | 
			
		||||
    cssClass = '';
 | 
			
		||||
    groupInfo?: CoreGroupInfo;
 | 
			
		||||
    editFormRender = '';
 | 
			
		||||
    editForm: FormGroup;
 | 
			
		||||
    extraImports: Type<unknown>[]  = [AddonModDataComponentsCompileModule];
 | 
			
		||||
    jsData? : {
 | 
			
		||||
        fields: Record<number, AddonModDataField>;
 | 
			
		||||
        database?: AddonModDataData;
 | 
			
		||||
        contents: AddonModDataEntryFields;
 | 
			
		||||
        errors?: Record<number, string>;
 | 
			
		||||
        form: FormGroup;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    errors: Record<number, string> = {};
 | 
			
		||||
 | 
			
		||||
    constructor() {
 | 
			
		||||
        this.siteId = CoreSites.getCurrentSiteId();
 | 
			
		||||
        this.editForm = new FormGroup({});
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    ngOnInit(): void {
 | 
			
		||||
        this.module = CoreNavigator.getRouteParam<CoreCourseModule>('module')!;
 | 
			
		||||
        this.entryId = CoreNavigator.getRouteNumberParam('entryId') || undefined;
 | 
			
		||||
        this.courseId = CoreNavigator.getRouteNumberParam('courseId')!;
 | 
			
		||||
        this.selectedGroup = CoreNavigator.getRouteNumberParam('group') || 0;
 | 
			
		||||
 | 
			
		||||
        // If entryId is lower than 0 or null, it is a new entry or an offline entry.
 | 
			
		||||
        this.isEditing = typeof this.entryId != 'undefined' && this.entryId > 0;
 | 
			
		||||
 | 
			
		||||
        this.title = this.module.name;
 | 
			
		||||
 | 
			
		||||
        this.fetchEntryData(true);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if we can leave the page or not and ask to confirm the lost of data.
 | 
			
		||||
     *
 | 
			
		||||
     * @return True if we can leave, false otherwise.
 | 
			
		||||
     */
 | 
			
		||||
    async canLeave(): Promise<boolean> {
 | 
			
		||||
        if (this.forceLeave || !this.entry) {
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const inputData = this.editForm.value;
 | 
			
		||||
 | 
			
		||||
        let changed = AddonModDataHelper.hasEditDataChanged(inputData, this.fieldsArray, this.entry.contents);
 | 
			
		||||
        changed = changed || (!this.isEditing && this.initialSelectedGroup != this.selectedGroup);
 | 
			
		||||
 | 
			
		||||
        if (changed) {
 | 
			
		||||
            // Show confirmation if some data has been modified.
 | 
			
		||||
            await CoreDomUtils.showConfirm(Translate.instant('coentryre.confirmcanceledit'));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Delete the local files from the tmp folder.
 | 
			
		||||
        const files = await AddonModDataHelper.getEditTmpFiles(inputData, this.fieldsArray, this.entry!.contents);
 | 
			
		||||
        CoreFileUploader.clearTmpFiles(files);
 | 
			
		||||
 | 
			
		||||
        CoreForms.triggerFormCancelledEvent(this.formElement, this.siteId);
 | 
			
		||||
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Fetch the entry data.
 | 
			
		||||
     *
 | 
			
		||||
     * @param refresh To refresh all downloaded data.
 | 
			
		||||
     * @return Resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async fetchEntryData(refresh = false): Promise<void> {
 | 
			
		||||
        try {
 | 
			
		||||
            this.database = await AddonModData.getDatabase(this.courseId, this.module.id);
 | 
			
		||||
            this.title = this.database.name || this.title;
 | 
			
		||||
            this.cssClass = 'addon-data-entries-' + this.database.id;
 | 
			
		||||
 | 
			
		||||
            this.fieldsArray = await AddonModData.getFields(this.database.id, { cmId: this.module.id });
 | 
			
		||||
            this.fields = CoreUtils.arrayToObject(this.fieldsArray, 'id');
 | 
			
		||||
 | 
			
		||||
            const entry = await AddonModDataHelper.fetchEntry(this.database, this.fieldsArray, this.entryId || 0);
 | 
			
		||||
            this.entry = entry.entry;
 | 
			
		||||
 | 
			
		||||
            // Load correct group.
 | 
			
		||||
            this.selectedGroup = this.entry.groupid;
 | 
			
		||||
 | 
			
		||||
            // Check permissions when adding a new entry or offline entry.
 | 
			
		||||
            if (!this.isEditing) {
 | 
			
		||||
                let haveAccess = false;
 | 
			
		||||
 | 
			
		||||
                if (refresh) {
 | 
			
		||||
                    this.groupInfo = await CoreGroups.getActivityGroupInfo(this.database.coursemodule);
 | 
			
		||||
                    this.selectedGroup = CoreGroups.validateGroupId(this.selectedGroup, this.groupInfo);
 | 
			
		||||
                    this.initialSelectedGroup = this.selectedGroup;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (this.groupInfo?.groups && this.groupInfo.groups.length > 0) {
 | 
			
		||||
                    if (refresh) {
 | 
			
		||||
                        const canAddGroup: Record<number, boolean> = {};
 | 
			
		||||
 | 
			
		||||
                        await Promise.all(this.groupInfo.groups.map(async (group) => {
 | 
			
		||||
                            const accessData = await AddonModData.getDatabaseAccessInformation(this.database!.id, {
 | 
			
		||||
                                cmId: this.module.id, groupId: group.id });
 | 
			
		||||
 | 
			
		||||
                            canAddGroup[group.id] = accessData.canaddentry;
 | 
			
		||||
                        }));
 | 
			
		||||
 | 
			
		||||
                        this.groupInfo.groups = this.groupInfo.groups.filter((group) => !!canAddGroup[group.id]);
 | 
			
		||||
 | 
			
		||||
                        haveAccess = canAddGroup[this.selectedGroup];
 | 
			
		||||
                    } else {
 | 
			
		||||
                        // Groups already filtered, so it have access.
 | 
			
		||||
                        haveAccess = true;
 | 
			
		||||
                    }
 | 
			
		||||
                } else {
 | 
			
		||||
                    const accessData = await AddonModData.getDatabaseAccessInformation(this.database.id, { cmId: this.module.id });
 | 
			
		||||
                    haveAccess = accessData.canaddentry;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (!haveAccess) {
 | 
			
		||||
                    // You shall not pass, go back.
 | 
			
		||||
                    CoreDomUtils.showErrorModal('addon.mod_data.noaccess', true);
 | 
			
		||||
 | 
			
		||||
                    // Go back to entry list.
 | 
			
		||||
                    this.forceLeave = true;
 | 
			
		||||
                    CoreNavigator.back();
 | 
			
		||||
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this.editFormRender = this.displayEditFields();
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            CoreDomUtils.showErrorModalDefault(error, 'core.course.errorgetmodule', true);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.loaded = true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Saves data.
 | 
			
		||||
     *
 | 
			
		||||
     * @param e Event.
 | 
			
		||||
     * @return Resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    async save(e: Event): Promise<void> {
 | 
			
		||||
        e.preventDefault();
 | 
			
		||||
        e.stopPropagation();
 | 
			
		||||
 | 
			
		||||
        const inputData = this.editForm.value;
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            let changed = AddonModDataHelper.hasEditDataChanged(
 | 
			
		||||
                inputData,
 | 
			
		||||
                this.fieldsArray,
 | 
			
		||||
                this.entry?.contents || {},
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            changed = changed || (!this.isEditing && this.initialSelectedGroup != this.selectedGroup);
 | 
			
		||||
            if (!changed) {
 | 
			
		||||
                if (this.entryId) {
 | 
			
		||||
                    await this.returnToEntryList();
 | 
			
		||||
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // New entry, no changes means no field filled, warn the user.
 | 
			
		||||
                throw new CoreError(Translate.instant('addon.mod_data.emptyaddform'));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const modal = await CoreDomUtils.showModalLoading('core.sending', true);
 | 
			
		||||
 | 
			
		||||
            // Create an ID to assign files.
 | 
			
		||||
            const entryTemp = this.entryId ? this.entryId : - (new Date().getTime());
 | 
			
		||||
            let editData: AddonModDataEntryWSField[] = [];
 | 
			
		||||
 | 
			
		||||
            try {
 | 
			
		||||
                try {
 | 
			
		||||
                    editData = await AddonModDataHelper.getEditDataFromForm(
 | 
			
		||||
                        inputData,
 | 
			
		||||
                        this.fieldsArray,
 | 
			
		||||
                        this.database!.id,
 | 
			
		||||
                        entryTemp,
 | 
			
		||||
                        this.entry?.contents || {},
 | 
			
		||||
                        this.offline,
 | 
			
		||||
                    );
 | 
			
		||||
                } catch (error) {
 | 
			
		||||
                    if (this.offline) {
 | 
			
		||||
                        throw error;
 | 
			
		||||
                    }
 | 
			
		||||
                    // Cannot submit in online, prepare for offline usage.
 | 
			
		||||
                    this.offline = true;
 | 
			
		||||
 | 
			
		||||
                    editData = await AddonModDataHelper.getEditDataFromForm(
 | 
			
		||||
                        inputData,
 | 
			
		||||
                        this.fieldsArray,
 | 
			
		||||
                        this.database!.id,
 | 
			
		||||
                        entryTemp,
 | 
			
		||||
                        this.entry?.contents || {},
 | 
			
		||||
                        this.offline,
 | 
			
		||||
                    );
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (editData.length <= 0) {
 | 
			
		||||
                    // No field filled, warn the user.
 | 
			
		||||
                    throw new CoreError(Translate.instant('addon.mod_data.emptyaddform'));
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                let updateEntryResult: AddonModDataEditEntryResult | AddonModDataAddEntryResult | undefined;
 | 
			
		||||
                if (this.isEditing) {
 | 
			
		||||
                    updateEntryResult = await AddonModData.editEntry(
 | 
			
		||||
                        this.database!.id,
 | 
			
		||||
                        this.entryId!,
 | 
			
		||||
                        this.courseId,
 | 
			
		||||
                        editData,
 | 
			
		||||
                        this.fieldsArray,
 | 
			
		||||
                        this.siteId,
 | 
			
		||||
                        this.offline,
 | 
			
		||||
                    );
 | 
			
		||||
                } else {
 | 
			
		||||
                    updateEntryResult = await AddonModData.addEntry(
 | 
			
		||||
                        this.database!.id,
 | 
			
		||||
                        entryTemp,
 | 
			
		||||
                        this.courseId,
 | 
			
		||||
                        editData,
 | 
			
		||||
                        this.selectedGroup,
 | 
			
		||||
                        this.fieldsArray,
 | 
			
		||||
                        this.siteId,
 | 
			
		||||
                        this.offline,
 | 
			
		||||
                    );
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // This is done if entry is updated when editing or creating if not.
 | 
			
		||||
                if ((this.isEditing && 'updated' in updateEntryResult && updateEntryResult.updated) ||
 | 
			
		||||
                    (!this.isEditing && 'newentryid' in updateEntryResult && updateEntryResult.newentryid)) {
 | 
			
		||||
 | 
			
		||||
                    CoreForms.triggerFormSubmittedEvent(this.formElement, updateEntryResult.sent, this.siteId);
 | 
			
		||||
 | 
			
		||||
                    const promises: Promise<void>[] = [];
 | 
			
		||||
 | 
			
		||||
                    if (updateEntryResult.sent) {
 | 
			
		||||
                        CoreEvents.trigger(CoreEvents.ACTIVITY_DATA_SENT, { module: 'data' });
 | 
			
		||||
 | 
			
		||||
                        if (this.isEditing) {
 | 
			
		||||
                            promises.push(AddonModData.invalidateEntryData(this.database!.id, this.entryId!, this.siteId));
 | 
			
		||||
                        }
 | 
			
		||||
                        promises.push(AddonModData.invalidateEntriesData(this.database!.id, this.siteId));
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    try {
 | 
			
		||||
                        await Promise.all(promises);
 | 
			
		||||
                        CoreEvents.trigger(
 | 
			
		||||
                            AddonModDataProvider.ENTRY_CHANGED,
 | 
			
		||||
                            { dataId: this.database!.id, entryId: this.entryId },
 | 
			
		||||
 | 
			
		||||
                            this.siteId,
 | 
			
		||||
                        );
 | 
			
		||||
                    } finally {
 | 
			
		||||
                        this.returnToEntryList();
 | 
			
		||||
                    }
 | 
			
		||||
                } else {
 | 
			
		||||
                    this.errors = {};
 | 
			
		||||
                    if (updateEntryResult.fieldnotifications) {
 | 
			
		||||
                        updateEntryResult.fieldnotifications.forEach((fieldNotif) => {
 | 
			
		||||
                            const field = this.fieldsArray.find((field) => field.name == fieldNotif.fieldname);
 | 
			
		||||
                            if (field) {
 | 
			
		||||
                                this.errors[field.id] = fieldNotif.notification;
 | 
			
		||||
                            }
 | 
			
		||||
                        });
 | 
			
		||||
                    }
 | 
			
		||||
                    this.jsData!.errors = this.errors;
 | 
			
		||||
 | 
			
		||||
                    setTimeout(() => {
 | 
			
		||||
                        this.scrollToFirstError();
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
            } finally {
 | 
			
		||||
                modal.dismiss();
 | 
			
		||||
            }
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            CoreDomUtils.showErrorModalDefault(error, 'Cannot edit entry', true);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Set group to see the database.
 | 
			
		||||
     *
 | 
			
		||||
     * @param groupId Group identifier to set.
 | 
			
		||||
     * @return Resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    setGroup(groupId: number): Promise<void> {
 | 
			
		||||
        this.selectedGroup = groupId;
 | 
			
		||||
        this.loaded = false;
 | 
			
		||||
 | 
			
		||||
        return this.fetchEntryData();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Displays Edit Search Fields.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Generated HTML.
 | 
			
		||||
     */
 | 
			
		||||
    protected displayEditFields(): string {
 | 
			
		||||
        this.jsData = {
 | 
			
		||||
            fields: this.fields,
 | 
			
		||||
            contents: CoreUtils.clone(this.entry?.contents) || {},
 | 
			
		||||
            form: this.editForm,
 | 
			
		||||
            database: this.database,
 | 
			
		||||
            errors: this.errors,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        let template = AddonModDataHelper.getTemplate(this.database!, AddonModDataTemplateType.ADD, this.fieldsArray);
 | 
			
		||||
 | 
			
		||||
        // Replace the fields found on template.
 | 
			
		||||
        this.fieldsArray.forEach((field) => {
 | 
			
		||||
            let replace = '[[' + field.name + ']]';
 | 
			
		||||
            replace = replace.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&');
 | 
			
		||||
            let replaceRegEx = new RegExp(replace, 'gi');
 | 
			
		||||
 | 
			
		||||
            // Replace field by a generic directive.
 | 
			
		||||
            const render = '<addon-mod-data-field-plugin [class.has-errors]="!!errors[' + field.id + ']" mode="edit" \
 | 
			
		||||
                [field]="fields[' + field.id + ']" [value]="contents[' + field.id + ']" [form]="form" [database]="database" \
 | 
			
		||||
                [error]="errors[' + field.id + ']"></addon-mod-data-field-plugin>';
 | 
			
		||||
            template = template.replace(replaceRegEx, render);
 | 
			
		||||
 | 
			
		||||
            // Replace the field id tag.
 | 
			
		||||
            replace = '[[' + field.name + '#id]]';
 | 
			
		||||
            replace = replace.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&');
 | 
			
		||||
            replaceRegEx = new RegExp(replace, 'gi');
 | 
			
		||||
 | 
			
		||||
            template = template.replace(replaceRegEx, 'field_' + field.id);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Editing tags is not supported.
 | 
			
		||||
        const replaceRegEx = new RegExp('##tags##', 'gi');
 | 
			
		||||
        const message = CoreTag.areTagsAvailableInSite()
 | 
			
		||||
            ? '<p class="item-dimmed">{{ \'addon.mod_data.edittagsnotsupported\' | translate }}</p>'
 | 
			
		||||
            : '';
 | 
			
		||||
        template = template.replace(replaceRegEx, message);
 | 
			
		||||
 | 
			
		||||
        return template;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Return to the entry list (previous page) discarding temp data.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async returnToEntryList(): Promise<void> {
 | 
			
		||||
        const inputData = this.editForm.value;
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            const files = await AddonModDataHelper.getEditTmpFiles(
 | 
			
		||||
                inputData,
 | 
			
		||||
                this.fieldsArray,
 | 
			
		||||
                this.entry?.contents || {},
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            CoreFileUploader.clearTmpFiles(files);
 | 
			
		||||
        } finally {
 | 
			
		||||
            // Go back to entry list.
 | 
			
		||||
            this.forceLeave = true;
 | 
			
		||||
            CoreNavigator.back();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Scroll to first error or to the top if not found.
 | 
			
		||||
     */
 | 
			
		||||
    protected scrollToFirstError(): void {
 | 
			
		||||
        if (!CoreDomUtils.scrollToElementBySelector(this.formElement.nativeElement, this.content, '.addon-data-error')) {
 | 
			
		||||
            this.content?.scrollToTop();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										82
									
								
								src/addons/mod/data/pages/entry/entry.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								src/addons/mod/data/pages/entry/entry.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,82 @@
 | 
			
		||||
<ion-header>
 | 
			
		||||
    <ion-toolbar>
 | 
			
		||||
        <ion-buttons slot="start">
 | 
			
		||||
            <ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
 | 
			
		||||
        </ion-buttons>
 | 
			
		||||
        <ion-title>
 | 
			
		||||
            <core-format-text [text]="title" contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
 | 
			
		||||
            </core-format-text>
 | 
			
		||||
        </ion-title>
 | 
			
		||||
    </ion-toolbar>
 | 
			
		||||
</ion-header>
 | 
			
		||||
<ion-content>
 | 
			
		||||
    <ion-refresher slot="fixed"
 | 
			
		||||
        [disabled]="!entryLoaded || !(isPullingToRefresh || !renderingEntry && !loadingRating && !loadingComments)"
 | 
			
		||||
        (ionRefresh)="refreshDatabase($event.target)">
 | 
			
		||||
        <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
 | 
			
		||||
    </ion-refresher>
 | 
			
		||||
    <core-loading [hideUntil]="entryLoaded && (isPullingToRefresh || !renderingEntry && !loadingRating && !loadingComments)">
 | 
			
		||||
        <!-- Database entries found to be synchronized -->
 | 
			
		||||
        <ion-card class="core-warning-card" *ngIf="entry && entry.hasOffline">
 | 
			
		||||
            <ion-item>
 | 
			
		||||
                <ion-icon name="fas-exclamation-triangle" slot="start"></ion-icon>
 | 
			
		||||
                <ion-label>{{ 'core.hasdatatosync' | translate: {$a: moduleName} }}</ion-label>
 | 
			
		||||
            </ion-item>
 | 
			
		||||
        </ion-card>
 | 
			
		||||
 | 
			
		||||
        <ion-item class="ion-text-wrap" *ngIf="groupInfo && (groupInfo.separateGroups || groupInfo.visibleGroups)">
 | 
			
		||||
            <ion-label id="addon-data-groupslabel">
 | 
			
		||||
                <ng-container *ngIf="groupInfo.separateGroups">{{ 'core.groupsvisible' | translate }}</ng-container>
 | 
			
		||||
                <ng-container *ngIf="groupInfo.visibleGroups">{{ 'core.groupsseparate' | translate }}</ng-container>
 | 
			
		||||
            </ion-label>
 | 
			
		||||
            <ion-select [(ngModel)]="selectedGroup" (ionChange)="setGroup(selectedGroup)" aria-labelledby="addon-data-groupslabel"
 | 
			
		||||
                interface="action-sheet">
 | 
			
		||||
                <ion-select-option *ngFor="let groupOpt of groupInfo.groups" [value]="groupOpt.id">
 | 
			
		||||
                    {{groupOpt.name}}
 | 
			
		||||
                </ion-select-option>
 | 
			
		||||
            </ion-select>
 | 
			
		||||
        </ion-item>
 | 
			
		||||
 | 
			
		||||
        <div class="addon-data-contents addon-data-entries-{{database.id}}" *ngIf="database && entry">
 | 
			
		||||
            <core-style [css]="database.csstemplate" prefix=".addon-data-entries-{{database.id}}"></core-style>
 | 
			
		||||
 | 
			
		||||
            <core-compile-html [text]="entryHtml" [jsData]="jsData" [extraImports]="extraImports"
 | 
			
		||||
                (compiling)="setRenderingEntry($event)"></core-compile-html>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <core-rating-rate *ngIf="database && entry && ratingInfo && (!database.approval || entry.approved)"
 | 
			
		||||
            [ratingInfo]="ratingInfo" contextLevel="module" [instanceId]="database.coursemodule" [itemId]="entry.id" [itemSetId]="0"
 | 
			
		||||
            [courseId]="courseId" [aggregateMethod]="database.assessed" [scaleId]="database.scale" [userId]="entry.userid"
 | 
			
		||||
            (onLoading)="setLoadingRating($event)" (onUpdate)="ratingUpdated()">
 | 
			
		||||
        </core-rating-rate>
 | 
			
		||||
        <core-rating-aggregate *ngIf="database && entry && ratingInfo" [ratingInfo]="ratingInfo" contextLevel="module"
 | 
			
		||||
            [instanceId]="database.coursemodule" [itemId]="entry.id" [courseId]="courseId" [aggregateMethod]="database.assessed"
 | 
			
		||||
            [scaleId]="database.scale">
 | 
			
		||||
        </core-rating-aggregate>
 | 
			
		||||
 | 
			
		||||
        <ion-item *ngIf="database && database.comments && entry && entry.id > 0 && commentsEnabled">
 | 
			
		||||
            <ion-label>
 | 
			
		||||
                <core-comments contextLevel="module" [instanceId]="database.coursemodule" component="mod_data" [itemId]="entry.id"
 | 
			
		||||
                    area="database_entry" [displaySpinner]="false" [courseId]="courseId" (onLoading)="setLoadingComments($event)">
 | 
			
		||||
                </core-comments>
 | 
			
		||||
            </ion-label>
 | 
			
		||||
        </ion-item>
 | 
			
		||||
 | 
			
		||||
        <ion-grid *ngIf="hasPrevious || hasNext">
 | 
			
		||||
            <ion-row class="ion-align-items-center">
 | 
			
		||||
                <ion-col *ngIf="hasPrevious">
 | 
			
		||||
                    <ion-button expand="block" fill="outline" (click)="gotoEntry(offset! -1)">
 | 
			
		||||
                        <ion-icon name="fas-chevron-left" slot="start"></ion-icon>
 | 
			
		||||
                        {{ 'core.previous' | translate }}
 | 
			
		||||
                    </ion-button>
 | 
			
		||||
                </ion-col>
 | 
			
		||||
                <ion-col *ngIf="hasNext">
 | 
			
		||||
                    <ion-button expand="block" (click)="gotoEntry(offset! + 1)">
 | 
			
		||||
                        {{ 'core.next' | translate }}
 | 
			
		||||
                        <ion-icon name="fas-chevron-right" slot="end"></ion-icon>
 | 
			
		||||
                    </ion-button>
 | 
			
		||||
                </ion-col>
 | 
			
		||||
            </ion-row>
 | 
			
		||||
        </ion-grid>
 | 
			
		||||
    </core-loading>
 | 
			
		||||
</ion-content>
 | 
			
		||||
							
								
								
									
										414
									
								
								src/addons/mod/data/pages/entry/entry.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										414
									
								
								src/addons/mod/data/pages/entry/entry.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,414 @@
 | 
			
		||||
// (C) Copyright 2015 Moodle Pty Ltd.
 | 
			
		||||
//
 | 
			
		||||
// Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
// you may not use this file except in compliance with the License.
 | 
			
		||||
// You may obtain a copy of the License at
 | 
			
		||||
//
 | 
			
		||||
//     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
//
 | 
			
		||||
// Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
// distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { Component, OnDestroy, ViewChild, ChangeDetectorRef, OnInit, Type } from '@angular/core';
 | 
			
		||||
import { CoreCommentsCommentsComponent } from '@features/comments/components/comments/comments';
 | 
			
		||||
import { CoreComments } from '@features/comments/services/comments';
 | 
			
		||||
import { CoreCourse } from '@features/course/services/course';
 | 
			
		||||
import { CoreCourseModule } from '@features/course/services/course-helper';
 | 
			
		||||
import { CoreRatingInfo } from '@features/rating/services/rating';
 | 
			
		||||
import { IonContent, IonRefresher } from '@ionic/angular';
 | 
			
		||||
import { CoreGroups, CoreGroupInfo } from '@services/groups';
 | 
			
		||||
import { CoreNavigator } from '@services/navigator';
 | 
			
		||||
import { CoreSites } from '@services/sites';
 | 
			
		||||
import { CoreDomUtils } from '@services/utils/dom';
 | 
			
		||||
import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
 | 
			
		||||
import { AddonModDataComponentsCompileModule } from '../../components/components-compile.module';
 | 
			
		||||
import { AddonModDataProvider,
 | 
			
		||||
    AddonModData,
 | 
			
		||||
    AddonModDataData,
 | 
			
		||||
    AddonModDataGetDataAccessInformationWSResponse,
 | 
			
		||||
    AddonModDataField,
 | 
			
		||||
    AddonModDataTemplateType,
 | 
			
		||||
    AddonModDataTemplateMode,
 | 
			
		||||
    AddonModDataEntry,
 | 
			
		||||
} from '../../services/data';
 | 
			
		||||
import { AddonModDataHelper } from '../../services/data-helper';
 | 
			
		||||
import { AddonModDataSyncProvider } from '../../services/data-sync';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Page that displays the view entry page.
 | 
			
		||||
 */
 | 
			
		||||
@Component({
 | 
			
		||||
    selector: 'page-addon-mod-data-entry',
 | 
			
		||||
    templateUrl: 'entry.html',
 | 
			
		||||
    styleUrls: ['../../data.scss'],
 | 
			
		||||
})
 | 
			
		||||
export class AddonModDataEntryPage implements OnInit, OnDestroy {
 | 
			
		||||
 | 
			
		||||
    @ViewChild(IonContent) content?: IonContent;
 | 
			
		||||
    @ViewChild(CoreCommentsCommentsComponent) comments?: CoreCommentsCommentsComponent;
 | 
			
		||||
 | 
			
		||||
    protected entryId?: number;
 | 
			
		||||
    protected syncObserver: CoreEventObserver; // It will observe the sync auto event.
 | 
			
		||||
    protected entryChangedObserver: CoreEventObserver; // It will observe the changed entry event.
 | 
			
		||||
    protected fields: Record<number, AddonModDataField> = {};
 | 
			
		||||
    protected fieldsArray: AddonModDataField[] = [];
 | 
			
		||||
 | 
			
		||||
    module!: CoreCourseModule;
 | 
			
		||||
    courseId!: number;
 | 
			
		||||
    offset?: number;
 | 
			
		||||
    title = '';
 | 
			
		||||
    moduleName = 'data';
 | 
			
		||||
    component = AddonModDataProvider.COMPONENT;
 | 
			
		||||
    entryLoaded = false;
 | 
			
		||||
    renderingEntry = false;
 | 
			
		||||
    loadingComments = false;
 | 
			
		||||
    loadingRating = false;
 | 
			
		||||
    selectedGroup = 0;
 | 
			
		||||
    entry?: AddonModDataEntry;
 | 
			
		||||
    hasPrevious = false;
 | 
			
		||||
    hasNext = false;
 | 
			
		||||
    access?: AddonModDataGetDataAccessInformationWSResponse;
 | 
			
		||||
    database?: AddonModDataData;
 | 
			
		||||
    groupInfo?: CoreGroupInfo;
 | 
			
		||||
    showComments = false;
 | 
			
		||||
    entryHtml = '';
 | 
			
		||||
    siteId: string;
 | 
			
		||||
    extraImports: Type<unknown>[]  = [AddonModDataComponentsCompileModule];
 | 
			
		||||
    jsData? : {
 | 
			
		||||
        fields: Record<number, AddonModDataField>;
 | 
			
		||||
        entries: Record<number, AddonModDataEntry>;
 | 
			
		||||
        database: AddonModDataData;
 | 
			
		||||
        module: CoreCourseModule;
 | 
			
		||||
        group: number;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    ratingInfo?: CoreRatingInfo;
 | 
			
		||||
    isPullingToRefresh = false; // Whether the last fetching of data was started by a pull-to-refresh action
 | 
			
		||||
    commentsEnabled = false;
 | 
			
		||||
 | 
			
		||||
    constructor(
 | 
			
		||||
        private cdr: ChangeDetectorRef,
 | 
			
		||||
    ) {
 | 
			
		||||
        this.moduleName = CoreCourse.translateModuleName('data');
 | 
			
		||||
        this.siteId = CoreSites.getCurrentSiteId();
 | 
			
		||||
 | 
			
		||||
        // Refresh data if this discussion is synchronized automatically.
 | 
			
		||||
        this.syncObserver = CoreEvents.on(AddonModDataSyncProvider.AUTO_SYNCED, (data) => {
 | 
			
		||||
            if (typeof data.entryId == 'undefined') {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if ((data.entryId == this.entryId || data.offlineEntryId == this.entryId) && this.database?.id == data.dataId) {
 | 
			
		||||
                if (data.deleted) {
 | 
			
		||||
                    // If deleted, go back.
 | 
			
		||||
                    CoreNavigator.back();
 | 
			
		||||
                } else {
 | 
			
		||||
                    this.entryId = data.entryId;
 | 
			
		||||
                    this.entryLoaded = false;
 | 
			
		||||
                    this.fetchEntryData(true);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }, this.siteId);
 | 
			
		||||
 | 
			
		||||
        // Refresh entry on change.
 | 
			
		||||
        this.entryChangedObserver = CoreEvents.on(AddonModDataProvider.ENTRY_CHANGED, (data) => {
 | 
			
		||||
            if (data.entryId == this.entryId && this.database?.id == data.dataId) {
 | 
			
		||||
                if (data.deleted) {
 | 
			
		||||
                    // If deleted, go back.
 | 
			
		||||
                    CoreNavigator.back();
 | 
			
		||||
                } else {
 | 
			
		||||
                    this.entryLoaded = false;
 | 
			
		||||
                    this.fetchEntryData(true);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }, this.siteId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    async ngOnInit(): Promise<void> {
 | 
			
		||||
        this.module = CoreNavigator.getRouteParam<CoreCourseModule>('module')!;
 | 
			
		||||
        this.entryId = CoreNavigator.getRouteNumberParam('entryId') || undefined;
 | 
			
		||||
        this.courseId = CoreNavigator.getRouteNumberParam('courseId')!;
 | 
			
		||||
        this.selectedGroup = CoreNavigator.getRouteNumberParam('group') || 0;
 | 
			
		||||
        this.offset = CoreNavigator.getRouteNumberParam('offset');
 | 
			
		||||
        this.title = this.module.name;
 | 
			
		||||
 | 
			
		||||
        this.commentsEnabled = !CoreComments.areCommentsDisabledInSite();
 | 
			
		||||
 | 
			
		||||
        await this.fetchEntryData();
 | 
			
		||||
        this.logView();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Fetch the entry data.
 | 
			
		||||
     *
 | 
			
		||||
     * @param refresh Whether to refresh the current data or not.
 | 
			
		||||
     * @param isPtr Whether is a pull to refresh action.
 | 
			
		||||
     * @return Resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async fetchEntryData(refresh = false, isPtr = false): Promise<void> {
 | 
			
		||||
        this.isPullingToRefresh = isPtr;
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            this.database = await AddonModData.getDatabase(this.courseId, this.module.id);
 | 
			
		||||
            this.title = this.database.name || this.title;
 | 
			
		||||
 | 
			
		||||
            this.fieldsArray = await AddonModData.getFields(this.database.id, { cmId: this.module.id });
 | 
			
		||||
            this.fields = CoreUtils.arrayToObject(this.fieldsArray, 'id');
 | 
			
		||||
 | 
			
		||||
            await this.setEntryFromOffset();
 | 
			
		||||
 | 
			
		||||
            this.access = await AddonModData.getDatabaseAccessInformation(this.database.id, { cmId: this.module.id });
 | 
			
		||||
 | 
			
		||||
            this.groupInfo = await CoreGroups.getActivityGroupInfo(this.database.coursemodule);
 | 
			
		||||
            this.selectedGroup = CoreGroups.validateGroupId(this.selectedGroup, this.groupInfo);
 | 
			
		||||
 | 
			
		||||
            const actions = AddonModDataHelper.getActions(this.database, this.access, this.entry!);
 | 
			
		||||
 | 
			
		||||
            const template = AddonModDataHelper.getTemplate(this.database, AddonModDataTemplateType.SINGLE, this.fieldsArray);
 | 
			
		||||
            this.entryHtml = AddonModDataHelper.displayShowFields(
 | 
			
		||||
                template,
 | 
			
		||||
                this.fieldsArray,
 | 
			
		||||
                this.entry!,
 | 
			
		||||
                this.offset,
 | 
			
		||||
                AddonModDataTemplateMode.SHOW,
 | 
			
		||||
                actions,
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            this.showComments = actions.comments;
 | 
			
		||||
 | 
			
		||||
            const entries: Record<number, AddonModDataEntry> = {};
 | 
			
		||||
            entries[this.entryId!] = this.entry!;
 | 
			
		||||
 | 
			
		||||
            // Pass the input data to the component.
 | 
			
		||||
            this.jsData = {
 | 
			
		||||
                fields: this.fields,
 | 
			
		||||
                entries: entries,
 | 
			
		||||
                database: this.database,
 | 
			
		||||
                module: this.module,
 | 
			
		||||
                group: this.selectedGroup,
 | 
			
		||||
            };
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            if (!refresh) {
 | 
			
		||||
                // Some call failed, retry without using cache since it might be a new activity.
 | 
			
		||||
                return this.refreshAllData(isPtr);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            CoreDomUtils.showErrorModalDefault(error, 'core.course.errorgetmodule', true);
 | 
			
		||||
        } finally {
 | 
			
		||||
            this.content?.scrollToTop();
 | 
			
		||||
            this.entryLoaded = true;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Go to selected entry without changing state.
 | 
			
		||||
     *
 | 
			
		||||
     * @param offset Entry offset.
 | 
			
		||||
     * @return Resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    async gotoEntry(offset: number): Promise<void> {
 | 
			
		||||
        this.offset = offset;
 | 
			
		||||
        this.entryId = undefined;
 | 
			
		||||
        this.entry = undefined;
 | 
			
		||||
        this.entryLoaded = false;
 | 
			
		||||
 | 
			
		||||
        await this.fetchEntryData();
 | 
			
		||||
        this.logView();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Refresh all the data.
 | 
			
		||||
     *
 | 
			
		||||
     * @param isPtr Whether is a pull to refresh action.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async refreshAllData(isPtr?: boolean): Promise<void> {
 | 
			
		||||
        const promises: Promise<void>[] = [];
 | 
			
		||||
 | 
			
		||||
        promises.push(AddonModData.invalidateDatabaseData(this.courseId));
 | 
			
		||||
        if (this.database) {
 | 
			
		||||
            promises.push(AddonModData.invalidateEntryData(this.database.id, this.entryId!));
 | 
			
		||||
            promises.push(CoreGroups.invalidateActivityGroupInfo(this.database.coursemodule));
 | 
			
		||||
            promises.push(AddonModData.invalidateEntriesData(this.database.id));
 | 
			
		||||
            promises.push(AddonModData.invalidateFieldsData(this.database.id));
 | 
			
		||||
 | 
			
		||||
            if (this.database.comments && this.entry && this.entry.id > 0 && this.commentsEnabled && this.comments) {
 | 
			
		||||
                // Refresh comments. Don't add it to promises because we don't want the comments fetch to block the entry fetch.
 | 
			
		||||
                this.comments.doRefresh().catch(() => {
 | 
			
		||||
                    // Ignore errors.
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await Promise.all(promises).finally(() =>
 | 
			
		||||
            this.fetchEntryData(true, isPtr));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Refresh the data.
 | 
			
		||||
     *
 | 
			
		||||
     * @param refresher Refresher.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    refreshDatabase(refresher?: IonRefresher): void {
 | 
			
		||||
        if (!this.entryLoaded) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.refreshAllData(true).finally(() => {
 | 
			
		||||
            refresher?.complete();
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Set group to see the database.
 | 
			
		||||
     *
 | 
			
		||||
     * @param groupId Group identifier to set.
 | 
			
		||||
     * @return Resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    async setGroup(groupId: number): Promise<void> {
 | 
			
		||||
        this.selectedGroup = groupId;
 | 
			
		||||
        this.offset = undefined;
 | 
			
		||||
        this.entry = undefined;
 | 
			
		||||
        this.entryId = undefined;
 | 
			
		||||
        this.entryLoaded = false;
 | 
			
		||||
 | 
			
		||||
        await this.fetchEntryData();
 | 
			
		||||
        this.logView();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Convenience function to fetch the entry and set next/previous entries.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async setEntryFromOffset(): Promise<void> {
 | 
			
		||||
        if (typeof this.offset == 'undefined' && typeof this.entryId != 'undefined') {
 | 
			
		||||
            // Entry id passed as navigation parameter instead of the offset.
 | 
			
		||||
            // We don't display next/previous buttons in this case.
 | 
			
		||||
            this.hasNext = false;
 | 
			
		||||
            this.hasPrevious = false;
 | 
			
		||||
 | 
			
		||||
            const entry = await AddonModDataHelper.fetchEntry(this.database!, this.fieldsArray, this.entryId);
 | 
			
		||||
            this.entry = entry.entry;
 | 
			
		||||
            this.ratingInfo = entry.ratinginfo;
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const perPage = AddonModDataProvider.PER_PAGE;
 | 
			
		||||
        const page = typeof this.offset != 'undefined' && this.offset >= 0
 | 
			
		||||
            ? Math.floor(this.offset / perPage)
 | 
			
		||||
            : 0;
 | 
			
		||||
 | 
			
		||||
        const entries = await AddonModDataHelper.fetchEntries(this.database!, this.fieldsArray, {
 | 
			
		||||
            groupId: this.selectedGroup,
 | 
			
		||||
            sort: 0,
 | 
			
		||||
            order: 'DESC',
 | 
			
		||||
            page,
 | 
			
		||||
            perPage,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        const pageEntries = (entries.offlineEntries || []).concat(entries.entries);
 | 
			
		||||
 | 
			
		||||
        // Index of the entry when concatenating offline and online page entries.
 | 
			
		||||
        let pageIndex = 0;
 | 
			
		||||
        if (typeof this.offset == 'undefined') {
 | 
			
		||||
            // No offset passed, display the first entry.
 | 
			
		||||
            pageIndex = 0;
 | 
			
		||||
        } else if (this.offset > 0) {
 | 
			
		||||
            // Online entry.
 | 
			
		||||
            pageIndex = this.offset % perPage + (entries.offlineEntries?.length || 0);
 | 
			
		||||
        } else {
 | 
			
		||||
            // Offline entry.
 | 
			
		||||
            pageIndex = this.offset + (entries.offlineEntries?.length || 0);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.entry = pageEntries[pageIndex];
 | 
			
		||||
        this.entryId = this.entry.id;
 | 
			
		||||
 | 
			
		||||
        this.hasPrevious = page > 0 || pageIndex > 0;
 | 
			
		||||
 | 
			
		||||
        if (pageIndex + 1 < pageEntries.length) {
 | 
			
		||||
            // Not the last entry on the page;
 | 
			
		||||
            this.hasNext = true;
 | 
			
		||||
        } else if (pageEntries.length < perPage) {
 | 
			
		||||
            // Last entry of the last page.
 | 
			
		||||
            this.hasNext = false;
 | 
			
		||||
        } else {
 | 
			
		||||
            // Last entry of the page, check if there are more pages.
 | 
			
		||||
            const entries = await AddonModData.getEntries(this.database!.id, {
 | 
			
		||||
                groupId: this.selectedGroup,
 | 
			
		||||
                page: page + 1,
 | 
			
		||||
                perPage: perPage,
 | 
			
		||||
            });
 | 
			
		||||
            this.hasNext = entries?.entries?.length > 0;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (this.entryId > 0) {
 | 
			
		||||
            // Online entry, we need to fetch the the rating info.
 | 
			
		||||
            const entry = await AddonModData.getEntry(this.database!.id, this.entryId, { cmId: this.module.id });
 | 
			
		||||
            this.ratingInfo = entry.ratinginfo;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Function called when entry is being rendered.
 | 
			
		||||
     */
 | 
			
		||||
    setRenderingEntry(rendering: boolean): void {
 | 
			
		||||
        this.renderingEntry = rendering;
 | 
			
		||||
        this.cdr.detectChanges();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Function called when comments component is loading data.
 | 
			
		||||
     */
 | 
			
		||||
    setLoadingComments(loading: boolean): void {
 | 
			
		||||
        this.loadingComments = loading;
 | 
			
		||||
        this.cdr.detectChanges();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Function called when rate component is loading data.
 | 
			
		||||
     */
 | 
			
		||||
    setLoadingRating(loading: boolean): void {
 | 
			
		||||
        this.loadingRating = loading;
 | 
			
		||||
        this.cdr.detectChanges();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Function called when rating is updated online.
 | 
			
		||||
     */
 | 
			
		||||
    ratingUpdated(): void {
 | 
			
		||||
        AddonModData.invalidateEntryData(this.database!.id, this.entryId!);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Log viewing the activity.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async logView(): Promise<void> {
 | 
			
		||||
        if (!this.database || !this.database.id) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await CoreUtils.ignoreErrors(AddonModData.logView(this.database.id, this.database.name));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Component being destroyed.
 | 
			
		||||
     */
 | 
			
		||||
    ngOnDestroy(): void {
 | 
			
		||||
        this.syncObserver?.off();
 | 
			
		||||
        this.entryChangedObserver?.off();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										23
									
								
								src/addons/mod/data/pages/index/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/addons/mod/data/pages/index/index.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,23 @@
 | 
			
		||||
<ion-header>
 | 
			
		||||
    <ion-toolbar>
 | 
			
		||||
        <ion-buttons slot="start">
 | 
			
		||||
            <ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
 | 
			
		||||
        </ion-buttons>
 | 
			
		||||
        <ion-title>
 | 
			
		||||
            <core-format-text [text]="title" contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
 | 
			
		||||
            </core-format-text>
 | 
			
		||||
        </ion-title>
 | 
			
		||||
 | 
			
		||||
        <ion-buttons slot="end">
 | 
			
		||||
            <!-- The buttons defined by the component will be added in here. -->
 | 
			
		||||
        </ion-buttons>
 | 
			
		||||
    </ion-toolbar>
 | 
			
		||||
</ion-header>
 | 
			
		||||
<ion-content>
 | 
			
		||||
    <ion-refresher slot="fixed" [disabled]="!activityComponent?.loaded" (ionRefresh)="activityComponent?.doRefresh($event)">
 | 
			
		||||
        <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
 | 
			
		||||
    </ion-refresher>
 | 
			
		||||
 | 
			
		||||
    <addon-mod-data-index [module]="module" [courseId]="courseId" [group]="group" (dataRetrieved)="updateData($event)">
 | 
			
		||||
    </addon-mod-data-index>
 | 
			
		||||
</ion-content>
 | 
			
		||||
							
								
								
									
										41
									
								
								src/addons/mod/data/pages/index/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/addons/mod/data/pages/index/index.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,41 @@
 | 
			
		||||
// (C) Copyright 2015 Moodle Pty Ltd.
 | 
			
		||||
//
 | 
			
		||||
// Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
// you may not use this file except in compliance with the License.
 | 
			
		||||
// You may obtain a copy of the License at
 | 
			
		||||
//
 | 
			
		||||
//     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
//
 | 
			
		||||
// Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
// distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { Component, OnInit, ViewChild } from '@angular/core';
 | 
			
		||||
import { CoreCourseModuleMainActivityPage } from '@features/course/classes/main-activity-page';
 | 
			
		||||
import { CoreNavigator } from '@services/navigator';
 | 
			
		||||
import { AddonModDataIndexComponent } from '../../components/index/index';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Page that displays a data.
 | 
			
		||||
 */
 | 
			
		||||
@Component({
 | 
			
		||||
    selector: 'page-addon-mod-data-index',
 | 
			
		||||
    templateUrl: 'index.html',
 | 
			
		||||
})
 | 
			
		||||
export class AddonModDataIndexPage extends CoreCourseModuleMainActivityPage<AddonModDataIndexComponent> implements OnInit {
 | 
			
		||||
 | 
			
		||||
    @ViewChild(AddonModDataIndexComponent) activityComponent?: AddonModDataIndexComponent;
 | 
			
		||||
 | 
			
		||||
    group = 0;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Component being initialized.
 | 
			
		||||
     */
 | 
			
		||||
    ngOnInit(): void {
 | 
			
		||||
        super.ngOnInit();
 | 
			
		||||
        this.group = CoreNavigator.getRouteNumberParam('group') || 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -579,7 +579,7 @@ export class AddonModDataHelperProvider {
 | 
			
		||||
                entryFieldDataToSend.push({
 | 
			
		||||
                    fieldid: fieldSubdata.fieldid,
 | 
			
		||||
                    subfield: fieldSubdata.subfield || '',
 | 
			
		||||
                    value: fieldSubdata.value ? JSON.stringify(value) : '',
 | 
			
		||||
                    value: value ? JSON.stringify(value) : '',
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
@ -4,6 +4,7 @@
 | 
			
		||||
    --toobar-background: var(--white);
 | 
			
		||||
    --button-color: var(--ion-text-color);
 | 
			
		||||
    --button-active-color: var(--gray);
 | 
			
		||||
    --background: var(--ion-item-background);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
:host-context(body.dark) {
 | 
			
		||||
@ -39,7 +40,7 @@
 | 
			
		||||
            border-top: 1px solid var(--ion-color-secondary);
 | 
			
		||||
            background: var(--background);
 | 
			
		||||
            flex-shrink: 1;
 | 
			
		||||
            font-size: 1.4rem;
 | 
			
		||||
            font-size: 1.1rem;
 | 
			
		||||
 | 
			
		||||
            .icon {
 | 
			
		||||
                color: var(--ion-color-secondary);
 | 
			
		||||
 | 
			
		||||
@ -56,6 +56,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterContentIn
 | 
			
		||||
    // Based on: https://github.com/judgewest2000/Ionic3RichText/
 | 
			
		||||
    // @todo: Anchor button, fullscreen...
 | 
			
		||||
    // @todo: Textarea height is not being updated when editor is resized. Height is calculated if any css is changed.
 | 
			
		||||
    // @todo: Implement ControlValueAccessor https://angular.io/api/forms/ControlValueAccessor.
 | 
			
		||||
 | 
			
		||||
    @Input() placeholder = ''; // Placeholder to set in textarea.
 | 
			
		||||
    @Input() control?: FormControl; // Form control.
 | 
			
		||||
@ -724,6 +725,8 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterContentIn
 | 
			
		||||
     * Hide the toolbar in phone mode.
 | 
			
		||||
     */
 | 
			
		||||
    hideToolbar(event: Event): void {
 | 
			
		||||
        this.element.classList.remove('has-focus');
 | 
			
		||||
 | 
			
		||||
        this.stopBubble(event);
 | 
			
		||||
 | 
			
		||||
        if (this.isPhone) {
 | 
			
		||||
@ -735,6 +738,10 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterContentIn
 | 
			
		||||
     * Show the toolbar.
 | 
			
		||||
     */
 | 
			
		||||
    showToolbar(event: Event): void {
 | 
			
		||||
        this.element.classList.add('ion-touched');
 | 
			
		||||
        this.element.classList.remove('ion-untouched');
 | 
			
		||||
        this.element.classList.add('has-focus');
 | 
			
		||||
 | 
			
		||||
        this.stopBubble(event);
 | 
			
		||||
 | 
			
		||||
        this.editorElement?.focus();
 | 
			
		||||
@ -747,7 +754,9 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterContentIn
 | 
			
		||||
     * @param event Event.
 | 
			
		||||
     */
 | 
			
		||||
    stopBubble(event: Event): void {
 | 
			
		||||
        event.preventDefault();
 | 
			
		||||
        if (event.type != 'mouseup') {
 | 
			
		||||
            event.preventDefault();
 | 
			
		||||
        }
 | 
			
		||||
        event.stopPropagation();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -904,6 +913,9 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterContentIn
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this.element.classList.add('ion-touched');
 | 
			
		||||
            this.element.classList.remove('ion-untouched');
 | 
			
		||||
 | 
			
		||||
            let draftText = entry.drafttext || '';
 | 
			
		||||
 | 
			
		||||
            // Revert untouched editor contents to an empty string.
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user