Merge pull request #3956 from alfonso-salces/MOBILE-4219
MOBILE-4219 blog: Add and edit blog entriesmain
commit
8dfd363d6f
|
@ -86,6 +86,7 @@ jobs:
|
|||
"@addon_mod_survey"
|
||||
"@addon_mod_workshop"
|
||||
"@addon_notifications"
|
||||
"@addon_blog"
|
||||
"@core"
|
||||
"@core_comments"
|
||||
"@core_course"
|
||||
|
|
|
@ -85,16 +85,25 @@
|
|||
"addon.block_timeline.searchevents": "block_timeline",
|
||||
"addon.block_timeline.sortbycourses": "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.blogentries": "blog",
|
||||
"addon.blog.entrybody": "blog",
|
||||
"addon.blog.entrytitle": "blog",
|
||||
"addon.blog.errorloadentries": "local_moodlemobileapp",
|
||||
"addon.blog.linktooriginalentry": "blog",
|
||||
"addon.blog.noentriesyet": "blog",
|
||||
"addon.blog.publishto": "blog",
|
||||
"addon.blog.publishtonoone": "blog",
|
||||
"addon.blog.publishtosite": "blog",
|
||||
"addon.blog.publishtoworld": "blog",
|
||||
"addon.blog.showonlyyourentries": "local_moodlemobileapp",
|
||||
"addon.blog.siteblogheading": "blog",
|
||||
"addon.blog.tags": "blog",
|
||||
"addon.calendar.allday": "calendar",
|
||||
"addon.calendar.calendar": "calendar",
|
||||
"addon.calendar.calendarevent": "local_moodlemobileapp",
|
||||
|
@ -1500,6 +1509,7 @@
|
|||
"core.block.tour_navigation_dashboard_content": "tool_usertours",
|
||||
"core.block.tour_navigation_dashboard_title": "tool_usertours",
|
||||
"core.browser": "local_moodlemobileapp",
|
||||
"core.bynameondate": "forum",
|
||||
"core.calculating": "local_moodlemobileapp",
|
||||
"core.cancel": "moodle",
|
||||
"core.cannotconnect": "local_moodlemobileapp",
|
||||
|
|
|
@ -16,13 +16,15 @@ import { Injector, NgModule } from '@angular/core';
|
|||
import { ROUTES, Routes } from '@angular/router';
|
||||
|
||||
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 { 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 { ADDON_BLOG_MAINMENU_PAGE_NAME } from './constants';
|
||||
import { CoreMainMenuComponentsModule } from '@features/mainmenu/components/components.module';
|
||||
import { canLeaveGuard } from '@guards/can-leave';
|
||||
|
||||
/**
|
||||
* Build module routes.
|
||||
|
@ -30,13 +32,23 @@ import { CoreMainMenuComponentsModule } from '@features/mainmenu/components/comp
|
|||
* @param injector Injector.
|
||||
* @returns Routes.
|
||||
*/
|
||||
function buildRoutes(injector: Injector): Routes {
|
||||
function buildRoutes(injector: Injector): Routes {
|
||||
return [
|
||||
...buildTabMainRoutes(injector, {
|
||||
{
|
||||
path: 'index',
|
||||
component: AddonBlogIndexPage,
|
||||
data: {
|
||||
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,
|
||||
CoreMainMenuComponentsModule,
|
||||
],
|
||||
declarations: [
|
||||
AddonBlogIndexPage,
|
||||
AddonBlogEntryOptionsMenuComponent,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: ROUTES,
|
||||
|
@ -56,8 +72,5 @@ function buildRoutes(injector: Injector): Routes {
|
|||
useFactory: buildRoutes,
|
||||
},
|
||||
],
|
||||
declarations: [
|
||||
AddonBlogEntriesPage,
|
||||
],
|
||||
})
|
||||
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 { CoreUserDelegate } from '@features/user/services/user-delegate';
|
||||
import { AddonBlogCourseOptionHandler } from './services/handlers/course-option';
|
||||
import { AddonBlogEditEntryLinkHandler } from './services/handlers/edit-entry-link';
|
||||
import { AddonBlogIndexLinkHandler } from './services/handlers/index-link';
|
||||
import { AddonBlogMainMenuHandler } from './services/handlers/mainmenu';
|
||||
import { AddonBlogTagAreaHandler } from './services/handlers/tag-area';
|
||||
|
@ -48,6 +49,7 @@ const routes: Routes = [
|
|||
multi: true,
|
||||
useValue: () => {
|
||||
CoreContentLinksDelegate.registerHandler(AddonBlogIndexLinkHandler.instance);
|
||||
CoreContentLinksDelegate.registerHandler(AddonBlogEditEntryLinkHandler.instance);
|
||||
CoreMainMenuDelegate.registerHandler(AddonBlogMainMenuHandler.instance);
|
||||
CoreUserDelegate.registerHandler(AddonBlogUserHandler.instance);
|
||||
CoreTagAreaDelegate.registerHandler(AddonBlogTagAreaHandler.instance);
|
||||
|
|
|
@ -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>
|
|
@ -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.
|
||||
|
||||
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",
|
||||
"blogentries": "Blog entries",
|
||||
"entrybody": "Blog entry body",
|
||||
"entrytitle": "Entry title",
|
||||
"errorloadentries": "Error loading blog entries.",
|
||||
"linktooriginalentry": "Link to original blog entry",
|
||||
"noentriesyet": "No visible entries here",
|
||||
"publishto": "Publish to",
|
||||
"publishtonoone": "Yourself (draft)",
|
||||
"publishtosite": "Anyone on this site",
|
||||
"publishtoworld": "Anyone in the world",
|
||||
"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}}"
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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.
|
||||
|
||||
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 { Component, OnInit } from '@angular/core';
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { CoreComments } from '@features/comments/services/comments';
|
||||
import { CoreMainMenuDeepLinkManager } from '@features/mainmenu/classes/deep-link-manager';
|
||||
import { CoreTag } from '@features/tag/services/tag';
|
||||
import { CoreUser, CoreUserProfile } from '@features/user/services/user';
|
||||
import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreTextUtils } from '@services/utils/text';
|
||||
import { CoreUrlUtils } from '@services/utils/url';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||
import { CoreTime } from '@singletons/time';
|
||||
|
||||
/**
|
||||
* Page that displays the list of blog entries.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'page-addon-blog-entries',
|
||||
templateUrl: 'entries.html',
|
||||
selector: 'page-addon-blog-index',
|
||||
templateUrl: 'index.html',
|
||||
styleUrl: './index.scss',
|
||||
})
|
||||
export class AddonBlogEntriesPage implements OnInit {
|
||||
export class AddonBlogIndexPage implements OnInit, OnDestroy {
|
||||
|
||||
title = '';
|
||||
|
||||
|
@ -59,6 +63,8 @@ export class AddonBlogEntriesPage implements OnInit {
|
|||
tagsEnabled = false;
|
||||
contextLevel: ContextLevel = ContextLevel.SYSTEM;
|
||||
contextInstanceId = 0;
|
||||
entryUpdateObserver: CoreEventObserver;
|
||||
optionsAvailable = false;
|
||||
|
||||
constructor() {
|
||||
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();
|
||||
|
||||
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;
|
||||
|
||||
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) => {
|
||||
switch (entry.publishstate) {
|
||||
|
@ -271,7 +292,7 @@ export class AddonBlogEntriesPage implements OnInit {
|
|||
*
|
||||
* @param refresher Refresher instance.
|
||||
*/
|
||||
refresh(refresher?: HTMLIonRefresherElement): void {
|
||||
async refresh(refresher?: HTMLIonRefresherElement): Promise<void> {
|
||||
const promises = this.entries.map((entry) =>
|
||||
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(() => {
|
||||
this.fetchEntries(true).finally(() => {
|
||||
if (refresher) {
|
||||
await CoreUtils.allPromises(promises);
|
||||
await this.fetchEntries(true);
|
||||
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 { CoreSite } from '@classes/sites/site';
|
||||
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 { CoreStatusWithWarningsWSResponse, CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws';
|
||||
import { makeSingleton } from '@singletons';
|
||||
|
@ -62,27 +62,84 @@ export class AddonBlogProvider {
|
|||
* Get blog entries.
|
||||
*
|
||||
* @param filter Filter to apply on search.
|
||||
* @param page Page of the blog entries to fetch.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param options WS Options.
|
||||
* @returns Promise to be resolved when the entries are retrieved.
|
||||
*/
|
||||
async getEntries(filter: AddonBlogFilter = {}, page: number = 0, siteId?: string): Promise<CoreBlogGetEntriesWSResponse> {
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
async getEntries(filter: AddonBlogFilter = {}, options?: AddonBlogGetEntriesOptions): Promise<CoreBlogGetEntriesWSResponse> {
|
||||
const site = await CoreSites.getSite(options?.siteId);
|
||||
|
||||
const data: CoreBlogGetEntriesWSParams = {
|
||||
filters: CoreUtils.objectToArrayOfObjects(filter, 'name', 'value'),
|
||||
page: page,
|
||||
page: options?.page ?? 0,
|
||||
perpage: AddonBlogProvider.ENTRIES_PER_PAGE,
|
||||
};
|
||||
|
||||
const preSets: CoreSiteWSPreSets = {
|
||||
cacheKey: this.getEntriesCacheKey(filter),
|
||||
updateFrequency: CoreSite.FREQUENCY_SOMETIMES,
|
||||
...CoreSites.getReadingStrategyPreSets(options?.readingStrategy),
|
||||
};
|
||||
|
||||
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.
|
||||
*
|
||||
|
@ -96,6 +153,19 @@ export class AddonBlogProvider {
|
|||
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.
|
||||
*
|
||||
|
@ -164,7 +234,7 @@ export type AddonBlogPost = {
|
|||
rating: number; // Post rating.
|
||||
format: number; // Post content format.
|
||||
attachment: string; // Post atachment.
|
||||
publishstate: string; // Post publish state.
|
||||
publishstate: AddonBlogPublishState; // Post publish state.
|
||||
lastmodified: number; // When it was last modified.
|
||||
created: number; // When it was created.
|
||||
usermodified: number; // User that updated the post.
|
||||
|
@ -201,3 +271,70 @@ export type AddonBlogFilter = {
|
|||
courseid?: number; // Course id
|
||||
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];
|
||||
|
|
|
@ -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 => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
CoreNavigator.navigateToSitePath('/blog', {
|
||||
CoreNavigator.navigateToSitePath('/blog/index', {
|
||||
params: { courseId: contextId, userId: user.id },
|
||||
});
|
||||
},
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
This is the stub file 6 created at 13/03/2025.
|
|
@ -0,0 +1 @@
|
|||
This is the stub file 7 created at 13/03/2025.
|
|
@ -12,6 +12,7 @@
|
|||
"areyousure": "Are you sure?",
|
||||
"back": "Back",
|
||||
"browser": "Browser",
|
||||
"bynameondate": "by {{$a.name}} - {{$a.date}}",
|
||||
"calculating": "Calculating",
|
||||
"cancel": "Cancel",
|
||||
"cannotconnect": "Can't connect to site",
|
||||
|
|
Loading…
Reference in New Issue