Merge pull request #3956 from alfonso-salces/MOBILE-4219
MOBILE-4219 blog: Add and edit blog entries
This commit is contained in:
		
						commit
						8dfd363d6f
					
				
							
								
								
									
										1
									
								
								.github/workflows/acceptance.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/acceptance.yml
									
									
									
									
										vendored
									
									
								
							@ -86,6 +86,7 @@ jobs:
 | 
				
			|||||||
              "@addon_mod_survey"
 | 
					              "@addon_mod_survey"
 | 
				
			||||||
              "@addon_mod_workshop"
 | 
					              "@addon_mod_workshop"
 | 
				
			||||||
              "@addon_notifications"
 | 
					              "@addon_notifications"
 | 
				
			||||||
 | 
					              "@addon_blog"
 | 
				
			||||||
              "@core"
 | 
					              "@core"
 | 
				
			||||||
              "@core_comments"
 | 
					              "@core_comments"
 | 
				
			||||||
              "@core_course"
 | 
					              "@core_course"
 | 
				
			||||||
 | 
				
			|||||||
@ -85,16 +85,25 @@
 | 
				
			|||||||
  "addon.block_timeline.searchevents": "block_timeline",
 | 
					  "addon.block_timeline.searchevents": "block_timeline",
 | 
				
			||||||
  "addon.block_timeline.sortbycourses": "block_timeline",
 | 
					  "addon.block_timeline.sortbycourses": "block_timeline",
 | 
				
			||||||
  "addon.block_timeline.sortbydates": "block_timeline",
 | 
					  "addon.block_timeline.sortbydates": "block_timeline",
 | 
				
			||||||
 | 
					  "addon.blog.addnewentry": "blog",
 | 
				
			||||||
 | 
					  "addon.blog.associations": "blog",
 | 
				
			||||||
 | 
					  "addon.blog.associatewithcourse": "blog",
 | 
				
			||||||
 | 
					  "addon.blog.associatewithmodule": "blog",
 | 
				
			||||||
 | 
					  "addon.blog.attachment": "blog",
 | 
				
			||||||
  "addon.blog.blog": "blog",
 | 
					  "addon.blog.blog": "blog",
 | 
				
			||||||
  "addon.blog.blogentries": "blog",
 | 
					  "addon.blog.blogentries": "blog",
 | 
				
			||||||
 | 
					  "addon.blog.entrybody": "blog",
 | 
				
			||||||
 | 
					  "addon.blog.entrytitle": "blog",
 | 
				
			||||||
  "addon.blog.errorloadentries": "local_moodlemobileapp",
 | 
					  "addon.blog.errorloadentries": "local_moodlemobileapp",
 | 
				
			||||||
  "addon.blog.linktooriginalentry": "blog",
 | 
					  "addon.blog.linktooriginalentry": "blog",
 | 
				
			||||||
  "addon.blog.noentriesyet": "blog",
 | 
					  "addon.blog.noentriesyet": "blog",
 | 
				
			||||||
 | 
					  "addon.blog.publishto": "blog",
 | 
				
			||||||
  "addon.blog.publishtonoone": "blog",
 | 
					  "addon.blog.publishtonoone": "blog",
 | 
				
			||||||
  "addon.blog.publishtosite": "blog",
 | 
					  "addon.blog.publishtosite": "blog",
 | 
				
			||||||
  "addon.blog.publishtoworld": "blog",
 | 
					  "addon.blog.publishtoworld": "blog",
 | 
				
			||||||
  "addon.blog.showonlyyourentries": "local_moodlemobileapp",
 | 
					  "addon.blog.showonlyyourentries": "local_moodlemobileapp",
 | 
				
			||||||
  "addon.blog.siteblogheading": "blog",
 | 
					  "addon.blog.siteblogheading": "blog",
 | 
				
			||||||
 | 
					  "addon.blog.tags": "blog",
 | 
				
			||||||
  "addon.calendar.allday": "calendar",
 | 
					  "addon.calendar.allday": "calendar",
 | 
				
			||||||
  "addon.calendar.calendar": "calendar",
 | 
					  "addon.calendar.calendar": "calendar",
 | 
				
			||||||
  "addon.calendar.calendarevent": "local_moodlemobileapp",
 | 
					  "addon.calendar.calendarevent": "local_moodlemobileapp",
 | 
				
			||||||
@ -1500,6 +1509,7 @@
 | 
				
			|||||||
  "core.block.tour_navigation_dashboard_content": "tool_usertours",
 | 
					  "core.block.tour_navigation_dashboard_content": "tool_usertours",
 | 
				
			||||||
  "core.block.tour_navigation_dashboard_title": "tool_usertours",
 | 
					  "core.block.tour_navigation_dashboard_title": "tool_usertours",
 | 
				
			||||||
  "core.browser": "local_moodlemobileapp",
 | 
					  "core.browser": "local_moodlemobileapp",
 | 
				
			||||||
 | 
					  "core.bynameondate": "forum",
 | 
				
			||||||
  "core.calculating": "local_moodlemobileapp",
 | 
					  "core.calculating": "local_moodlemobileapp",
 | 
				
			||||||
  "core.cancel": "moodle",
 | 
					  "core.cancel": "moodle",
 | 
				
			||||||
  "core.cannotconnect": "local_moodlemobileapp",
 | 
					  "core.cannotconnect": "local_moodlemobileapp",
 | 
				
			||||||
 | 
				
			|||||||
@ -16,13 +16,15 @@ import { Injector, NgModule } from '@angular/core';
 | 
				
			|||||||
import { ROUTES, Routes } from '@angular/router';
 | 
					import { ROUTES, Routes } from '@angular/router';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { CoreSharedModule } from '@/core/shared.module';
 | 
					import { CoreSharedModule } from '@/core/shared.module';
 | 
				
			||||||
import { AddonBlogEntriesPage } from './pages/entries/entries';
 | 
					import { AddonBlogIndexPage } from './pages/index';
 | 
				
			||||||
import { CoreCommentsComponentsModule } from '@features/comments/components/components.module';
 | 
					import { CoreCommentsComponentsModule } from '@features/comments/components/components.module';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { CoreTagComponentsModule } from '@features/tag/components/components.module';
 | 
					import { CoreTagComponentsModule } from '@features/tag/components/components.module';
 | 
				
			||||||
 | 
					import { CoreMainMenuComponentsModule } from '@features/mainmenu/components/components.module';
 | 
				
			||||||
 | 
					import { AddonBlogEntryOptionsMenuComponent } from './components/entry-options-menu';
 | 
				
			||||||
import { buildTabMainRoutes } from '@features/mainmenu/mainmenu-tab-routing.module';
 | 
					import { buildTabMainRoutes } from '@features/mainmenu/mainmenu-tab-routing.module';
 | 
				
			||||||
import { ADDON_BLOG_MAINMENU_PAGE_NAME } from './constants';
 | 
					import { ADDON_BLOG_MAINMENU_PAGE_NAME } from './constants';
 | 
				
			||||||
import { CoreMainMenuComponentsModule } from '@features/mainmenu/components/components.module';
 | 
					import { canLeaveGuard } from '@guards/can-leave';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Build module routes.
 | 
					 * Build module routes.
 | 
				
			||||||
@ -30,13 +32,23 @@ import { CoreMainMenuComponentsModule } from '@features/mainmenu/components/comp
 | 
				
			|||||||
 * @param injector Injector.
 | 
					 * @param injector Injector.
 | 
				
			||||||
 * @returns Routes.
 | 
					 * @returns Routes.
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
function buildRoutes(injector: Injector): Routes {
 | 
					 function buildRoutes(injector: Injector): Routes {
 | 
				
			||||||
    return [
 | 
					    return [
 | 
				
			||||||
        ...buildTabMainRoutes(injector, {
 | 
					        {
 | 
				
			||||||
 | 
					            path: 'index',
 | 
				
			||||||
 | 
					            component: AddonBlogIndexPage,
 | 
				
			||||||
            data: {
 | 
					            data: {
 | 
				
			||||||
                mainMenuTabRoot: ADDON_BLOG_MAINMENU_PAGE_NAME,
 | 
					                mainMenuTabRoot: ADDON_BLOG_MAINMENU_PAGE_NAME,
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            component: AddonBlogEntriesPage,
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            path: 'edit/:id',
 | 
				
			||||||
 | 
					            loadComponent: () => import('./pages/edit-entry/edit-entry').then(c => c.AddonBlogEditEntryPage),
 | 
				
			||||||
 | 
					            canDeactivate: [canLeaveGuard],
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        ...buildTabMainRoutes(injector, {
 | 
				
			||||||
 | 
					            redirectTo: 'index',
 | 
				
			||||||
 | 
					            pathMatch: 'full',
 | 
				
			||||||
        }),
 | 
					        }),
 | 
				
			||||||
    ];
 | 
					    ];
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -48,6 +60,10 @@ function buildRoutes(injector: Injector): Routes {
 | 
				
			|||||||
        CoreTagComponentsModule,
 | 
					        CoreTagComponentsModule,
 | 
				
			||||||
        CoreMainMenuComponentsModule,
 | 
					        CoreMainMenuComponentsModule,
 | 
				
			||||||
    ],
 | 
					    ],
 | 
				
			||||||
 | 
					    declarations: [
 | 
				
			||||||
 | 
					        AddonBlogIndexPage,
 | 
				
			||||||
 | 
					        AddonBlogEntryOptionsMenuComponent,
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
    providers: [
 | 
					    providers: [
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            provide: ROUTES,
 | 
					            provide: ROUTES,
 | 
				
			||||||
@ -56,8 +72,5 @@ function buildRoutes(injector: Injector): Routes {
 | 
				
			|||||||
            useFactory: buildRoutes,
 | 
					            useFactory: buildRoutes,
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
    ],
 | 
					    ],
 | 
				
			||||||
    declarations: [
 | 
					 | 
				
			||||||
        AddonBlogEntriesPage,
 | 
					 | 
				
			||||||
    ],
 | 
					 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
export class AddonBlogLazyModule {}
 | 
					export class AddonBlogLazyModule {}
 | 
				
			||||||
 | 
				
			|||||||
@ -23,6 +23,7 @@ import { CoreMainMenuDelegate } from '@features/mainmenu/services/mainmenu-deleg
 | 
				
			|||||||
import { CoreTagAreaDelegate } from '@features/tag/services/tag-area-delegate';
 | 
					import { CoreTagAreaDelegate } from '@features/tag/services/tag-area-delegate';
 | 
				
			||||||
import { CoreUserDelegate } from '@features/user/services/user-delegate';
 | 
					import { CoreUserDelegate } from '@features/user/services/user-delegate';
 | 
				
			||||||
import { AddonBlogCourseOptionHandler } from './services/handlers/course-option';
 | 
					import { AddonBlogCourseOptionHandler } from './services/handlers/course-option';
 | 
				
			||||||
 | 
					import { AddonBlogEditEntryLinkHandler } from './services/handlers/edit-entry-link';
 | 
				
			||||||
import { AddonBlogIndexLinkHandler } from './services/handlers/index-link';
 | 
					import { AddonBlogIndexLinkHandler } from './services/handlers/index-link';
 | 
				
			||||||
import { AddonBlogMainMenuHandler } from './services/handlers/mainmenu';
 | 
					import { AddonBlogMainMenuHandler } from './services/handlers/mainmenu';
 | 
				
			||||||
import { AddonBlogTagAreaHandler } from './services/handlers/tag-area';
 | 
					import { AddonBlogTagAreaHandler } from './services/handlers/tag-area';
 | 
				
			||||||
@ -48,6 +49,7 @@ const routes: Routes = [
 | 
				
			|||||||
            multi: true,
 | 
					            multi: true,
 | 
				
			||||||
            useValue: () => {
 | 
					            useValue: () => {
 | 
				
			||||||
                CoreContentLinksDelegate.registerHandler(AddonBlogIndexLinkHandler.instance);
 | 
					                CoreContentLinksDelegate.registerHandler(AddonBlogIndexLinkHandler.instance);
 | 
				
			||||||
 | 
					                CoreContentLinksDelegate.registerHandler(AddonBlogEditEntryLinkHandler.instance);
 | 
				
			||||||
                CoreMainMenuDelegate.registerHandler(AddonBlogMainMenuHandler.instance);
 | 
					                CoreMainMenuDelegate.registerHandler(AddonBlogMainMenuHandler.instance);
 | 
				
			||||||
                CoreUserDelegate.registerHandler(AddonBlogUserHandler.instance);
 | 
					                CoreUserDelegate.registerHandler(AddonBlogUserHandler.instance);
 | 
				
			||||||
                CoreTagAreaDelegate.registerHandler(AddonBlogTagAreaHandler.instance);
 | 
					                CoreTagAreaDelegate.registerHandler(AddonBlogTagAreaHandler.instance);
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										16
									
								
								src/addons/blog/components/entry-options-menu.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src/addons/blog/components/entry-options-menu.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,16 @@
 | 
				
			|||||||
 | 
					<ion-content>
 | 
				
			||||||
 | 
					    <ion-list>
 | 
				
			||||||
 | 
					        <ion-item button class="ion-text-wrap" (click)="action('edit')" [detail]="false">
 | 
				
			||||||
 | 
					            <ion-icon name="fas-pen" slot="start" aria-hidden="true" />
 | 
				
			||||||
 | 
					            <ion-label>
 | 
				
			||||||
 | 
					                <p class="item-heading">{{ 'core.edit' | translate }}</p>
 | 
				
			||||||
 | 
					            </ion-label>
 | 
				
			||||||
 | 
					        </ion-item>
 | 
				
			||||||
 | 
					        <ion-item button class="ion-text-wrap" (click)="action('delete')" [detail]="false">
 | 
				
			||||||
 | 
					            <ion-icon name="fas-trash" slot="start" aria-hidden="true" color="danger" />
 | 
				
			||||||
 | 
					            <ion-label color="danger">
 | 
				
			||||||
 | 
					                <p class="item-heading">{{ 'core.delete' | translate }}</p>
 | 
				
			||||||
 | 
					            </ion-label>
 | 
				
			||||||
 | 
					        </ion-item>
 | 
				
			||||||
 | 
					    </ion-list>
 | 
				
			||||||
 | 
					</ion-content>
 | 
				
			||||||
							
								
								
									
										32
									
								
								src/addons/blog/components/entry-options-menu.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/addons/blog/components/entry-options-menu.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,32 @@
 | 
				
			|||||||
 | 
					// (C) Copyright 2015 Moodle Pty Ltd.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					// you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					// You may obtain a copy of the License at
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					//     http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					// distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					// See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					// limitations under the License.
 | 
				
			||||||
 | 
					import { Component } from '@angular/core';
 | 
				
			||||||
 | 
					import { PopoverController } from '@singletons';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Component({
 | 
				
			||||||
 | 
					    selector: 'addon-blog-entry-options-menu',
 | 
				
			||||||
 | 
					    templateUrl: './entry-options-menu.html',
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					export class AddonBlogEntryOptionsMenuComponent {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Do an action over the course.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param action Action name to take.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    action(action: string): void {
 | 
				
			||||||
 | 
					        PopoverController.dismiss(action);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -13,3 +13,4 @@
 | 
				
			|||||||
// limitations under the License.
 | 
					// limitations under the License.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const ADDON_BLOG_MAINMENU_PAGE_NAME = 'blog';
 | 
					export const ADDON_BLOG_MAINMENU_PAGE_NAME = 'blog';
 | 
				
			||||||
 | 
					export const ADDON_BLOG_ENTRY_UPDATED = 'blog_entry_updated';
 | 
				
			||||||
 | 
				
			|||||||
@ -1,12 +1,21 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
 | 
					    "addnewentry": "Add a new entry",
 | 
				
			||||||
 | 
					    "associations": "Associations",
 | 
				
			||||||
 | 
					    "attachment": "Attachment",
 | 
				
			||||||
    "blog": "Blog",
 | 
					    "blog": "Blog",
 | 
				
			||||||
    "blogentries": "Blog entries",
 | 
					    "blogentries": "Blog entries",
 | 
				
			||||||
 | 
					    "entrybody": "Blog entry body",
 | 
				
			||||||
 | 
					    "entrytitle": "Entry title",
 | 
				
			||||||
    "errorloadentries": "Error loading blog entries.",
 | 
					    "errorloadentries": "Error loading blog entries.",
 | 
				
			||||||
    "linktooriginalentry": "Link to original blog entry",
 | 
					    "linktooriginalentry": "Link to original blog entry",
 | 
				
			||||||
    "noentriesyet": "No visible entries here",
 | 
					    "noentriesyet": "No visible entries here",
 | 
				
			||||||
 | 
					    "publishto": "Publish to",
 | 
				
			||||||
    "publishtonoone": "Yourself (draft)",
 | 
					    "publishtonoone": "Yourself (draft)",
 | 
				
			||||||
    "publishtosite": "Anyone on this site",
 | 
					    "publishtosite": "Anyone on this site",
 | 
				
			||||||
    "publishtoworld": "Anyone in the world",
 | 
					    "publishtoworld": "Anyone in the world",
 | 
				
			||||||
    "showonlyyourentries": "Show only your entries",
 | 
					    "showonlyyourentries": "Show only your entries",
 | 
				
			||||||
    "siteblogheading": "Site blog"
 | 
					    "siteblogheading": "Site blog",
 | 
				
			||||||
 | 
					    "tags": "Tags",
 | 
				
			||||||
 | 
					    "associatewithcourse": "Blog about course {{$a.coursename}}",
 | 
				
			||||||
 | 
					    "associatewithmodule": "Blog about {{$a.modtype}}: {{$a.modname}}"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										93
									
								
								src/addons/blog/pages/edit-entry/edit-entry.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								src/addons/blog/pages/edit-entry/edit-entry.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,93 @@
 | 
				
			|||||||
 | 
					<ion-header>
 | 
				
			||||||
 | 
					    <ion-toolbar>
 | 
				
			||||||
 | 
					        <ion-buttons slot="start">
 | 
				
			||||||
 | 
					            <ion-back-button [text]="'core.back' | translate" />
 | 
				
			||||||
 | 
					        </ion-buttons>
 | 
				
			||||||
 | 
					        <ion-title>
 | 
				
			||||||
 | 
					            <h1>{{ entry ? entry.subject : 'addon.blog.addnewentry' | translate }}</h1>
 | 
				
			||||||
 | 
					        </ion-title>
 | 
				
			||||||
 | 
					        <ion-buttons slot="end" />
 | 
				
			||||||
 | 
					    </ion-toolbar>
 | 
				
			||||||
 | 
					</ion-header>
 | 
				
			||||||
 | 
					<ion-content>
 | 
				
			||||||
 | 
					    <core-loading [hideUntil]="loaded">
 | 
				
			||||||
 | 
					        <form [formGroup]="form">
 | 
				
			||||||
 | 
					            <ion-item>
 | 
				
			||||||
 | 
					                <ion-input labelPlacement="stacked" formControlName="subject" type="text"
 | 
				
			||||||
 | 
					                    [placeholder]="'addon.blog.entrytitle' | translate" name="title">
 | 
				
			||||||
 | 
					                    <p>{{ 'addon.blog.entrytitle' | translate }}</p>
 | 
				
			||||||
 | 
					                </ion-input>
 | 
				
			||||||
 | 
					            </ion-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <ion-item>
 | 
				
			||||||
 | 
					                <ion-label position="stacked" for="addon_blog_entry_body">{{ 'addon.blog.entrybody' | translate }}</ion-label>
 | 
				
			||||||
 | 
					                <core-rich-text-editor name="addon_blog_entry_body" [attr.aria-label]="'addon.blog.entrybody' | translate"
 | 
				
			||||||
 | 
					                    [control]="form.controls.summary" [placeholder]="'addon.blog.entrybody' | translate" [componentId]="component"
 | 
				
			||||||
 | 
					                    [autoSave]="true" [contextInstanceId]="contextInstanceId" [contextLevel]="contextLevel"
 | 
				
			||||||
 | 
					                    [elementId]="entry?.id ?? 'new_entry'" />
 | 
				
			||||||
 | 
					            </ion-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <ion-item>
 | 
				
			||||||
 | 
					                <core-combobox name="addon_blog_publish_to" formControlName="publishState" [label]="'addon.blog.publishto' | translate">
 | 
				
			||||||
 | 
					                    <ion-select-option class="core-select-option-title" [value]="publishState.draft">
 | 
				
			||||||
 | 
					                        {{ 'addon.blog.publishtonoone' | translate }}
 | 
				
			||||||
 | 
					                    </ion-select-option>
 | 
				
			||||||
 | 
					                    <ion-select-option class="core-select-option-title" [value]="publishState.site">
 | 
				
			||||||
 | 
					                        {{ 'addon.blog.publishtosite' | translate }}
 | 
				
			||||||
 | 
					                    </ion-select-option>
 | 
				
			||||||
 | 
					                </core-combobox>
 | 
				
			||||||
 | 
					            </ion-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <core-attachments [files]="files" [maxSubmissions]="maxFiles" [maxSize]="0" [component]="component" [allowOffline]="true"
 | 
				
			||||||
 | 
					                [componentId]="entry?.id ?? 0" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            @if (entry && courseId && associatedCourse) {
 | 
				
			||||||
 | 
					            <ion-item class="divider section" (click)="toggleAssociations()" button [detail]="false"
 | 
				
			||||||
 | 
					                [attr.aria-label]="(associationsExpanded ? 'core.collapse' : 'core.expand') | translate"
 | 
				
			||||||
 | 
					                [attr.aria-expanded]="associationsExpanded" aria-controls="addon-blog-associations"
 | 
				
			||||||
 | 
					                [class.expandable-status-icon-expanded]="associationsExpanded">
 | 
				
			||||||
 | 
					                <ion-icon [name]="associationsExpanded ? 'fas-chevron-down' : 'fas-chevron-right'" flip-rtl slot="start"
 | 
				
			||||||
 | 
					                    class="expandable-status-icon" />
 | 
				
			||||||
 | 
					                <ion-label>
 | 
				
			||||||
 | 
					                    <h2>{{ 'addon.blog.associations' | translate }}</h2>
 | 
				
			||||||
 | 
					                </ion-label>
 | 
				
			||||||
 | 
					            </ion-item>
 | 
				
			||||||
 | 
					            <div id="addon-blog-associations">
 | 
				
			||||||
 | 
					                @if (associationsExpanded) {
 | 
				
			||||||
 | 
					                <ion-item>
 | 
				
			||||||
 | 
					                    @if (associatedModule) {
 | 
				
			||||||
 | 
					                    <ion-toggle formControlName="associateWithModule">
 | 
				
			||||||
 | 
					                        <core-format-text [text]="'addon.blog.associatewithmodule' | translate: {
 | 
				
			||||||
 | 
					                            $a: { modtype: associatedModule.modname, modname: associatedModule.name }
 | 
				
			||||||
 | 
					                            }" [component]="component" [componentId]="entry.id" [contextLevel]="contextLevel"
 | 
				
			||||||
 | 
					                            [contextInstanceId]="contextInstanceId" [courseId]="entry.courseid" />
 | 
				
			||||||
 | 
					                    </ion-toggle>
 | 
				
			||||||
 | 
					                    } @else if (associatedCourse) {
 | 
				
			||||||
 | 
					                    <ion-toggle formControlName="associateWithCourse">
 | 
				
			||||||
 | 
					                        <core-format-text
 | 
				
			||||||
 | 
					                            [text]="'addon.blog.associatewithcourse' | translate: { $a: { coursename: associatedCourse.fullname } }"
 | 
				
			||||||
 | 
					                            [component]="component" [componentId]="entry.id" [contextLevel]="contextLevel"
 | 
				
			||||||
 | 
					                            [contextInstanceId]="contextInstanceId" [courseId]="entry.courseid" />
 | 
				
			||||||
 | 
					                    </ion-toggle>
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                </ion-item>
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <ion-item class="addon-blog-entry-buttons">
 | 
				
			||||||
 | 
					                <ion-label>
 | 
				
			||||||
 | 
					                    <ion-row>
 | 
				
			||||||
 | 
					                        <ion-col>
 | 
				
			||||||
 | 
					                            <ion-button expand="block" [attr.aria-label]="(entry ? 'core.save' : 'addon.blog.addnewentry') | translate"
 | 
				
			||||||
 | 
					                                [disabled]="form.invalid || (entry && !hasDataChangedForEdit)" (click)="save()">
 | 
				
			||||||
 | 
					                                {{ (entry ? 'core.save' : 'addon.blog.addnewentry') | translate }}
 | 
				
			||||||
 | 
					                            </ion-button>
 | 
				
			||||||
 | 
					                        </ion-col>
 | 
				
			||||||
 | 
					                    </ion-row>
 | 
				
			||||||
 | 
					                </ion-label>
 | 
				
			||||||
 | 
					            </ion-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        </form>
 | 
				
			||||||
 | 
					    </core-loading>
 | 
				
			||||||
 | 
					</ion-content>
 | 
				
			||||||
							
								
								
									
										364
									
								
								src/addons/blog/pages/edit-entry/edit-entry.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										364
									
								
								src/addons/blog/pages/edit-entry/edit-entry.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,364 @@
 | 
				
			|||||||
 | 
					// (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 { CoreSharedModule } from '@/core/shared.module';
 | 
				
			||||||
 | 
					import { ADDON_BLOG_ENTRY_UPDATED } from '@addons/blog/constants';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					    AddonBlog,
 | 
				
			||||||
 | 
					    AddonBlogAddEntryOption,
 | 
				
			||||||
 | 
					    AddonBlogFilter,
 | 
				
			||||||
 | 
					    AddonBlogPost,
 | 
				
			||||||
 | 
					    AddonBlogProvider,
 | 
				
			||||||
 | 
					    AddonBlogPublishState,
 | 
				
			||||||
 | 
					    ADDON_BLOG_PUBLISH_STATE,
 | 
				
			||||||
 | 
					} from '@addons/blog/services/blog';
 | 
				
			||||||
 | 
					import { Component, OnInit } from '@angular/core';
 | 
				
			||||||
 | 
					import { FormControl, FormGroup, Validators } from '@angular/forms';
 | 
				
			||||||
 | 
					import { CoreError } from '@classes/errors/error';
 | 
				
			||||||
 | 
					import { CoreCommentsComponentsModule } from '@features/comments/components/components.module';
 | 
				
			||||||
 | 
					import { CoreCourse } from '@features/course/services/course';
 | 
				
			||||||
 | 
					import { CoreCourseHelper, CoreCourseModuleData } from '@features/course/services/course-helper';
 | 
				
			||||||
 | 
					import { CoreCourseBasicData } from '@features/courses/services/courses';
 | 
				
			||||||
 | 
					import { CoreEditorComponentsModule } from '@features/editor/components/components.module';
 | 
				
			||||||
 | 
					import { CoreFileUploader } from '@features/fileuploader/services/fileuploader';
 | 
				
			||||||
 | 
					import { CoreTagComponentsModule } from '@features/tag/components/components.module';
 | 
				
			||||||
 | 
					import { CanLeave } from '@guards/can-leave';
 | 
				
			||||||
 | 
					import { CoreNavigator } from '@services/navigator';
 | 
				
			||||||
 | 
					import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
 | 
				
			||||||
 | 
					import { CoreDomUtils } from '@services/utils/dom';
 | 
				
			||||||
 | 
					import { CoreUtils } from '@services/utils/utils';
 | 
				
			||||||
 | 
					import { CoreWSFile } from '@services/ws';
 | 
				
			||||||
 | 
					import { Translate } from '@singletons';
 | 
				
			||||||
 | 
					import { CoreEvents } from '@singletons/events';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Component({
 | 
				
			||||||
 | 
					    selector: 'addon-blog-edit-entry',
 | 
				
			||||||
 | 
					    templateUrl: './edit-entry.html',
 | 
				
			||||||
 | 
					    standalone: true,
 | 
				
			||||||
 | 
					    imports: [
 | 
				
			||||||
 | 
					        CoreEditorComponentsModule,
 | 
				
			||||||
 | 
					        CoreSharedModule,
 | 
				
			||||||
 | 
					        CoreCommentsComponentsModule,
 | 
				
			||||||
 | 
					        CoreTagComponentsModule,
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					export class AddonBlogEditEntryPage implements CanLeave, OnInit {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    publishState = ADDON_BLOG_PUBLISH_STATE;
 | 
				
			||||||
 | 
					    form = new FormGroup({
 | 
				
			||||||
 | 
					        subject: new FormControl<string>('', { nonNullable: true, validators: [Validators.required] }),
 | 
				
			||||||
 | 
					        summary: new FormControl<string>('', { nonNullable: true, validators: [Validators.required] }),
 | 
				
			||||||
 | 
					        publishState: new FormControl<AddonBlogPublishState>(
 | 
				
			||||||
 | 
					            ADDON_BLOG_PUBLISH_STATE.draft,
 | 
				
			||||||
 | 
					            { nonNullable: true, validators: [Validators.required] },
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        associateWithCourse: new FormControl<boolean>(false, { nonNullable: true, validators: [Validators.required] }),
 | 
				
			||||||
 | 
					        associateWithModule: new FormControl<boolean>(false, { nonNullable: true, validators: [Validators.required] }),
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    entry?: AddonBlogPost;
 | 
				
			||||||
 | 
					    loaded = false;
 | 
				
			||||||
 | 
					    maxFiles = 99;
 | 
				
			||||||
 | 
					    initialFiles: CoreWSFile[] = [];
 | 
				
			||||||
 | 
					    files: CoreWSFile[] = [];
 | 
				
			||||||
 | 
					    courseId?: number;
 | 
				
			||||||
 | 
					    modId?: number;
 | 
				
			||||||
 | 
					    userId?: number;
 | 
				
			||||||
 | 
					    associatedCourse?: CoreCourseBasicData;
 | 
				
			||||||
 | 
					    associatedModule?: CoreCourseModuleData;
 | 
				
			||||||
 | 
					    associationsExpanded = false;
 | 
				
			||||||
 | 
					    contextLevel: ContextLevel = ContextLevel.SYSTEM;
 | 
				
			||||||
 | 
					    contextInstanceId = 0;
 | 
				
			||||||
 | 
					    component = AddonBlogProvider.COMPONENT;
 | 
				
			||||||
 | 
					    siteHomeId?: number;
 | 
				
			||||||
 | 
					    forceLeave = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Gives if the form is not pristine. (only for existing entries)
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @returns Data has changed or not.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    get hasDataChangedForEdit(): boolean {
 | 
				
			||||||
 | 
					        const form = this.form.controls;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return form.summary.value !== this.entry?.summary ||
 | 
				
			||||||
 | 
					            form.subject.value !== this.entry?.subject ||
 | 
				
			||||||
 | 
					            form.publishState.value !== this.entry?.publishstate ||
 | 
				
			||||||
 | 
					            CoreFileUploader.areFileListDifferent(this.files, this.initialFiles) ||
 | 
				
			||||||
 | 
					            form.associateWithModule.value !== (this.entry?.moduleid !== 0) ||
 | 
				
			||||||
 | 
					            form.associateWithCourse.value !== (this.entry?.courseid !== 0);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Gives if the form is not pristine. (only for new entries)
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @returns Data has changed or not.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    get hasDataChangedForNewEntry(): boolean {
 | 
				
			||||||
 | 
					        const form = this.form.controls;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return form.subject.value !== '' ||
 | 
				
			||||||
 | 
					            form.summary.value !== '' ||
 | 
				
			||||||
 | 
					            form.publishState.value !== ADDON_BLOG_PUBLISH_STATE.draft ||
 | 
				
			||||||
 | 
					            CoreFileUploader.areFileListDifferent(this.files, this.initialFiles);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @inheritdoc
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    async ngOnInit(): Promise<void> {
 | 
				
			||||||
 | 
					        const site = await CoreSites.getSite();
 | 
				
			||||||
 | 
					        const isEditingEnabled = await AddonBlog.isEditingEnabled();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (!site || !isEditingEnabled) {
 | 
				
			||||||
 | 
					            return CoreNavigator.back();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const entryId = CoreNavigator.getRouteNumberParam('id');
 | 
				
			||||||
 | 
					        const lastModified = CoreNavigator.getRouteNumberParam('lastModified');
 | 
				
			||||||
 | 
					        const filters: AddonBlogFilter | undefined = CoreNavigator.getRouteParam('filters');
 | 
				
			||||||
 | 
					        this.userId = CoreNavigator.getRouteNumberParam('userId');
 | 
				
			||||||
 | 
					        this.siteHomeId = CoreSites.getCurrentSiteHomeId();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (!entryId) {
 | 
				
			||||||
 | 
					            this.loaded = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            this.entry = await this.getEntry({ filters, lastModified, entryId });
 | 
				
			||||||
 | 
					            this.files = this.entry.attachmentfiles ?? [];
 | 
				
			||||||
 | 
					            this.initialFiles = [...this.files];
 | 
				
			||||||
 | 
					            this.courseId = this.entry.courseid;
 | 
				
			||||||
 | 
					            this.modId = this.entry.coursemoduleid ? this.entry.coursemoduleid : CoreNavigator.getRouteNumberParam('cmId');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (this.courseId) {
 | 
				
			||||||
 | 
					                this.form.controls.associateWithCourse.setValue(true);
 | 
				
			||||||
 | 
					                const { course } = await CoreCourseHelper.getCourse(this.courseId);
 | 
				
			||||||
 | 
					                this.associatedCourse = course;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (this.modId) {
 | 
				
			||||||
 | 
					                this.form.controls.associateWithModule.setValue(true);
 | 
				
			||||||
 | 
					                this.associatedModule = await CoreCourse.getModule(this.modId);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } catch (error) {
 | 
				
			||||||
 | 
					            CoreDomUtils.showErrorModalDefault(error, 'Error retrieving data.');
 | 
				
			||||||
 | 
					            this.forceLeave = true;
 | 
				
			||||||
 | 
					            CoreNavigator.back();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.form.setValue({
 | 
				
			||||||
 | 
					            subject: this.entry?.subject ?? '',
 | 
				
			||||||
 | 
					            summary: this.entry?.summary ?? '',
 | 
				
			||||||
 | 
					            publishState: this.entry?.publishstate ?? ADDON_BLOG_PUBLISH_STATE.draft,
 | 
				
			||||||
 | 
					            associateWithCourse: this.form.controls.associateWithCourse.value,
 | 
				
			||||||
 | 
					            associateWithModule: this.form.controls.associateWithModule.value,
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.calculateContext();
 | 
				
			||||||
 | 
					        this.loaded = true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Retrieves blog entry.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @returns Blog entry.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    protected async getEntry(params: AddonBlogEditEntryGetEntryParams): Promise<AddonBlogPost> {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            const { entries } = await AddonBlog.getEntries(
 | 
				
			||||||
 | 
					                { entryid: params.entryId },
 | 
				
			||||||
 | 
					                { readingStrategy: CoreSitesReadingStrategy.PREFER_NETWORK },
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            const selectedEntry = entries.find(entry => entry.id === params.entryId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (!selectedEntry) {
 | 
				
			||||||
 | 
					                throw new CoreError('Entry not found');
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (params.filters && params.lastModified && selectedEntry.lastmodified < params.lastModified) {
 | 
				
			||||||
 | 
					                throw new CoreError('Entry is outdated');
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return selectedEntry;
 | 
				
			||||||
 | 
					        } catch (error) {
 | 
				
			||||||
 | 
					            if (!params.filters || CoreUtils.isWebServiceError(error)) {
 | 
				
			||||||
 | 
					                // Cannot get the entry, reject.
 | 
				
			||||||
 | 
					                throw error;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            const updatedEntries = await AddonBlog.getEntries(params.filters);
 | 
				
			||||||
 | 
					            const entry = updatedEntries.entries.find(entry => entry.id === params.entryId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (!entry) {
 | 
				
			||||||
 | 
					                throw error;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return entry;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Calculate context level and context instance.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    calculateContext(): void {
 | 
				
			||||||
 | 
					        // Calculate the context level.
 | 
				
			||||||
 | 
					        if (this.userId && !this.courseId && !this.modId) {
 | 
				
			||||||
 | 
					            this.contextLevel = ContextLevel.USER;
 | 
				
			||||||
 | 
					            this.contextInstanceId = this.userId;
 | 
				
			||||||
 | 
					        } else if (this.courseId && this.courseId != this.siteHomeId) {
 | 
				
			||||||
 | 
					            this.contextLevel = ContextLevel.COURSE;
 | 
				
			||||||
 | 
					            this.contextInstanceId = this.courseId;
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            this.contextLevel = ContextLevel.SYSTEM;
 | 
				
			||||||
 | 
					            this.contextInstanceId = 0;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Update or create entry.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @returns Promise resolved when done.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    async save(): Promise<void> {
 | 
				
			||||||
 | 
					        const { summary, subject, publishState } = this.form.value;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (!subject || !summary || !publishState) {
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const loading = await CoreDomUtils.showModalLoading('core.sending', true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (this.entry) {
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                if (!CoreFileUploader.areFileListDifferent(this.files, this.initialFiles)) {
 | 
				
			||||||
 | 
					                    return await this.saveEntry();
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                const { attachmentsid } = await AddonBlog.prepareEntryForEdition({ entryid: this.entry.id });
 | 
				
			||||||
 | 
					                const removedFiles = CoreFileUploader.getFilesToDelete(this.initialFiles, this.files);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (removedFiles.length) {
 | 
				
			||||||
 | 
					                    await CoreFileUploader.deleteDraftFiles(attachmentsid, removedFiles);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                await CoreFileUploader.uploadFiles(attachmentsid, this.files);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                return await this.saveEntry(attachmentsid);
 | 
				
			||||||
 | 
					            } catch (error) {
 | 
				
			||||||
 | 
					                CoreDomUtils.showErrorModalDefault(error, 'Error updating entry.');
 | 
				
			||||||
 | 
					            } finally {
 | 
				
			||||||
 | 
					                await loading.dismiss();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            if (!this.files.length) {
 | 
				
			||||||
 | 
					                return await this.saveEntry();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            const attachmentId = await CoreFileUploader.uploadOrReuploadFiles(this.files, this.component);
 | 
				
			||||||
 | 
					            await this.saveEntry(attachmentId);
 | 
				
			||||||
 | 
					        } catch (error) {
 | 
				
			||||||
 | 
					            CoreDomUtils.showErrorModalDefault(error, 'Error creating entry.');
 | 
				
			||||||
 | 
					        } finally {
 | 
				
			||||||
 | 
					            await loading.dismiss();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Expand or collapse associations.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    toggleAssociations(): void {
 | 
				
			||||||
 | 
					        this.associationsExpanded = !this.associationsExpanded;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Check if the user can leave the view. If there are changes to be saved, it will ask for confirm.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @returns Promise resolved with true if can leave the view, rejected otherwise.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    async canLeave(): Promise<boolean> {
 | 
				
			||||||
 | 
					        if (this.forceLeave) {
 | 
				
			||||||
 | 
					            return true;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if ((!this.entry && this.hasDataChangedForNewEntry) || (this.entry && this.hasDataChangedForEdit)) {
 | 
				
			||||||
 | 
					            // Modified, confirm user wants to go back.
 | 
				
			||||||
 | 
					            await CoreDomUtils.showConfirm(Translate.instant('core.confirmcanceledit'));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Add attachment to options list.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param attachmentsId Attachment ID.
 | 
				
			||||||
 | 
					     * @param options Options list.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    addAttachments(attachmentsId: number | undefined, options: AddonBlogAddEntryOption[]): void {
 | 
				
			||||||
 | 
					        if (attachmentsId === undefined) {
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        options.push({ name: 'attachmentsid', value: attachmentsId });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Create or update entry.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param attachmentsId Attachments.
 | 
				
			||||||
 | 
					     * @returns Promise resolved when done.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    async saveEntry(attachmentsId?: number): Promise<void> {
 | 
				
			||||||
 | 
					        const { summary, subject, publishState } = this.form.value;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (!summary || !subject || !publishState) {
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const options: AddonBlogAddEntryOption[] = [
 | 
				
			||||||
 | 
					            { name: 'publishstate', value: publishState },
 | 
				
			||||||
 | 
					            { name: 'courseassoc', value: this.form.controls.associateWithCourse.value && this.courseId ? this.courseId : 0 },
 | 
				
			||||||
 | 
					            { name: 'modassoc', value: this.form.controls.associateWithModule.value && this.modId ? this.modId : 0 },
 | 
				
			||||||
 | 
					        ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.addAttachments(attachmentsId, options);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.entry
 | 
				
			||||||
 | 
					            ? await AddonBlog.updateEntry({ subject, summary, summaryformat: 1, options , entryid: this.entry.id })
 | 
				
			||||||
 | 
					            : await AddonBlog.addEntry({ subject, summary, summaryformat: 1, options });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        CoreEvents.trigger(ADDON_BLOG_ENTRY_UPDATED);
 | 
				
			||||||
 | 
					        this.forceLeave = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return CoreNavigator.back();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type AddonBlogEditEntryGetEntryParams = {
 | 
				
			||||||
 | 
					    entryId: number;
 | 
				
			||||||
 | 
					    filters?: AddonBlogFilter;
 | 
				
			||||||
 | 
					    lastModified?: number;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@ -1,78 +0,0 @@
 | 
				
			|||||||
<ion-header>
 | 
					 | 
				
			||||||
    <ion-toolbar>
 | 
					 | 
				
			||||||
        <ion-buttons slot="start">
 | 
					 | 
				
			||||||
            <ion-back-button [text]="'core.back' | translate" />
 | 
					 | 
				
			||||||
        </ion-buttons>
 | 
					 | 
				
			||||||
        <ion-title>
 | 
					 | 
				
			||||||
            <h1>{{ title | translate }}</h1>
 | 
					 | 
				
			||||||
        </ion-title>
 | 
					 | 
				
			||||||
        <ion-buttons slot="end">
 | 
					 | 
				
			||||||
            <core-user-menu-button />
 | 
					 | 
				
			||||||
        </ion-buttons>
 | 
					 | 
				
			||||||
    </ion-toolbar>
 | 
					 | 
				
			||||||
</ion-header>
 | 
					 | 
				
			||||||
<ion-content class="limited-width">
 | 
					 | 
				
			||||||
    <ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refresh($event.target)">
 | 
					 | 
				
			||||||
        <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}" />
 | 
					 | 
				
			||||||
    </ion-refresher>
 | 
					 | 
				
			||||||
    <core-loading [hideUntil]="loaded">
 | 
					 | 
				
			||||||
        <ion-item *ngIf="showMyEntriesToggle">
 | 
					 | 
				
			||||||
            <ion-toggle [(ngModel)]="onlyMyEntries" (ionChange)="onlyMyEntriesToggleChanged(onlyMyEntries)">
 | 
					 | 
				
			||||||
                {{ 'addon.blog.showonlyyourentries' | translate }}
 | 
					 | 
				
			||||||
            </ion-toggle>
 | 
					 | 
				
			||||||
        </ion-item>
 | 
					 | 
				
			||||||
        <core-empty-box *ngIf="entries && entries.length === 0" icon="far-newspaper" [message]="'addon.blog.noentriesyet' | translate" />
 | 
					 | 
				
			||||||
        <ng-container *ngFor="let entry of entries">
 | 
					 | 
				
			||||||
            <ion-card *ngIf="!onlyMyEntries || entry.userid === currentUserId">
 | 
					 | 
				
			||||||
                <ion-item class="ion-text-wrap">
 | 
					 | 
				
			||||||
                    <core-user-avatar [user]="entry.user" slot="start" [courseId]="entry.courseid" />
 | 
					 | 
				
			||||||
                    <ion-label>
 | 
					 | 
				
			||||||
                        <div class="flex-row ion-justify-content-between ion-align-items-center">
 | 
					 | 
				
			||||||
                            <h2>
 | 
					 | 
				
			||||||
                                <core-format-text [text]="entry.subject" [contextLevel]="contextLevel"
 | 
					 | 
				
			||||||
                                    [contextInstanceId]="contextInstanceId" [courseId]="entry.courseid" />
 | 
					 | 
				
			||||||
                            </h2>
 | 
					 | 
				
			||||||
                            <ion-note class="ion-text-end">
 | 
					 | 
				
			||||||
                                {{ 'addon.blog.' + entry.publishTranslated! | translate}}
 | 
					 | 
				
			||||||
                            </ion-note>
 | 
					 | 
				
			||||||
                        </div>
 | 
					 | 
				
			||||||
                        <div class="flex-row ion-justify-content-between ion-align-items-center">
 | 
					 | 
				
			||||||
                            {{entry.user && entry.user.fullname}}
 | 
					 | 
				
			||||||
                            <ion-note class="ion-text-end">
 | 
					 | 
				
			||||||
                                {{entry.created | coreDateDayOrTime}}
 | 
					 | 
				
			||||||
                            </ion-note>
 | 
					 | 
				
			||||||
                        </div>
 | 
					 | 
				
			||||||
                    </ion-label>
 | 
					 | 
				
			||||||
                </ion-item>
 | 
					 | 
				
			||||||
                <ion-card-content>
 | 
					 | 
				
			||||||
                    <ion-item class="ion-text-wrap">
 | 
					 | 
				
			||||||
                        <ion-label>
 | 
					 | 
				
			||||||
                            <core-format-text [text]="entry.summary" [component]="component" [componentId]="entry.id"
 | 
					 | 
				
			||||||
                                [contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="entry.courseid" />
 | 
					 | 
				
			||||||
                        </ion-label>
 | 
					 | 
				
			||||||
                    </ion-item>
 | 
					 | 
				
			||||||
                    <ion-item class="ion-text-wrap" *ngIf="tagsEnabled && entry.tags && entry.tags!.length > 0">
 | 
					 | 
				
			||||||
                        <ion-label>
 | 
					 | 
				
			||||||
                            <div slot="start">{{ 'core.tag.tags' | translate }}:</div>
 | 
					 | 
				
			||||||
                            <core-tag-list [tags]="entry.tags" />
 | 
					 | 
				
			||||||
                        </ion-label>
 | 
					 | 
				
			||||||
                    </ion-item>
 | 
					 | 
				
			||||||
                    <core-comments *ngIf="commentsEnabled" [component]="component" [itemId]="entry.id" area="format_blog"
 | 
					 | 
				
			||||||
                        [instanceId]="entry.userid" contextLevel="user" [showItem]="true" [courseId]="entry.courseid" />
 | 
					 | 
				
			||||||
                    <core-file *ngFor="let file of entry.attachmentfiles" [file]="file" [component]="component" [componentId]="entry.id" />
 | 
					 | 
				
			||||||
                    <ion-item *ngIf="entry.uniquehash" [href]="entry.uniquehash" core-link [detail]="true">
 | 
					 | 
				
			||||||
                        <ion-label>{{ 'addon.blog.linktooriginalentry' | translate }}</ion-label>
 | 
					 | 
				
			||||||
                    </ion-item>
 | 
					 | 
				
			||||||
                </ion-card-content>
 | 
					 | 
				
			||||||
                <div class="ion-text-center ion-margin-bottom" *ngIf="entry.lastmodified > entry.created">
 | 
					 | 
				
			||||||
                    <ion-note>
 | 
					 | 
				
			||||||
                        <ion-icon name="fas-clock" [attr.aria-label]="'core.lastmodified' | translate" /> {{entry.lastmodified
 | 
					 | 
				
			||||||
                        |
 | 
					 | 
				
			||||||
                        coreTimeAgo}}
 | 
					 | 
				
			||||||
                    </ion-note>
 | 
					 | 
				
			||||||
                </div>
 | 
					 | 
				
			||||||
            </ion-card>
 | 
					 | 
				
			||||||
        </ng-container>
 | 
					 | 
				
			||||||
        <core-infinite-loading [enabled]="canLoadMore" (action)="loadMore($event)" [error]="loadMoreError" />
 | 
					 | 
				
			||||||
    </core-loading>
 | 
					 | 
				
			||||||
</ion-content>
 | 
					 | 
				
			||||||
							
								
								
									
										134
									
								
								src/addons/blog/pages/index/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										134
									
								
								src/addons/blog/pages/index/index.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,134 @@
 | 
				
			|||||||
 | 
					<ion-header>
 | 
				
			||||||
 | 
					    <ion-toolbar>
 | 
				
			||||||
 | 
					        <ion-buttons slot="start">
 | 
				
			||||||
 | 
					            <ion-back-button [text]="'core.back' | translate" />
 | 
				
			||||||
 | 
					        </ion-buttons>
 | 
				
			||||||
 | 
					        <ion-title>
 | 
				
			||||||
 | 
					            <h1>{{ title | translate }}</h1>
 | 
				
			||||||
 | 
					        </ion-title>
 | 
				
			||||||
 | 
					        <ion-buttons slot="end">
 | 
				
			||||||
 | 
					            <core-user-menu-button />
 | 
				
			||||||
 | 
					        </ion-buttons>
 | 
				
			||||||
 | 
					    </ion-toolbar>
 | 
				
			||||||
 | 
					</ion-header>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<ion-content class="limited-width">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refresh($event.target)">
 | 
				
			||||||
 | 
					        <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}" />
 | 
				
			||||||
 | 
					    </ion-refresher>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <core-loading [hideUntil]="loaded">
 | 
				
			||||||
 | 
					        @if (showMyEntriesToggle) {
 | 
				
			||||||
 | 
					        <ion-item>
 | 
				
			||||||
 | 
					            <ion-toggle [(ngModel)]="onlyMyEntries" (ionChange)="onlyMyEntriesToggleChanged(onlyMyEntries)">
 | 
				
			||||||
 | 
					                {{ 'addon.blog.showonlyyourentries' | translate }}
 | 
				
			||||||
 | 
					            </ion-toggle>
 | 
				
			||||||
 | 
					        </ion-item>
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        @for (entry of entries; track entry.id) {
 | 
				
			||||||
 | 
					        @if (!onlyMyEntries || entry.userid === currentUserId) {
 | 
				
			||||||
 | 
					        <div class="entry ion-padding-start ion-padding-top ion-padding-end" [id]="'entry-' + entry.id">
 | 
				
			||||||
 | 
					            <div class="entry-subject flex ion-text-wrap ion-justify-content-between ion-align-items-center">
 | 
				
			||||||
 | 
					                <h3>
 | 
				
			||||||
 | 
					                    <core-format-text [text]="entry.subject" [contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId"
 | 
				
			||||||
 | 
					                        [courseId]="entry.courseid" />
 | 
				
			||||||
 | 
					                    @if (entry.userid === currentUserId && entry.publishTranslated === 'publishtonoone') {
 | 
				
			||||||
 | 
					                    <span class="entry-draft">
 | 
				
			||||||
 | 
					                        <ion-badge color="warning"> {{ 'addon.blog.publishtonoone' | translate }} </ion-badge>
 | 
				
			||||||
 | 
					                    </span>
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                </h3>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                @if (entry.userid === currentUserId && optionsAvailable) {
 | 
				
			||||||
 | 
					                <div class="core-button-spinner">
 | 
				
			||||||
 | 
					                    <ion-button fill="clear" [attr.aria-label]="'core.displayoptions' | translate"
 | 
				
			||||||
 | 
					                        (click)="showEntryActionsPopover($event, entry)">
 | 
				
			||||||
 | 
					                        <ion-icon slot="icon-only" aria-hidden="true" name="ellipsis-vertical" />
 | 
				
			||||||
 | 
					                    </ion-button>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <div class="entry-creation-info flex ion-align-items-center">
 | 
				
			||||||
 | 
					                <span>
 | 
				
			||||||
 | 
					                    <core-user-avatar [user]="entry.user" [courseId]="entry.courseid" />
 | 
				
			||||||
 | 
					                </span>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <span [innerHTML]="'core.bynameondate' | translate: {
 | 
				
			||||||
 | 
					                        '$a': { name: '<strong>' + entry?.user?.fullname + '</strong>', date: (entry.created | coreDateDayOrTime) }
 | 
				
			||||||
 | 
					                    }">
 | 
				
			||||||
 | 
					                </span>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <ion-label>
 | 
				
			||||||
 | 
					                <div class="entry-summary" [ngClass]="{ 'border-bottom': entry.lastmodified <= entry.created }" [collapsible-item]="64">
 | 
				
			||||||
 | 
					                    <div class="ion-margin-bottom">
 | 
				
			||||||
 | 
					                        <core-format-text [text]="entry.summary" [component]="component" [componentId]="entry.id"
 | 
				
			||||||
 | 
					                            [contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="entry.courseid" />
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    @if (tagsEnabled && entry.tags && entry.tags!.length > 0) {
 | 
				
			||||||
 | 
					                    <ion-item class="ion-text-wrap">
 | 
				
			||||||
 | 
					                        <ion-label>
 | 
				
			||||||
 | 
					                            <div slot="start">{{ 'core.tag.tags' | translate }}:</div>
 | 
				
			||||||
 | 
					                            <core-tag-list [tags]="entry.tags" />
 | 
				
			||||||
 | 
					                        </ion-label>
 | 
				
			||||||
 | 
					                    </ion-item>
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    @for (file of entry.attachmentfiles; track $index) {
 | 
				
			||||||
 | 
					                    <core-file [file]="file" [component]="this.component" [componentId]="entry.id" />
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    @if (entry.uniquehash) {
 | 
				
			||||||
 | 
					                    <ion-item [href]="entry.uniquehash" core-link [detail]="true">
 | 
				
			||||||
 | 
					                        <ion-label>{{ 'addon.blog.linktooriginalentry' | translate }}</ion-label>
 | 
				
			||||||
 | 
					                    </ion-item>
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					            </ion-label>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            @if (entry.lastmodified > entry.created) {
 | 
				
			||||||
 | 
					            <div class="entry-last-modification flex ion-justify-content-between border-bottom ion-padding-top ion-padding-bottom">
 | 
				
			||||||
 | 
					                <ion-note class="flex ion-align-items-center">
 | 
				
			||||||
 | 
					                    <ion-icon name="fas-clock" [attr.aria-label]="'core.lastmodified' | translate" />
 | 
				
			||||||
 | 
					                    {{ entry.lastmodified | coreTimeAgo }}
 | 
				
			||||||
 | 
					                </ion-note>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                @if (entry.userid === currentUserId && entry.publishstate !== 'draft') {
 | 
				
			||||||
 | 
					                <ion-badge class="entry-visibility-permission" color="success">
 | 
				
			||||||
 | 
					                    <ion-icon name="fas-eye" />
 | 
				
			||||||
 | 
					                    {{ 'addon.blog.' + entry.publishTranslated | translate }}
 | 
				
			||||||
 | 
					                </ion-badge>
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            @if (commentsEnabled) {
 | 
				
			||||||
 | 
					            <core-comments [component]="this.component" [itemId]="entry.id" area="format_blog" [instanceId]="entry.userid"
 | 
				
			||||||
 | 
					                contextLevel="user" [showItem]="true" [courseId]="entry.courseid" />
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        } @empty {
 | 
				
			||||||
 | 
					        <core-empty-box icon="far-newspaper" [message]="'addon.blog.noentriesyet' | translate" />
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <core-infinite-loading [enabled]="canLoadMore" (action)="loadMore($event)" [error]="loadMoreError" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    </core-loading>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <!-- Create a blog entry. -->
 | 
				
			||||||
 | 
					    <ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end" *ngIf="loaded && optionsAvailable">
 | 
				
			||||||
 | 
					        <ion-fab-button (click)="createNewEntry()" [attr.aria-label]="'addon.blog.addnewentry' | translate">
 | 
				
			||||||
 | 
					            <ion-icon name="fas-pen-to-square" aria-hidden="true" />
 | 
				
			||||||
 | 
					            <span class="sr-only">{{ 'addon.blog.addnewentry' | translate }}</span>
 | 
				
			||||||
 | 
					        </ion-fab-button>
 | 
				
			||||||
 | 
					    </ion-fab>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					</ion-content>
 | 
				
			||||||
							
								
								
									
										75
									
								
								src/addons/blog/pages/index/index.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								src/addons/blog/pages/index/index.scss
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,75 @@
 | 
				
			|||||||
 | 
					@use "theme/globals" as *;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					:host {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ion-card {
 | 
				
			||||||
 | 
					        padding: .5rem 1rem;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .entry {
 | 
				
			||||||
 | 
					        border-top: 1px solid var(--stroke);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        &-visibility-permission {
 | 
				
			||||||
 | 
					            display: flex;
 | 
				
			||||||
 | 
					            align-items: center;
 | 
				
			||||||
 | 
					            font-size: 0.875rem;
 | 
				
			||||||
 | 
					            font-weight: 500;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            ion-icon {
 | 
				
			||||||
 | 
					                margin-right: .3rem;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        &-draft {
 | 
				
			||||||
 | 
					            margin-left: .3rem;
 | 
				
			||||||
 | 
					            position: relative;
 | 
				
			||||||
 | 
					            top: 4px;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        &-subject {
 | 
				
			||||||
 | 
					            core-format-text {
 | 
				
			||||||
 | 
					                font-size: 1.25rem;
 | 
				
			||||||
 | 
					                font-weight: 500;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            &::part(native) {
 | 
				
			||||||
 | 
					                padding-left: 0;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        &-creation-info {
 | 
				
			||||||
 | 
					            core-user-avatar {
 | 
				
			||||||
 | 
					                --userpicture-padding: .6rem;
 | 
				
			||||||
 | 
					                margin-left: -.5rem;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        &-last-modification {
 | 
				
			||||||
 | 
					            ion-icon {
 | 
				
			||||||
 | 
					                margin-right: .3rem;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .core-button-spinner {
 | 
				
			||||||
 | 
					        margin-right: -.5rem;
 | 
				
			||||||
 | 
					        align-self: start;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        ion-button::part(native) {
 | 
				
			||||||
 | 
					            --padding-end: 0;
 | 
				
			||||||
 | 
					            --padding-start: 0;
 | 
				
			||||||
 | 
					            --padding-left: 0;
 | 
				
			||||||
 | 
					            --padding-right: 0;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    core-comments ::ng-deep {
 | 
				
			||||||
 | 
					        &::part(native) {
 | 
				
			||||||
 | 
					            --padding-start: 0;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .border-bottom {
 | 
				
			||||||
 | 
					        border-bottom: 1px solid var(--stroke);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -13,29 +13,33 @@
 | 
				
			|||||||
// limitations under the License.
 | 
					// limitations under the License.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { ContextLevel } from '@/core/constants';
 | 
					import { ContextLevel } from '@/core/constants';
 | 
				
			||||||
 | 
					import { AddonBlogEntryOptionsMenuComponent } from '@addons/blog/components/entry-options-menu';
 | 
				
			||||||
 | 
					import { ADDON_BLOG_ENTRY_UPDATED } from '@addons/blog/constants';
 | 
				
			||||||
import { AddonBlog, AddonBlogFilter, AddonBlogPost, AddonBlogProvider } from '@addons/blog/services/blog';
 | 
					import { AddonBlog, AddonBlogFilter, AddonBlogPost, AddonBlogProvider } from '@addons/blog/services/blog';
 | 
				
			||||||
import { Component, OnInit } from '@angular/core';
 | 
					import { Component, OnDestroy, OnInit } from '@angular/core';
 | 
				
			||||||
import { CoreComments } from '@features/comments/services/comments';
 | 
					import { CoreComments } from '@features/comments/services/comments';
 | 
				
			||||||
import { CoreMainMenuDeepLinkManager } from '@features/mainmenu/classes/deep-link-manager';
 | 
					import { CoreMainMenuDeepLinkManager } from '@features/mainmenu/classes/deep-link-manager';
 | 
				
			||||||
import { CoreTag } from '@features/tag/services/tag';
 | 
					import { CoreTag } from '@features/tag/services/tag';
 | 
				
			||||||
import { CoreUser, CoreUserProfile } from '@features/user/services/user';
 | 
					import { CoreUser, CoreUserProfile } from '@features/user/services/user';
 | 
				
			||||||
import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics';
 | 
					import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics';
 | 
				
			||||||
import { CoreNavigator } from '@services/navigator';
 | 
					import { CoreNavigator } from '@services/navigator';
 | 
				
			||||||
import { CoreSites } from '@services/sites';
 | 
					import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
 | 
				
			||||||
import { CoreDomUtils } from '@services/utils/dom';
 | 
					import { CoreDomUtils } from '@services/utils/dom';
 | 
				
			||||||
import { CoreTextUtils } from '@services/utils/text';
 | 
					import { CoreTextUtils } from '@services/utils/text';
 | 
				
			||||||
import { CoreUrlUtils } from '@services/utils/url';
 | 
					import { CoreUrlUtils } from '@services/utils/url';
 | 
				
			||||||
import { CoreUtils } from '@services/utils/utils';
 | 
					import { CoreUtils } from '@services/utils/utils';
 | 
				
			||||||
 | 
					import { CoreEventObserver, CoreEvents } from '@singletons/events';
 | 
				
			||||||
import { CoreTime } from '@singletons/time';
 | 
					import { CoreTime } from '@singletons/time';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Page that displays the list of blog entries.
 | 
					 * Page that displays the list of blog entries.
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
@Component({
 | 
					@Component({
 | 
				
			||||||
    selector: 'page-addon-blog-entries',
 | 
					    selector: 'page-addon-blog-index',
 | 
				
			||||||
    templateUrl: 'entries.html',
 | 
					    templateUrl: 'index.html',
 | 
				
			||||||
 | 
					    styleUrl: './index.scss',
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
export class AddonBlogEntriesPage implements OnInit {
 | 
					export class AddonBlogIndexPage implements OnInit, OnDestroy {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    title = '';
 | 
					    title = '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -59,6 +63,8 @@ export class AddonBlogEntriesPage implements OnInit {
 | 
				
			|||||||
    tagsEnabled = false;
 | 
					    tagsEnabled = false;
 | 
				
			||||||
    contextLevel: ContextLevel = ContextLevel.SYSTEM;
 | 
					    contextLevel: ContextLevel = ContextLevel.SYSTEM;
 | 
				
			||||||
    contextInstanceId = 0;
 | 
					    contextInstanceId = 0;
 | 
				
			||||||
 | 
					    entryUpdateObserver: CoreEventObserver;
 | 
				
			||||||
 | 
					    optionsAvailable = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    constructor() {
 | 
					    constructor() {
 | 
				
			||||||
        this.currentUserId = CoreSites.getCurrentSiteUserId();
 | 
					        this.currentUserId = CoreSites.getCurrentSiteUserId();
 | 
				
			||||||
@ -82,6 +88,12 @@ export class AddonBlogEntriesPage implements OnInit {
 | 
				
			|||||||
                }),
 | 
					                }),
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.entryUpdateObserver = CoreEvents.on(ADDON_BLOG_ENTRY_UPDATED, async () => {
 | 
				
			||||||
 | 
					            this.loaded = false;
 | 
				
			||||||
 | 
					            await CoreUtils.ignoreErrors(this.refresh());
 | 
				
			||||||
 | 
					            this.loaded = true;
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
@ -146,6 +158,7 @@ export class AddonBlogEntriesPage implements OnInit {
 | 
				
			|||||||
        deepLinkManager.treatLink();
 | 
					        deepLinkManager.treatLink();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        await this.fetchEntries();
 | 
					        await this.fetchEntries();
 | 
				
			||||||
 | 
					        this.optionsAvailable = await AddonBlog.isEditingEnabled();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
@ -165,7 +178,15 @@ export class AddonBlogEntriesPage implements OnInit {
 | 
				
			|||||||
        const loadPage = this.onlyMyEntries ? this.userPageLoaded : this.pageLoaded;
 | 
					        const loadPage = this.onlyMyEntries ? this.userPageLoaded : this.pageLoaded;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
            const result = await AddonBlog.getEntries(this.filter, loadPage);
 | 
					            const result = await AddonBlog.getEntries(
 | 
				
			||||||
 | 
					                this.filter,
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    page: loadPage,
 | 
				
			||||||
 | 
					                    readingStrategy: refresh
 | 
				
			||||||
 | 
					                        ? CoreSitesReadingStrategy.PREFER_NETWORK
 | 
				
			||||||
 | 
					                        : undefined,
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            const promises = result.entries.map(async (entry: AddonBlogPostFormatted) => {
 | 
					            const promises = result.entries.map(async (entry: AddonBlogPostFormatted) => {
 | 
				
			||||||
                switch (entry.publishstate) {
 | 
					                switch (entry.publishstate) {
 | 
				
			||||||
@ -271,7 +292,7 @@ export class AddonBlogEntriesPage implements OnInit {
 | 
				
			|||||||
     *
 | 
					     *
 | 
				
			||||||
     * @param refresher Refresher instance.
 | 
					     * @param refresher Refresher instance.
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    refresh(refresher?: HTMLIonRefresherElement): void {
 | 
					    async refresh(refresher?: HTMLIonRefresherElement): Promise<void> {
 | 
				
			||||||
        const promises = this.entries.map((entry) =>
 | 
					        const promises = this.entries.map((entry) =>
 | 
				
			||||||
            CoreComments.invalidateCommentsData(ContextLevel.USER, entry.userid, this.component, entry.id, 'format_blog'));
 | 
					            CoreComments.invalidateCommentsData(ContextLevel.USER, entry.userid, this.component, entry.id, 'format_blog'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -287,13 +308,70 @@ export class AddonBlogEntriesPage implements OnInit {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        CoreUtils.allPromises(promises).finally(() => {
 | 
					        await CoreUtils.allPromises(promises);
 | 
				
			||||||
            this.fetchEntries(true).finally(() => {
 | 
					        await this.fetchEntries(true);
 | 
				
			||||||
                if (refresher) {
 | 
					        refresher?.complete();
 | 
				
			||||||
                    refresher?.complete();
 | 
					    }
 | 
				
			||||||
                }
 | 
					
 | 
				
			||||||
            });
 | 
					    /**
 | 
				
			||||||
 | 
					     * Redirect to entry creation form.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    createNewEntry(): void {
 | 
				
			||||||
 | 
					        CoreNavigator.navigateToSitePath('blog/edit/0');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Delete entry by id.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param id Entry id.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    async deleteEntry(id: number): Promise<void> {
 | 
				
			||||||
 | 
					        const loading = await CoreDomUtils.showModalLoading();
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            await AddonBlog.deleteEntry({ entryid: id });
 | 
				
			||||||
 | 
					            await this.refresh();
 | 
				
			||||||
 | 
					        } catch (error) {
 | 
				
			||||||
 | 
					            CoreDomUtils.showErrorModalDefault(error, 'addon.blog.errorloadentries', true);
 | 
				
			||||||
 | 
					        } finally {
 | 
				
			||||||
 | 
					            loading.dismiss();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Show the context menu.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param event Click Event.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    async showEntryActionsPopover(event: Event, entry: AddonBlogPostFormatted): Promise<void> {
 | 
				
			||||||
 | 
					        event.preventDefault();
 | 
				
			||||||
 | 
					        event.stopPropagation();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const popoverData = await CoreDomUtils.openPopover<string>({
 | 
				
			||||||
 | 
					            component: AddonBlogEntryOptionsMenuComponent,
 | 
				
			||||||
 | 
					            event,
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        switch (popoverData) {
 | 
				
			||||||
 | 
					            case 'edit':
 | 
				
			||||||
 | 
					                await CoreNavigator.navigateToSitePath(`blog/edit/${entry.id}`, {
 | 
				
			||||||
 | 
					                    params: this.filter.cmid
 | 
				
			||||||
 | 
					                        ? { cmId: this.filter.cmid, filters: this.filter, lastModified: entry.lastmodified }
 | 
				
			||||||
 | 
					                        : { filters: this.filter, lastModified: entry.lastmodified },
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
 | 
					            case 'delete':
 | 
				
			||||||
 | 
					                await this.deleteEntry(entry.id);
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
 | 
					            default:
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @inheritdoc
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    ngOnDestroy(): void {
 | 
				
			||||||
 | 
					        this.entryUpdateObserver.off();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -16,7 +16,7 @@ import { Injectable } from '@angular/core';
 | 
				
			|||||||
import { CoreSiteWSPreSets } from '@classes/sites/authenticated-site';
 | 
					import { CoreSiteWSPreSets } from '@classes/sites/authenticated-site';
 | 
				
			||||||
import { CoreSite } from '@classes/sites/site';
 | 
					import { CoreSite } from '@classes/sites/site';
 | 
				
			||||||
import { CoreTagItem } from '@features/tag/services/tag';
 | 
					import { CoreTagItem } from '@features/tag/services/tag';
 | 
				
			||||||
import { CoreSites } from '@services/sites';
 | 
					import { CoreSites, CoreSitesCommonWSOptions } from '@services/sites';
 | 
				
			||||||
import { CoreUtils } from '@services/utils/utils';
 | 
					import { CoreUtils } from '@services/utils/utils';
 | 
				
			||||||
import { CoreStatusWithWarningsWSResponse, CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws';
 | 
					import { CoreStatusWithWarningsWSResponse, CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws';
 | 
				
			||||||
import { makeSingleton } from '@singletons';
 | 
					import { makeSingleton } from '@singletons';
 | 
				
			||||||
@ -62,27 +62,84 @@ export class AddonBlogProvider {
 | 
				
			|||||||
     * Get blog entries.
 | 
					     * Get blog entries.
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     * @param filter Filter to apply on search.
 | 
					     * @param filter Filter to apply on search.
 | 
				
			||||||
     * @param page Page of the blog entries to fetch.
 | 
					     * @param options WS Options.
 | 
				
			||||||
     * @param siteId Site ID. If not defined, current site.
 | 
					 | 
				
			||||||
     * @returns Promise to be resolved when the entries are retrieved.
 | 
					     * @returns Promise to be resolved when the entries are retrieved.
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    async getEntries(filter: AddonBlogFilter = {}, page: number = 0, siteId?: string): Promise<CoreBlogGetEntriesWSResponse> {
 | 
					    async getEntries(filter: AddonBlogFilter = {}, options?: AddonBlogGetEntriesOptions): Promise<CoreBlogGetEntriesWSResponse> {
 | 
				
			||||||
        const site = await CoreSites.getSite(siteId);
 | 
					        const site = await CoreSites.getSite(options?.siteId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const data: CoreBlogGetEntriesWSParams = {
 | 
					        const data: CoreBlogGetEntriesWSParams = {
 | 
				
			||||||
            filters: CoreUtils.objectToArrayOfObjects(filter, 'name', 'value'),
 | 
					            filters: CoreUtils.objectToArrayOfObjects(filter, 'name', 'value'),
 | 
				
			||||||
            page: page,
 | 
					            page: options?.page ?? 0,
 | 
				
			||||||
            perpage: AddonBlogProvider.ENTRIES_PER_PAGE,
 | 
					            perpage: AddonBlogProvider.ENTRIES_PER_PAGE,
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const preSets: CoreSiteWSPreSets = {
 | 
					        const preSets: CoreSiteWSPreSets = {
 | 
				
			||||||
            cacheKey: this.getEntriesCacheKey(filter),
 | 
					            cacheKey: this.getEntriesCacheKey(filter),
 | 
				
			||||||
            updateFrequency: CoreSite.FREQUENCY_SOMETIMES,
 | 
					            updateFrequency: CoreSite.FREQUENCY_SOMETIMES,
 | 
				
			||||||
 | 
					            ...CoreSites.getReadingStrategyPreSets(options?.readingStrategy),
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return site.read('core_blog_get_entries', data, preSets);
 | 
					        return site.read('core_blog_get_entries', data, preSets);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Create a new entry.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param params WS Params.
 | 
				
			||||||
 | 
					     * @param siteId Site ID where the entry should be created.
 | 
				
			||||||
 | 
					     * @returns Entry id.
 | 
				
			||||||
 | 
					     * @since 4.4
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    async addEntry(params: AddonBlogAddEntryWSParams, siteId?: string): Promise<number> {
 | 
				
			||||||
 | 
					        const site = await CoreSites.getSite(siteId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return await site.write<number>('core_blog_add_entry', params);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Update an entry.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param params WS Params.
 | 
				
			||||||
 | 
					     * @param siteId Site ID of the entry.
 | 
				
			||||||
 | 
					     * @since 4.4
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    async updateEntry(params: AddonBlogUpdateEntryWSParams, siteId?: string): Promise<void> {
 | 
				
			||||||
 | 
					        const site = await CoreSites.getSite(siteId);
 | 
				
			||||||
 | 
					        await site.write('core_blog_update_entry', params);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Prepare entry for edition by entry id.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param params WS Params.
 | 
				
			||||||
 | 
					     * @param siteId Site ID of the entry.
 | 
				
			||||||
 | 
					     * @returns WS Response
 | 
				
			||||||
 | 
					     * @since 4.4
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    async prepareEntryForEdition(
 | 
				
			||||||
 | 
					        params: AddonBlogPrepareEntryForEditionWSParams,
 | 
				
			||||||
 | 
					        siteId?: string,
 | 
				
			||||||
 | 
					    ): Promise<AddonBlogPrepareEntryForEditionWSResponse> {
 | 
				
			||||||
 | 
					        const site = await CoreSites.getSite(siteId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return await site.write<AddonBlogPrepareEntryForEditionWSResponse>('core_blog_prepare_entry_for_edition', params);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Delete entry by id.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param params WS params.
 | 
				
			||||||
 | 
					     * @param siteId Site ID of the entry.
 | 
				
			||||||
 | 
					     * @returns Entry deleted successfully or not.
 | 
				
			||||||
 | 
					     * @since 4.4
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    async deleteEntry(params: AddonBlogDeleteEntryWSParams, siteId?: string): Promise<AddonBlogDeleteEntryWSResponse> {
 | 
				
			||||||
 | 
					        const site = await CoreSites.getSite(siteId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return await site.write('core_blog_delete_entry', params);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Invalidate blog entries WS call.
 | 
					     * Invalidate blog entries WS call.
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
@ -96,6 +153,19 @@ export class AddonBlogProvider {
 | 
				
			|||||||
        await site.invalidateWsCacheForKey(this.getEntriesCacheKey(filter));
 | 
					        await site.invalidateWsCacheForKey(this.getEntriesCacheKey(filter));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Is editing blog entry enabled.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param siteId Site ID.
 | 
				
			||||||
 | 
					     * @returns is enabled or not.
 | 
				
			||||||
 | 
					     * @since 4.4
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    async isEditingEnabled(siteId?: string): Promise<boolean> {
 | 
				
			||||||
 | 
					        const site = await CoreSites.getSite(siteId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return site.wsAvailable('core_blog_update_entry');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Trigger the blog_entries_viewed event.
 | 
					     * Trigger the blog_entries_viewed event.
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
@ -164,7 +234,7 @@ export type AddonBlogPost = {
 | 
				
			|||||||
    rating: number; // Post rating.
 | 
					    rating: number; // Post rating.
 | 
				
			||||||
    format: number; // Post content format.
 | 
					    format: number; // Post content format.
 | 
				
			||||||
    attachment: string; // Post atachment.
 | 
					    attachment: string; // Post atachment.
 | 
				
			||||||
    publishstate: string; // Post publish state.
 | 
					    publishstate: AddonBlogPublishState; // Post publish state.
 | 
				
			||||||
    lastmodified: number; // When it was last modified.
 | 
					    lastmodified: number; // When it was last modified.
 | 
				
			||||||
    created: number; // When it was created.
 | 
					    created: number; // When it was created.
 | 
				
			||||||
    usermodified: number; // User that updated the post.
 | 
					    usermodified: number; // User that updated the post.
 | 
				
			||||||
@ -201,3 +271,70 @@ export type AddonBlogFilter = {
 | 
				
			|||||||
    courseid?: number; // Course id
 | 
					    courseid?: number; // Course id
 | 
				
			||||||
    search?: string;   // Search term.
 | 
					    search?: string;   // Search term.
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * core_blog_add_entry & core_blog_update_entry ws params.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export type AddonBlogAddEntryWSParams = {
 | 
				
			||||||
 | 
					    subject: string;
 | 
				
			||||||
 | 
					    summary: string;
 | 
				
			||||||
 | 
					    summaryformat: number;
 | 
				
			||||||
 | 
					    options: AddonBlogAddEntryOption[];
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type AddonBlogUpdateEntryWSParams = AddonBlogAddEntryWSParams & { entryid: number };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Add entry options.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export type AddonBlogAddEntryOption = {
 | 
				
			||||||
 | 
					    name: 'inlineattachmentsid' | 'attachmentsid' | 'publishstate' | 'courseassoc' | 'modassoc' | 'tags';
 | 
				
			||||||
 | 
					    value: string | number;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * core_blog_prepare_entry_for_edition ws params.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export type AddonBlogPrepareEntryForEditionWSResponse = {
 | 
				
			||||||
 | 
					    inlineattachmentsid: number;
 | 
				
			||||||
 | 
					    attachmentsid: number;
 | 
				
			||||||
 | 
					    areas: AddonBlogPrepareEntryForEditionArea[];
 | 
				
			||||||
 | 
					    warnings: string[];
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type AddonBlogPrepareEntryForEditionWSParams = {
 | 
				
			||||||
 | 
					    entryid: number;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * core_blog_prepare_entry_for_edition Area object.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export type AddonBlogPrepareEntryForEditionArea = {
 | 
				
			||||||
 | 
					    area: string;
 | 
				
			||||||
 | 
					    options: AddonBlogPrepareEntryForEditionOption[];
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * core_blog_prepare_entry_for_edition Option object.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export type AddonBlogPrepareEntryForEditionOption = {
 | 
				
			||||||
 | 
					    name: string;
 | 
				
			||||||
 | 
					    value: unknown;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type AddonBlogDeleteEntryWSParams = {
 | 
				
			||||||
 | 
					    entryid: number;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type AddonBlogDeleteEntryWSResponse = {
 | 
				
			||||||
 | 
					    status: boolean; // Status: true only if we set the policyagreed to 1 for the user.
 | 
				
			||||||
 | 
					    warnings?: CoreWSExternalWarning[];
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type AddonBlogGetEntriesOptions = CoreSitesCommonWSOptions & {
 | 
				
			||||||
 | 
					    page?: number;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const ADDON_BLOG_PUBLISH_STATE = { draft: 'draft', site: 'site', public: 'public' } as const;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type AddonBlogPublishState = typeof ADDON_BLOG_PUBLISH_STATE[keyof typeof ADDON_BLOG_PUBLISH_STATE];
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										57
									
								
								src/addons/blog/services/handlers/edit-entry-link.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								src/addons/blog/services/handlers/edit-entry-link.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,57 @@
 | 
				
			|||||||
 | 
					// (C) Copyright 2015 Moodle Pty Ltd.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					// you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					// You may obtain a copy of the License at
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					//     http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					// distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					// See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					// limitations under the License.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { Injectable } from '@angular/core';
 | 
				
			||||||
 | 
					import { Params } from '@angular/router';
 | 
				
			||||||
 | 
					import { CoreContentLinksHandlerBase } from '@features/contentlinks/classes/base-handler';
 | 
				
			||||||
 | 
					import { CoreContentLinksAction } from '@features/contentlinks/services/contentlinks-delegate';
 | 
				
			||||||
 | 
					import { CoreNavigator } from '@services/navigator';
 | 
				
			||||||
 | 
					import { makeSingleton } from '@singletons';
 | 
				
			||||||
 | 
					import { AddonBlog } from '../blog';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Handler to treat links to edit blog entry page.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					@Injectable({ providedIn: 'root' })
 | 
				
			||||||
 | 
					export class AddonBlogEditEntryLinkHandlerService extends CoreContentLinksHandlerBase {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    name = 'AddonBlogEditEntryLinkHandler';
 | 
				
			||||||
 | 
					    featureName = 'CoreUserDelegate_AddonBlog:blogs';
 | 
				
			||||||
 | 
					    pattern = /\/blog\/(add|edit)\.php/;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @inheritdoc
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    getActions(siteIds: string[], url: string, params: Record<string, string>): CoreContentLinksAction[] {
 | 
				
			||||||
 | 
					        const pageParams: Params = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        pageParams.courseId = params.courseid;
 | 
				
			||||||
 | 
					        pageParams.cmId = params.modid;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return [{
 | 
				
			||||||
 | 
					            action: async (siteId: string): Promise<void> => {
 | 
				
			||||||
 | 
					                await CoreNavigator.navigateToSitePath(`/blog/edit/${params.entryid ?? 0}`, { params: pageParams, siteId });
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        }];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @inheritdoc
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    isEnabled(siteId: string): Promise<boolean> {
 | 
				
			||||||
 | 
					        return AddonBlog.isPluginEnabled(siteId);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					export const AddonBlogEditEntryLinkHandler = makeSingleton(AddonBlogEditEntryLinkHandlerService);
 | 
				
			||||||
@ -73,7 +73,7 @@ export class AddonBlogUserHandlerService implements CoreUserProfileHandler {
 | 
				
			|||||||
            action: (event, user, context, contextId): void => {
 | 
					            action: (event, user, context, contextId): void => {
 | 
				
			||||||
                event.preventDefault();
 | 
					                event.preventDefault();
 | 
				
			||||||
                event.stopPropagation();
 | 
					                event.stopPropagation();
 | 
				
			||||||
                CoreNavigator.navigateToSitePath('/blog', {
 | 
					                CoreNavigator.navigateToSitePath('/blog/index', {
 | 
				
			||||||
                    params: { courseId: contextId, userId: user.id },
 | 
					                    params: { courseId: contextId, userId: user.id },
 | 
				
			||||||
                });
 | 
					                });
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										68
									
								
								src/addons/blog/tests/behat/edit-entry.feature
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								src/addons/blog/tests/behat/edit-entry.feature
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,68 @@
 | 
				
			|||||||
 | 
					@addon_blog @core_blog @app @javascript @lms_from4.4
 | 
				
			||||||
 | 
					Feature: Edit blog entries
 | 
				
			||||||
 | 
					  In order to add or edit blog entries as User
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Background:
 | 
				
			||||||
 | 
					    Given the following "users" exist:
 | 
				
			||||||
 | 
					      | username | firstname | lastname | email              |
 | 
				
			||||||
 | 
					      | testuser  | Test      | User     | moodle@example.com |
 | 
				
			||||||
 | 
					      | testuser2 | Test      | User2    | moodle@example.com |
 | 
				
			||||||
 | 
					    And the following "core_blog > entries" exist:
 | 
				
			||||||
 | 
					      | subject       | body                     | user     |
 | 
				
			||||||
 | 
					      | Blog post one | User 1 blog post content | testuser |
 | 
				
			||||||
 | 
					      | Blog post two | User 1 blog post content | testuser |
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Scenario: Edit blog entry
 | 
				
			||||||
 | 
					    Given I entered the app as "testuser"
 | 
				
			||||||
 | 
					    When I press the user menu button in the app
 | 
				
			||||||
 | 
					    And I press "Blog entries" in the app
 | 
				
			||||||
 | 
					    Then I should find "Blog post one" in the app
 | 
				
			||||||
 | 
					    And I press "Display options" in the app
 | 
				
			||||||
 | 
					    And I press "Edit" in the app
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Then I should find "Blog post one" in the app
 | 
				
			||||||
 | 
					    And I set the field "Entry title" to "Blog post one (updated)" in the app
 | 
				
			||||||
 | 
					    And I set the field "Blog entry body" to "User 1 blog post content (updated)" in the app
 | 
				
			||||||
 | 
					    And I press "Publish to" in the app
 | 
				
			||||||
 | 
					    And I press "Yourself (draft)" in the app
 | 
				
			||||||
 | 
					    And I press "Save" in the app
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Then I should find "Blog post one (updated)" in the app
 | 
				
			||||||
 | 
					    And I should find "User 1 blog post content (updated)" in the app
 | 
				
			||||||
 | 
					    And I should find "Yourself (draft)" near "User 1 blog post content (updated)" in the app
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Scenario: Add a blog entry
 | 
				
			||||||
 | 
					    Given I entered the app as "testuser"
 | 
				
			||||||
 | 
					    When I press the user menu button in the app
 | 
				
			||||||
 | 
					    And I press "Blog entries" in the app
 | 
				
			||||||
 | 
					    And I press "Add a new entry" in the app
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    And I set the field "Entry title" to "New blog entry" in the app
 | 
				
			||||||
 | 
					    And I set the field "Blog entry body" to "This is a new blog entry." in the app
 | 
				
			||||||
 | 
					    And I press "Publish to" in the app
 | 
				
			||||||
 | 
					    And I press "Anyone on this site" in the app
 | 
				
			||||||
 | 
					    And I press "Add a new entry" "button" in the app
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Then I should find "Blog entries" in the app
 | 
				
			||||||
 | 
					    And I should find "New blog entry" in the app
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Scenario: Add a blog entry with attachments
 | 
				
			||||||
 | 
					    Given I entered the app as "testuser"
 | 
				
			||||||
 | 
					    When I press the user menu button in the app
 | 
				
			||||||
 | 
					    And I press "Blog entries" in the app
 | 
				
			||||||
 | 
					    And I press "Add a new entry" in the app
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    And I set the field "Entry title" to "Entry with attachments" in the app
 | 
				
			||||||
 | 
					    And I set the field "Blog entry body" to "This is a new blog entry with attachments." in the app
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    And I press "Add file" in the app
 | 
				
			||||||
 | 
					    And I upload "stub6.txt" to "File" ".action-sheet-button" in the app
 | 
				
			||||||
 | 
					    And I press "Add file" in the app
 | 
				
			||||||
 | 
					    And I upload "stub7.txt" to "File" ".action-sheet-button" in the app
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    And I press "Add a new entry" "button" in the app
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Then I should find "Blog entries" in the app
 | 
				
			||||||
 | 
					    And I should find "Entry with attachments" in the app
 | 
				
			||||||
 | 
					    And I should find "stub6.txt" in the app
 | 
				
			||||||
 | 
					    And I should find "stub7.txt" in the app
 | 
				
			||||||
							
								
								
									
										30
									
								
								src/addons/blog/tests/behat/entries.feature
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/addons/blog/tests/behat/entries.feature
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,30 @@
 | 
				
			|||||||
 | 
					@addon_blog @core_blog @app @javascript @lms_from4.4
 | 
				
			||||||
 | 
					Feature: Blog entries
 | 
				
			||||||
 | 
					  In order to modify or delete a blog entry
 | 
				
			||||||
 | 
					  As a user
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Background:
 | 
				
			||||||
 | 
					    Given the following "users" exist:
 | 
				
			||||||
 | 
					      | username | firstname | lastname | email              |
 | 
				
			||||||
 | 
					      | testuser | Test      | User     | moodle@example.com |
 | 
				
			||||||
 | 
					    And the following "core_blog > entries" exist:
 | 
				
			||||||
 | 
					      | subject       | body                     | user     |
 | 
				
			||||||
 | 
					      | Blog post one | User 1 blog post content | testuser |
 | 
				
			||||||
 | 
					      | Blog post two | User 1 blog post content | testuser |
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Scenario: List every blog entry
 | 
				
			||||||
 | 
					    Given I entered the app as "testuser"
 | 
				
			||||||
 | 
					    When I press the user menu button in the app
 | 
				
			||||||
 | 
					    And I press "Blog entries" in the app
 | 
				
			||||||
 | 
					    Then I should find "Blog post one" in the app
 | 
				
			||||||
 | 
					    And I should find "Blog post two" in the app
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Scenario: Delete blog entry
 | 
				
			||||||
 | 
					    Given I entered the app as "testuser"
 | 
				
			||||||
 | 
					    When I press the user menu button in the app
 | 
				
			||||||
 | 
					    And I press "Blog entries" in the app
 | 
				
			||||||
 | 
					    Then I should find "Blog post one" in the app
 | 
				
			||||||
 | 
					    When I press "Display options" near "Blog post one" in the app
 | 
				
			||||||
 | 
					    And I press "Delete" in the app
 | 
				
			||||||
 | 
					    And I pull to refresh in the app
 | 
				
			||||||
 | 
					    And I should not find "Blog post one" in the app
 | 
				
			||||||
							
								
								
									
										1
									
								
								src/addons/blog/tests/behat/fixtures/stub6.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/addons/blog/tests/behat/fixtures/stub6.txt
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					This is the stub file 6 created at 13/03/2025.
 | 
				
			||||||
							
								
								
									
										1
									
								
								src/addons/blog/tests/behat/fixtures/stub7.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/addons/blog/tests/behat/fixtures/stub7.txt
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					This is the stub file 7 created at 13/03/2025.
 | 
				
			||||||
@ -12,6 +12,7 @@
 | 
				
			|||||||
    "areyousure": "Are you sure?",
 | 
					    "areyousure": "Are you sure?",
 | 
				
			||||||
    "back": "Back",
 | 
					    "back": "Back",
 | 
				
			||||||
    "browser": "Browser",
 | 
					    "browser": "Browser",
 | 
				
			||||||
 | 
					    "bynameondate": "by {{$a.name}} - {{$a.date}}",
 | 
				
			||||||
    "calculating": "Calculating",
 | 
					    "calculating": "Calculating",
 | 
				
			||||||
    "cancel": "Cancel",
 | 
					    "cancel": "Cancel",
 | 
				
			||||||
    "cannotconnect": "Can't connect to site",
 | 
					    "cannotconnect": "Can't connect to site",
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user