Merge pull request #4043 from alfonso-salces/MOBILE-4547
MOBILE-4547 blog: Support offline blogmain
commit
660636d96f
|
@ -29,6 +29,10 @@ import { AddonBlogMainMenuHandler } from './services/handlers/mainmenu';
|
|||
import { AddonBlogTagAreaHandler } from './services/handlers/tag-area';
|
||||
import { AddonBlogUserHandler } from './services/handlers/user';
|
||||
import { ADDON_BLOG_MAINMENU_PAGE_NAME } from './constants';
|
||||
import { CORE_SITE_SCHEMAS } from '@services/sites';
|
||||
import { BLOG_OFFLINE_SITE_SCHEMA } from './services/database/blog';
|
||||
import { CoreCronDelegate } from '@services/cron';
|
||||
import { AddonBlogSyncCronHandler } from './services/handlers/sync-cron';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
|
@ -44,6 +48,11 @@ const routes: Routes = [
|
|||
CoreCourseIndexRoutingModule.forChild({ children: routes }),
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: CORE_SITE_SCHEMAS,
|
||||
useValue: [BLOG_OFFLINE_SITE_SCHEMA],
|
||||
multi: true,
|
||||
},
|
||||
{
|
||||
provide: APP_INITIALIZER,
|
||||
multi: true,
|
||||
|
@ -54,6 +63,7 @@ const routes: Routes = [
|
|||
CoreUserDelegate.registerHandler(AddonBlogUserHandler.instance);
|
||||
CoreTagAreaDelegate.registerHandler(AddonBlogTagAreaHandler.instance);
|
||||
CoreCourseOptionsDelegate.registerHandler(AddonBlogCourseOptionHandler.instance);
|
||||
CoreCronDelegate.register(AddonBlogSyncCronHandler.instance);
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
|
@ -14,3 +14,6 @@
|
|||
|
||||
export const ADDON_BLOG_MAINMENU_PAGE_NAME = 'blog';
|
||||
export const ADDON_BLOG_ENTRY_UPDATED = 'blog_entry_updated';
|
||||
export const ADDON_BLOG_AUTO_SYNCED = 'addon_blog_autom_synced';
|
||||
export const ADDON_BLOG_MANUAL_SYNCED = 'addon_blog_manual_synced';
|
||||
export const ADDON_BLOG_SYNC_ID = 'blog';
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
// 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 { ADDON_BLOG_ENTRY_UPDATED, ADDON_BLOG_SYNC_ID } from '@addons/blog/constants';
|
||||
import {
|
||||
AddonBlog,
|
||||
AddonBlogAddEntryOption,
|
||||
|
@ -22,7 +22,9 @@ import {
|
|||
AddonBlogProvider,
|
||||
AddonBlogPublishState,
|
||||
} from '@addons/blog/services/blog';
|
||||
import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
|
||||
import { AddonBlogOffline } from '@addons/blog/services/blog-offline';
|
||||
import { Component, ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
||||
import { AddonBlogSync } from '@addons/blog/services/blog-sync';
|
||||
import { FormControl, FormGroup, Validators } from '@angular/forms';
|
||||
import { CoreError } from '@classes/errors/error';
|
||||
import { CoreCommentsComponentsModule } from '@features/comments/components/components.module';
|
||||
|
@ -30,18 +32,21 @@ 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 { CoreFileUploader, CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader';
|
||||
import { CoreTagComponentsModule } from '@features/tag/components/components.module';
|
||||
import { CanLeave } from '@guards/can-leave';
|
||||
import { CoreLoadings } from '@services/loadings';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
import { CoreNetwork } from '@services/network';
|
||||
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
|
||||
import { CoreSync } from '@services/sync';
|
||||
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';
|
||||
import { CoreForms } from '@singletons/form';
|
||||
import { CoreFileEntry } from '@services/file-helper';
|
||||
import { CoreTimeUtils } from '@services/utils/time';
|
||||
|
||||
@Component({
|
||||
selector: 'addon-blog-edit-entry',
|
||||
|
@ -54,7 +59,7 @@ import { CoreForms } from '@singletons/form';
|
|||
CoreTagComponentsModule,
|
||||
],
|
||||
})
|
||||
export class AddonBlogEditEntryPage implements CanLeave, OnInit {
|
||||
export class AddonBlogEditEntryPage implements CanLeave, OnInit, OnDestroy {
|
||||
|
||||
@ViewChild('editEntryForm') formElement!: ElementRef;
|
||||
|
||||
|
@ -70,11 +75,11 @@ export class AddonBlogEditEntryPage implements CanLeave, OnInit {
|
|||
associateWithModule: new FormControl<boolean>(false, { nonNullable: true, validators: [Validators.required] }),
|
||||
});
|
||||
|
||||
entry?: AddonBlogPost;
|
||||
entry?: AddonBlogPost | AddonBlogEditEntryFormattedOfflinePost;
|
||||
loaded = false;
|
||||
maxFiles = 99;
|
||||
initialFiles: CoreWSFile[] = [];
|
||||
files: CoreWSFile[] = [];
|
||||
initialFiles: CoreFileEntry[] = [];
|
||||
files: CoreFileEntry[] = [];
|
||||
courseId?: number;
|
||||
modId?: number;
|
||||
userId?: number;
|
||||
|
@ -88,6 +93,7 @@ export class AddonBlogEditEntryPage implements CanLeave, OnInit {
|
|||
component = AddonBlogProvider.COMPONENT;
|
||||
siteHomeId?: number;
|
||||
forceLeave = false;
|
||||
isOfflineEntry = false;
|
||||
|
||||
/**
|
||||
* Gives if the form is not pristine. (only for existing entries)
|
||||
|
@ -130,15 +136,17 @@ export class AddonBlogEditEntryPage implements CanLeave, OnInit {
|
|||
return CoreNavigator.back();
|
||||
}
|
||||
|
||||
const entryId = CoreNavigator.getRouteNumberParam('id');
|
||||
const entryId = CoreNavigator.getRouteParam('id');
|
||||
const lastModified = CoreNavigator.getRouteNumberParam('lastModified');
|
||||
const filters: AddonBlogFilter | undefined = CoreNavigator.getRouteParam('filters');
|
||||
const courseId = CoreNavigator.getRouteNumberParam('courseId');
|
||||
const cmId = CoreNavigator.getRouteNumberParam('cmId');
|
||||
this.userId = CoreNavigator.getRouteNumberParam('userId');
|
||||
this.siteHomeId = CoreSites.getCurrentSiteHomeId();
|
||||
this.isOfflineEntry = entryId?.startsWith('new-') ?? false;
|
||||
const entryIdParsed = Number(entryId);
|
||||
|
||||
if (!entryId) {
|
||||
if (entryIdParsed === 0) {
|
||||
this.loaded = true;
|
||||
|
||||
try {
|
||||
|
@ -162,11 +170,27 @@ export class AddonBlogEditEntryPage implements CanLeave, OnInit {
|
|||
}
|
||||
|
||||
try {
|
||||
this.entry = await this.getEntry({ filters, lastModified, entryId });
|
||||
this.files = this.entry.attachmentfiles ?? [];
|
||||
await AddonBlogSync.waitForSync(ADDON_BLOG_SYNC_ID);
|
||||
|
||||
if (!this.isOfflineEntry) {
|
||||
const offlineContent = await this.getFormattedBlogOfflineEntry({ id: entryIdParsed });
|
||||
this.entry = offlineContent ?? await this.getEntry({ filters, lastModified, entryId: entryIdParsed });
|
||||
} else {
|
||||
this.entry = await this.getFormattedBlogOfflineEntry({ created: Number(entryId?.slice(4)) });
|
||||
|
||||
if (!this.entry) {
|
||||
throw new CoreError('This offline entry no longer exists.');
|
||||
}
|
||||
}
|
||||
|
||||
this.files = [...(this.entry.attachmentfiles ?? [])];
|
||||
this.initialFiles = [...this.files];
|
||||
this.courseId = this.courseId || this.entry.courseid;
|
||||
this.modId = CoreNavigator.getRouteNumberParam('cmId') || this.entry.coursemoduleid;
|
||||
|
||||
if (this.entry) {
|
||||
CoreSync.blockOperation(AddonBlogProvider.COMPONENT, this.entry.id ?? this.entry.created);
|
||||
this.courseId = this.courseId || this.entry.courseid;
|
||||
this.modId = CoreNavigator.getRouteNumberParam('cmId') || this.entry.coursemoduleid;
|
||||
}
|
||||
|
||||
if (this.courseId) {
|
||||
this.form.controls.associateWithCourse.setValue(true);
|
||||
|
@ -198,6 +222,17 @@ export class AddonBlogEditEntryPage implements CanLeave, OnInit {
|
|||
this.loaded = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
if (!this.entry) {
|
||||
return;
|
||||
}
|
||||
|
||||
CoreSync.unblockOperation(AddonBlogProvider.COMPONENT, this.entry.id ?? this.entry.created);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves blog entry.
|
||||
*
|
||||
|
@ -270,14 +305,27 @@ export class AddonBlogEditEntryPage implements CanLeave, OnInit {
|
|||
|
||||
const loading = await CoreLoadings.show('core.sending', true);
|
||||
|
||||
if (this.entry) {
|
||||
if (this.entry?.id) {
|
||||
try {
|
||||
if (!CoreNetwork.isOnline()) {
|
||||
const attachmentsId = await this.uploadOrStoreFiles({ entryId: this.entry.id });
|
||||
|
||||
return await this.saveEntry({ attachmentsId });
|
||||
}
|
||||
|
||||
if (!CoreFileUploader.areFileListDifferent(this.files, this.initialFiles)) {
|
||||
return await this.saveEntry();
|
||||
return await this.saveEntry({});
|
||||
}
|
||||
|
||||
const { attachmentsid } = await AddonBlog.prepareEntryForEdition({ entryid: this.entry.id });
|
||||
const removedFiles = CoreFileUploader.getFilesToDelete(this.initialFiles, this.files);
|
||||
|
||||
const lastModified = CoreNavigator.getRouteNumberParam('lastModified');
|
||||
const filters: AddonBlogFilter | undefined = CoreNavigator.getRouteParam('filters');
|
||||
const entry = this.entry && 'attachment' in this.entry
|
||||
? this.entry
|
||||
: await CoreUtils.ignoreErrors(this.getEntry({ filters, lastModified, entryId: this.entry.id }));
|
||||
|
||||
const removedFiles = CoreFileUploader.getFilesToDelete(entry?.attachmentfiles ?? [], this.files);
|
||||
|
||||
if (removedFiles.length) {
|
||||
await CoreFileUploader.deleteDraftFiles(attachmentsid, removedFiles);
|
||||
|
@ -285,30 +333,65 @@ export class AddonBlogEditEntryPage implements CanLeave, OnInit {
|
|||
|
||||
await CoreFileUploader.uploadFiles(attachmentsid, this.files);
|
||||
|
||||
return await this.saveEntry(attachmentsid);
|
||||
return await this.saveEntry({ attachmentsId: attachmentsid });
|
||||
} catch (error) {
|
||||
CoreDomUtils.showErrorModalDefault(error, 'Error updating entry.');
|
||||
if (CoreUtils.isWebServiceError(error)) {
|
||||
// It's a WebService error, the user cannot send the message so don't store it.
|
||||
CoreDomUtils.showErrorModalDefault(error, 'Error updating entry.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const attachmentsId = await this.uploadOrStoreFiles({ entryId: this.entry.id, forceStorage: true });
|
||||
|
||||
return await this.saveEntry({ attachmentsId, forceOffline: true });
|
||||
} finally {
|
||||
await loading.dismiss();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const created = this.entry?.created ?? CoreTimeUtils.timestamp();
|
||||
|
||||
try {
|
||||
if (!this.files.length) {
|
||||
return await this.saveEntry();
|
||||
return await this.saveEntry({ created });
|
||||
}
|
||||
|
||||
const attachmentId = await CoreFileUploader.uploadOrReuploadFiles(this.files, this.component);
|
||||
await this.saveEntry(attachmentId);
|
||||
const attachmentsId = await this.uploadOrStoreFiles({ created });
|
||||
await this.saveEntry({ created, attachmentsId });
|
||||
} catch (error) {
|
||||
CoreDomUtils.showErrorModalDefault(error, 'Error creating entry.');
|
||||
if (CoreUtils.isWebServiceError(error)) {
|
||||
// It's a WebService error, the user cannot send the message so don't store it.
|
||||
CoreDomUtils.showErrorModalDefault(error, 'Error creating entry.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const attachmentsId = await this.uploadOrStoreFiles({ created, forceStorage: true });
|
||||
|
||||
return await this.saveEntry({ attachmentsId, forceOffline: true });
|
||||
} finally {
|
||||
await loading.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload or store locally files.
|
||||
*
|
||||
* @param param Folder where files will be located.
|
||||
* @returns folder where files will be located.
|
||||
*/
|
||||
async uploadOrStoreFiles(param: AddonBlogEditEntryUploadOrStoreFilesParam): Promise<number | CoreFileUploaderStoreFilesResult> {
|
||||
if (CoreNetwork.isOnline() && !param.forceStorage) {
|
||||
return await CoreFileUploader.uploadOrReuploadFiles(this.files, this.component);
|
||||
}
|
||||
|
||||
const folder = 'entryId' in param ? { id: param.entryId } : { created: param.created };
|
||||
const folderPath = await AddonBlogOffline.getOfflineEntryFilesFolderPath(folder);
|
||||
|
||||
return await CoreFileUploader.storeFilesToUpload(folderPath, this.files);
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand or collapse associations.
|
||||
*/
|
||||
|
@ -336,27 +419,13 @@ export class AddonBlogEditEntryPage implements CanLeave, OnInit {
|
|||
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.
|
||||
* @param params Creation date and attachments ID.
|
||||
* @returns Promise resolved when done.
|
||||
*/
|
||||
async saveEntry(attachmentsId?: number): Promise<void> {
|
||||
async saveEntry(params: AddonBlogEditEntrySaveEntryParams): Promise<void> {
|
||||
const { summary, subject, publishState } = this.form.value;
|
||||
|
||||
if (!summary || !subject || !publishState) {
|
||||
|
@ -369,11 +438,30 @@ export class AddonBlogEditEntryPage implements CanLeave, OnInit {
|
|||
{ name: 'modassoc', value: this.form.controls.associateWithModule.value && this.modId ? this.modId : 0 },
|
||||
];
|
||||
|
||||
this.addAttachments(attachmentsId, options);
|
||||
if (params.attachmentsId) {
|
||||
options.push({ name: 'attachmentsid', value: params.attachmentsId });
|
||||
}
|
||||
|
||||
this.entry
|
||||
? await AddonBlog.updateEntry({ subject, summary, summaryformat: 1, options , entryid: this.entry.id })
|
||||
: await AddonBlog.addEntry({ subject, summary, summaryformat: 1, options });
|
||||
if (!this.entry?.id) {
|
||||
await AddonBlog.addEntry({
|
||||
subject,
|
||||
summary,
|
||||
summaryformat: 1,
|
||||
options,
|
||||
created: params.created ?? CoreTimeUtils.timestamp(),
|
||||
forceOffline: params.forceOffline,
|
||||
});
|
||||
} else {
|
||||
await AddonBlog.updateEntry({
|
||||
subject,
|
||||
summary,
|
||||
summaryformat: 1,
|
||||
options,
|
||||
forceOffline: params.forceOffline,
|
||||
entryid: this.entry.id,
|
||||
created: this.entry.created,
|
||||
});
|
||||
}
|
||||
|
||||
CoreEvents.trigger(ADDON_BLOG_ENTRY_UPDATED);
|
||||
this.forceLeave = true;
|
||||
|
@ -382,10 +470,36 @@ export class AddonBlogEditEntryPage implements CanLeave, OnInit {
|
|||
return CoreNavigator.back();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a formatted blog offline entry.
|
||||
*
|
||||
* @param params Entry creation date or entry ID.
|
||||
* @returns Formatted entry.
|
||||
*/
|
||||
async getFormattedBlogOfflineEntry(
|
||||
params: AddonBlogEditGetFormattedBlogOfflineEntryParams,
|
||||
): Promise<AddonBlogEditEntryFormattedOfflinePost | undefined> {
|
||||
const entryRecord = await AddonBlogOffline.getOfflineEntry(params);
|
||||
|
||||
return entryRecord ? await AddonBlog.formatOfflineEntry(entryRecord) : undefined;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type AddonBlogEditEntryGetEntryParams = {
|
||||
entryId: number;
|
||||
filters?: AddonBlogFilter;
|
||||
lastModified?: number;
|
||||
type AddonBlogEditGetFormattedBlogOfflineEntryParams = { id: number } | { created: number };
|
||||
|
||||
type AddonBlogEditEntryUploadOrStoreFilesParam = ({ entryId: number } | { created: number }) & { forceStorage?: boolean };
|
||||
|
||||
type AddonBlogEditEntryGetEntryParams = { entryId: number; filters?: AddonBlogFilter; lastModified?: number };
|
||||
|
||||
type AddonBlogEditEntryPost = Omit<AddonBlogPost, 'id'> & { id?: number };
|
||||
|
||||
type AddonBlogEditEntrySaveEntryParams = {
|
||||
created?: number;
|
||||
attachmentsId?: number | CoreFileUploaderStoreFilesResult;
|
||||
forceOffline?: boolean;
|
||||
};
|
||||
|
||||
type AddonBlogEditEntryFormattedOfflinePost = Omit<
|
||||
AddonBlogEditEntryPost, | 'attachment' | 'attachmentfiles' | 'rating' | 'format' | 'usermodified' | 'module'
|
||||
> & { attachmentfiles?: CoreFileEntry[] };
|
||||
|
|
|
@ -7,6 +7,11 @@
|
|||
<h1>{{ title | translate }}</h1>
|
||||
</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<core-context-menu>
|
||||
<core-context-menu-item [hidden]="syncHidden()" [priority]="400" [content]="'core.settings.synchronizenow' | translate"
|
||||
(action)="fetchEntries(true, true, true)" [iconAction]="syncIcon" [closeOnClick]="false" />
|
||||
</core-context-menu>
|
||||
|
||||
<core-user-menu-button />
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
|
@ -14,11 +19,11 @@
|
|||
|
||||
<ion-content class="limited-width">
|
||||
|
||||
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refresh($event.target)">
|
||||
<ion-refresher slot="fixed" [disabled]="!loaded()" (ionRefresh)="refresh(true, $event.target)">
|
||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}" />
|
||||
</ion-refresher>
|
||||
|
||||
<core-loading [hideUntil]="loaded">
|
||||
<core-loading [hideUntil]="loaded()">
|
||||
@if (showMyEntriesToggle) {
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-toggle [(ngModel)]="onlyMyEntries" (ionChange)="onlyMyEntriesToggleChanged(onlyMyEntries)">
|
||||
|
@ -27,8 +32,17 @@
|
|||
</ion-item>
|
||||
}
|
||||
|
||||
@for (entry of entries; track entry.id) {
|
||||
<div class="entry ion-padding-start ion-padding-top ion-padding-end" [id]="'entry-' + entry.id">
|
||||
@if (hasOfflineDataToSync()) {
|
||||
<ion-card class="core-warning-card">
|
||||
<ion-item>
|
||||
<ion-icon name="fas-triangle-exclamation" slot="start" aria-hidden="true" />
|
||||
<ion-label>{{ 'core.hasdatatosync' | translate:{ $a: 'addon.blog.blog' | translate } }}</ion-label>
|
||||
</ion-item>
|
||||
</ion-card>
|
||||
}
|
||||
|
||||
@for (entry of entries; track getEntryTemplateId(entry)) {
|
||||
<div class="entry ion-padding-start ion-padding-top ion-padding-end" [id]="getEntryTemplateId(entry)">
|
||||
<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"
|
||||
|
@ -38,14 +52,29 @@
|
|||
<ion-badge color="warning"> {{ 'addon.blog.publishtonoone' | translate }} </ion-badge>
|
||||
</span>
|
||||
}
|
||||
|
||||
@if (!getEntryId(entry) || entry.updatedOffline) {
|
||||
<span class="entry-draft">
|
||||
<ion-badge color="light">
|
||||
<ion-icon name="fas-clock" [attr.aria-label]="'core.lastmodified' | translate" />
|
||||
{{ 'core.notsent' | translate }}
|
||||
</ion-badge>
|
||||
</span>
|
||||
}
|
||||
</h3>
|
||||
|
||||
@if (entry.userid === currentUserId && optionsAvailable) {
|
||||
@if (entry.userid === currentUserId && optionsAvailable && !entry.deleted) {
|
||||
<ion-button fill="clear" [ariaLabel]="'core.displayoptions' | translate" (click)="showEntryActionsPopover($event, entry)"
|
||||
class="entry-options">
|
||||
<ion-icon slot="icon-only" aria-hidden="true" name="ellipsis-vertical" />
|
||||
</ion-button>
|
||||
}
|
||||
|
||||
@if (entry.deleted) {
|
||||
<ion-button slot="end" fill="clear" color="danger" (click)="undoDelete(entry)" [ariaLabel]="'core.restore' | translate">
|
||||
<ion-icon name="fas-rotate-left" slot="icon-only" aria-hidden="true" />
|
||||
</ion-button>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="entry-creation-info flex ion-align-items-center">
|
||||
|
@ -64,8 +93,8 @@
|
|||
|
||||
<div class="entry-summary" [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" />
|
||||
<core-format-text [text]="entry.summary" [component]="component" [componentId]="getEntryId(entry)"
|
||||
[contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="entry.courseid" />
|
||||
</div>
|
||||
|
||||
@if (tagsEnabled && entry.tags && entry.tags!.length > 0) {
|
||||
|
@ -77,9 +106,7 @@
|
|||
</ion-item>
|
||||
}
|
||||
|
||||
@for (file of entry.attachmentfiles; track $index) {
|
||||
<core-file [file]="file" [component]="this.component" [componentId]="entry.id" />
|
||||
}
|
||||
<core-files [files]="entry.attachmentfiles" [component]="component" [componentId]="getEntryId(entry) ?? entry.created" />
|
||||
|
||||
@if (entry.uniquehash) {
|
||||
<ion-item [href]="entry.uniquehash" core-link [detail]="true" lines="none">
|
||||
|
@ -106,8 +133,8 @@
|
|||
</ion-item>
|
||||
}
|
||||
|
||||
@if (commentsEnabled) {
|
||||
<core-comments [component]="this.component" [itemId]="entry.id" area="format_blog" [instanceId]="entry.userid"
|
||||
@if (getEntryId(entry) && commentsEnabled) {
|
||||
<core-comments [component]="this.component" [itemId]="getEntryId(entry)" area="format_blog" [instanceId]="entry.userid"
|
||||
contextLevel="user" [showItem]="true" [courseId]="entry.courseid" />
|
||||
}
|
||||
|
||||
|
@ -121,12 +148,13 @@
|
|||
</core-loading>
|
||||
|
||||
<!-- Create a blog entry. -->
|
||||
<ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end"
|
||||
*ngIf="loaded && optionsAvailable && (!filter.userid || (filter.userid && currentUserId === filter.userid))">
|
||||
@if ((filter.userid === currentUserId || showMyEntriesToggle) && loaded() && optionsAvailable) {
|
||||
<ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end">
|
||||
<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>
|
||||
|
|
|
@ -12,19 +12,30 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { ContextLevel } from '@/core/constants';
|
||||
import { ADDON_BLOG_ENTRY_UPDATED } from '@addons/blog/constants';
|
||||
import { AddonBlog, AddonBlogFilter, AddonBlogPost, AddonBlogProvider } from '@addons/blog/services/blog';
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { ContextLevel, CoreConstants } from '@/core/constants';
|
||||
import {
|
||||
ADDON_BLOG_AUTO_SYNCED,
|
||||
ADDON_BLOG_ENTRY_UPDATED,
|
||||
ADDON_BLOG_MANUAL_SYNCED,
|
||||
} from '@addons/blog/constants';
|
||||
import {
|
||||
AddonBlog,
|
||||
AddonBlogFilter,
|
||||
AddonBlogOfflinePostFormatted,
|
||||
AddonBlogPostFormatted,
|
||||
AddonBlogProvider,
|
||||
} from '@addons/blog/services/blog';
|
||||
import { AddonBlogOffline, AddonBlogOfflineEntry } from '@addons/blog/services/blog-offline';
|
||||
import { AddonBlogSync } from '@addons/blog/services/blog-sync';
|
||||
import { Component, computed, OnDestroy, OnInit, signal } 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 { CoreNetwork } from '@services/network';
|
||||
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreFileHelper } from '@services/file-helper';
|
||||
import { CoreUrl } from '@singletons/url';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { CoreArray } from '@singletons/array';
|
||||
|
@ -32,6 +43,7 @@ import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
|||
import { CoreTime } from '@singletons/time';
|
||||
import { CorePopovers } from '@services/popovers';
|
||||
import { CoreLoadings } from '@services/loadings';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
/**
|
||||
* Page that displays the list of blog entries.
|
||||
|
@ -50,10 +62,13 @@ export class AddonBlogIndexPage implements OnInit, OnDestroy {
|
|||
protected siteHomeId: number;
|
||||
protected logView: () => void;
|
||||
|
||||
loaded = false;
|
||||
loaded = signal(false);
|
||||
canLoadMore = false;
|
||||
loadMoreError = false;
|
||||
entries: AddonBlogPostFormatted[] = [];
|
||||
entries: (AddonBlogOfflinePostFormatted | AddonBlogPostFormatted)[] = [];
|
||||
entriesToRemove: { id: number; subject: string }[] = [];
|
||||
entriesToUpdate: AddonBlogOfflineEntry[] = [];
|
||||
offlineEntries: AddonBlogOfflineEntry[] = [];
|
||||
currentUserId: number;
|
||||
showMyEntriesToggle = false;
|
||||
onlyMyEntries = false;
|
||||
|
@ -63,11 +78,20 @@ export class AddonBlogIndexPage implements OnInit, OnDestroy {
|
|||
contextLevel: ContextLevel = ContextLevel.SYSTEM;
|
||||
contextInstanceId = 0;
|
||||
entryUpdateObserver: CoreEventObserver;
|
||||
syncObserver: CoreEventObserver;
|
||||
onlineObserver: Subscription;
|
||||
optionsAvailable = false;
|
||||
hasOfflineDataToSync = signal(false);
|
||||
isOnline = signal(false);
|
||||
siteId: string;
|
||||
syncIcon = CoreConstants.ICON_SYNC;
|
||||
syncHidden = computed(() => !this.loaded() || !this.isOnline() || !this.hasOfflineDataToSync());
|
||||
|
||||
constructor() {
|
||||
this.currentUserId = CoreSites.getCurrentSiteUserId();
|
||||
this.siteHomeId = CoreSites.getCurrentSiteHomeId();
|
||||
this.siteId = CoreSites.getCurrentSiteId();
|
||||
this.isOnline.set(CoreNetwork.isOnline());
|
||||
|
||||
this.logView = CoreTime.once(async () => {
|
||||
await CoreUtils.ignoreErrors(AddonBlog.logView(this.filter));
|
||||
|
@ -89,10 +113,35 @@ export class AddonBlogIndexPage implements OnInit, OnDestroy {
|
|||
});
|
||||
|
||||
this.entryUpdateObserver = CoreEvents.on(ADDON_BLOG_ENTRY_UPDATED, async () => {
|
||||
this.loaded = false;
|
||||
this.loaded.set(false);
|
||||
await CoreUtils.ignoreErrors(this.refresh());
|
||||
this.loaded = true;
|
||||
this.loaded.set(true);
|
||||
});
|
||||
|
||||
this.syncObserver = CoreEvents.onMultiple([ADDON_BLOG_MANUAL_SYNCED, ADDON_BLOG_AUTO_SYNCED], async ({ source }) => {
|
||||
if (this === source) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loaded.set(false);
|
||||
await CoreUtils.ignoreErrors(this.refresh(false));
|
||||
this.loaded.set(true);
|
||||
});
|
||||
|
||||
// Refresh online status when changes.
|
||||
this.onlineObserver = CoreNetwork.onChange().subscribe(async () => {
|
||||
this.isOnline.set(CoreNetwork.isOnline());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves an unique id to be used in template.
|
||||
*
|
||||
* @param entry Entry.
|
||||
* @returns Entry template ID.
|
||||
*/
|
||||
getEntryTemplateId(entry: AddonBlogOfflinePostFormatted | AddonBlogPostFormatted): string {
|
||||
return 'entry-' + ('id' in entry && entry.id ? entry.id : ('created-' + entry.created));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -156,23 +205,52 @@ export class AddonBlogIndexPage implements OnInit, OnDestroy {
|
|||
const deepLinkManager = new CoreMainMenuDeepLinkManager();
|
||||
deepLinkManager.treatLink();
|
||||
|
||||
await this.fetchEntries();
|
||||
await this.fetchEntries(false, false, true);
|
||||
this.optionsAvailable = await AddonBlog.isEditingEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves entry id or undefined.
|
||||
*
|
||||
* @param entry Entry.
|
||||
* @returns Entry id or undefined.
|
||||
*/
|
||||
getEntryId(entry: AddonBlogPostFormatted | AddonBlogOfflinePostFormatted): number | undefined {
|
||||
return this.isOnlineEntry(entry) ? entry.id : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch blog entries.
|
||||
*
|
||||
* @param refresh Empty events array first.
|
||||
* @returns Promise with the entries.
|
||||
*/
|
||||
protected async fetchEntries(refresh: boolean = false): Promise<void> {
|
||||
protected async fetchEntries(refresh: boolean, showSyncErrors = false, sync?: boolean): Promise<void> {
|
||||
this.loadMoreError = false;
|
||||
|
||||
if (refresh) {
|
||||
this.pageLoaded = 0;
|
||||
}
|
||||
|
||||
if (this.isOnline() && sync) {
|
||||
// Try to synchronize offline events.
|
||||
try {
|
||||
const result = await AddonBlogSync.syncEntriesForSite(CoreSites.getCurrentSiteId());
|
||||
|
||||
if (result.warnings && result.warnings.length) {
|
||||
CoreDomUtils.showAlert(undefined, result.warnings[0]);
|
||||
}
|
||||
|
||||
if (result.updated) {
|
||||
CoreEvents.trigger(ADDON_BLOG_MANUAL_SYNCED, { ...result, source: this });
|
||||
}
|
||||
} catch (error) {
|
||||
if (showSyncErrors) {
|
||||
CoreDomUtils.showErrorModalDefault(error, 'core.errorsync', true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await AddonBlog.getEntries(
|
||||
this.filter,
|
||||
|
@ -184,59 +262,72 @@ export class AddonBlogIndexPage implements OnInit, OnDestroy {
|
|||
},
|
||||
);
|
||||
|
||||
const promises = result.entries.map(async (entry: AddonBlogPostFormatted) => {
|
||||
switch (entry.publishstate) {
|
||||
case 'draft':
|
||||
entry.publishTranslated = 'publishtonoone';
|
||||
break;
|
||||
case 'site':
|
||||
entry.publishTranslated = 'publishtosite';
|
||||
break;
|
||||
case 'public':
|
||||
entry.publishTranslated = 'publishtoworld';
|
||||
break;
|
||||
default:
|
||||
entry.publishTranslated = 'privacy:unknown';
|
||||
break;
|
||||
}
|
||||
await Promise.all(result.entries.map(async (entry: AddonBlogPostFormatted) => AddonBlog.formatEntry(entry)));
|
||||
|
||||
// Calculate the context. This code was inspired by calendar events, Moodle doesn't do this for blogs.
|
||||
if (entry.moduleid || entry.coursemoduleid) {
|
||||
entry.contextLevel = ContextLevel.MODULE;
|
||||
entry.contextInstanceId = entry.moduleid || entry.coursemoduleid;
|
||||
} else if (entry.courseid) {
|
||||
entry.contextLevel = ContextLevel.COURSE;
|
||||
entry.contextInstanceId = entry.courseid;
|
||||
} else {
|
||||
entry.contextLevel = ContextLevel.USER;
|
||||
entry.contextInstanceId = entry.userid;
|
||||
}
|
||||
this.entries = refresh
|
||||
? result.entries
|
||||
: this.entries.concat(result.entries).sort((a, b) => {
|
||||
if ('id' in a && !('id' in b)) {
|
||||
return 1;
|
||||
} else if ('id' in b && !('id' in a)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
entry.summary = CoreFileHelper.replacePluginfileUrls(entry.summary, entry.summaryfiles || []);
|
||||
|
||||
entry.user = await CoreUtils.ignoreErrors(CoreUser.getProfile(entry.userid, entry.courseid, true));
|
||||
});
|
||||
|
||||
if (refresh) {
|
||||
this.entries = result.entries;
|
||||
} else {
|
||||
this.entries = CoreArray.unique(this.entries
|
||||
.concat(result.entries), 'id')
|
||||
.sort((a, b) => b.created - a.created);
|
||||
}
|
||||
return b.created - a.created;
|
||||
});
|
||||
|
||||
this.canLoadMore = result.totalentries > this.entries.length;
|
||||
await this.loadOfflineEntries(this.pageLoaded === 0);
|
||||
this.entries = CoreArray.unique(this.entries, 'id');
|
||||
|
||||
this.pageLoaded++;
|
||||
await Promise.all(promises);
|
||||
this.logView();
|
||||
} catch (error) {
|
||||
CoreDomUtils.showErrorModalDefault(error, 'addon.blog.errorloadentries', true);
|
||||
this.loadMoreError = true; // Set to prevent infinite calls with infinite-loading.
|
||||
} finally {
|
||||
this.loaded = true;
|
||||
this.loaded.set(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load offline entries and format them.
|
||||
*
|
||||
* @param loadCreated Load offline entries to create or not.
|
||||
*/
|
||||
async loadOfflineEntries(loadCreated: boolean): Promise<void> {
|
||||
if (loadCreated) {
|
||||
this.offlineEntries = await AddonBlogOffline.getOfflineEntries(this.filter);
|
||||
this.entriesToUpdate = this.offlineEntries.filter(entry => !!entry.id);
|
||||
this.entriesToRemove = await AddonBlogOffline.getEntriesToRemove();
|
||||
const entriesToCreate = this.offlineEntries.filter(entry => !entry.id);
|
||||
|
||||
const formattedEntries = await Promise.all(entriesToCreate.map(async (entryToCreate) =>
|
||||
await AddonBlog.formatOfflineEntry(entryToCreate)));
|
||||
|
||||
this.entries = [...formattedEntries, ...this.entries];
|
||||
}
|
||||
|
||||
if (this.entriesToUpdate.length) {
|
||||
this.entries = await Promise.all(this.entries.map(async (entry) => {
|
||||
const entryToUpdate = this.entriesToUpdate.find(entryToUpdate =>
|
||||
this.isOnlineEntry(entry) && entryToUpdate.id === entry.id);
|
||||
|
||||
return !entryToUpdate || !('id' in entry) ? entry : await AddonBlog.formatOfflineEntry(entryToUpdate, entry);
|
||||
}));
|
||||
}
|
||||
|
||||
for (const entryToRemove of this.entriesToRemove) {
|
||||
const foundEntry = this.entries.find(entry => ('id' in entry && entry.id === entryToRemove.id));
|
||||
|
||||
if (foundEntry) {
|
||||
foundEntry.deleted = true;
|
||||
}
|
||||
}
|
||||
|
||||
this.hasOfflineDataToSync.set(this.offlineEntries.length > 0 || this.entriesToRemove.length > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle between showing only my entries or not.
|
||||
*
|
||||
|
@ -257,6 +348,16 @@ export class AddonBlogIndexPage implements OnInit, OnDestroy {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if provided entry is online.
|
||||
*
|
||||
* @param entry Entry.
|
||||
* @returns Whether it's an online entry.
|
||||
*/
|
||||
isOnlineEntry(entry: AddonBlogOfflinePostFormatted | AddonBlogPostFormatted): entry is AddonBlogPostFormatted {
|
||||
return 'id' in entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to load more entries.
|
||||
*
|
||||
|
@ -264,7 +365,7 @@ export class AddonBlogIndexPage implements OnInit, OnDestroy {
|
|||
* @returns Resolved when done.
|
||||
*/
|
||||
loadMore(infiniteComplete?: () => void): Promise<void> {
|
||||
return this.fetchEntries().finally(() => {
|
||||
return this.fetchEntries(false).finally(() => {
|
||||
infiniteComplete && infiniteComplete();
|
||||
});
|
||||
}
|
||||
|
@ -272,11 +373,21 @@ export class AddonBlogIndexPage implements OnInit, OnDestroy {
|
|||
/**
|
||||
* Refresh blog entries on PTR.
|
||||
*
|
||||
* @param sync Sync entries.
|
||||
* @param refresher Refresher instance.
|
||||
*/
|
||||
async refresh(refresher?: HTMLIonRefresherElement): Promise<void> {
|
||||
const promises = this.entries.map((entry) =>
|
||||
CoreComments.invalidateCommentsData(ContextLevel.USER, entry.userid, this.component, entry.id, 'format_blog'));
|
||||
async refresh(sync = true, refresher?: HTMLIonRefresherElement): Promise<void> {
|
||||
const promises = this.entries.map((entry) => {
|
||||
if (this.isOnlineEntry(entry)) {
|
||||
return CoreComments.invalidateCommentsData(
|
||||
ContextLevel.USER,
|
||||
entry.userid,
|
||||
this.component,
|
||||
entry.id,
|
||||
'format_blog',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
promises.push(AddonBlog.invalidateEntries(this.filter));
|
||||
|
||||
|
@ -291,7 +402,7 @@ export class AddonBlogIndexPage implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
await CoreUtils.allPromises(promises);
|
||||
await this.fetchEntries(true);
|
||||
await this.fetchEntries(true, false, sync);
|
||||
refresher?.complete();
|
||||
}
|
||||
|
||||
|
@ -303,15 +414,21 @@ export class AddonBlogIndexPage implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
/**
|
||||
* Delete entry by id.
|
||||
* Delete entry.
|
||||
*
|
||||
* @param id Entry id.
|
||||
* @param entryToRemove Entry.
|
||||
*/
|
||||
async deleteEntry(id: number): Promise<void> {
|
||||
async deleteEntry(entryToRemove: AddonBlogOfflinePostFormatted | AddonBlogPostFormatted): Promise<void> {
|
||||
const loading = await CoreLoadings.show();
|
||||
|
||||
try {
|
||||
await AddonBlog.deleteEntry({ entryid: id });
|
||||
await this.refresh();
|
||||
if ('id' in entryToRemove && entryToRemove.id) {
|
||||
await AddonBlog.deleteEntry({ entryid: entryToRemove.id, subject: entryToRemove.subject });
|
||||
} else {
|
||||
await AddonBlogOffline.deleteOfflineEntryRecord({ created: entryToRemove.created });
|
||||
}
|
||||
|
||||
CoreEvents.trigger(ADDON_BLOG_ENTRY_UPDATED);
|
||||
} catch (error) {
|
||||
CoreDomUtils.showErrorModalDefault(error, 'addon.blog.errorloadentries', true);
|
||||
} finally {
|
||||
|
@ -323,8 +440,9 @@ export class AddonBlogIndexPage implements OnInit, OnDestroy {
|
|||
* Show the context menu.
|
||||
*
|
||||
* @param event Click Event.
|
||||
* @param entry Entry to remove.
|
||||
*/
|
||||
async showEntryActionsPopover(event: Event, entry: AddonBlogPostFormatted): Promise<void> {
|
||||
async showEntryActionsPopover(event: Event, entry: AddonBlogPostFormatted | AddonBlogOfflinePostFormatted): Promise<void> {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
|
@ -337,36 +455,41 @@ export class AddonBlogIndexPage implements OnInit, OnDestroy {
|
|||
});
|
||||
|
||||
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 },
|
||||
case 'edit': {
|
||||
await CoreNavigator.navigateToSitePath(`blog/edit/${this.isOnlineEntry(entry) && entry.id
|
||||
? entry.id
|
||||
: 'new-' + entry.created}`, {
|
||||
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);
|
||||
await this.deleteEntry(entry);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Undo entry deletion.
|
||||
*
|
||||
* @param entry Entry to prevent deletion.
|
||||
*/
|
||||
async undoDelete(entry: AddonBlogOfflinePostFormatted | AddonBlogPostFormatted): Promise<void> {
|
||||
await AddonBlogOffline.unmarkEntryAsRemoved('id' in entry ? entry.id : entry.created);
|
||||
CoreEvents.trigger(ADDON_BLOG_ENTRY_UPDATED);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.entryUpdateObserver.off();
|
||||
this.syncObserver.off();
|
||||
this.onlineObserver.unsubscribe();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Blog post with some calculated data.
|
||||
*/
|
||||
type AddonBlogPostFormatted = AddonBlogPost & {
|
||||
publishTranslated?: string; // Calculated in the app. Key of the string to translate the publish state of the post.
|
||||
user?: CoreUserProfile; // Calculated in the app. Data of the user that wrote the post.
|
||||
contextLevel?: ContextLevel; // Calculated in the app. The context level of the entry.
|
||||
contextInstanceId?: number; // Calculated in the app. The context instance id.
|
||||
};
|
||||
|
|
|
@ -0,0 +1,179 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { CoreFileUploader } from '@features/fileuploader/services/fileuploader';
|
||||
import { CoreFile } from '@services/file';
|
||||
import { CoreFileEntry } from '@services/file-helper';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { makeSingleton } from '@singletons';
|
||||
import { CorePath } from '@singletons/path';
|
||||
import { AddonBlogFilter } from './blog';
|
||||
import {
|
||||
AddonBlogOfflineEntryDBRecord,
|
||||
OFFLINE_BLOG_ENTRIES_REMOVED_TABLE_NAME,
|
||||
OFFLINE_BLOG_ENTRIES_TABLE_NAME,
|
||||
} from './database/blog';
|
||||
|
||||
/**
|
||||
* Service to handle offline blog.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AddonBlogOfflineService {
|
||||
|
||||
/**
|
||||
* Delete an offline entry.
|
||||
*
|
||||
* @param params Entry creation date or ID.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @returns Promise resolved if deleted, rejected if failure.
|
||||
*/
|
||||
async deleteOfflineEntryRecord(params: AddonBlogOfflineParams, siteId?: string): Promise<void> {
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
const conditions = 'id' in params ? { id: params.id } : { created: params.created };
|
||||
await site.getDb().deleteRecords(OFFLINE_BLOG_ENTRIES_TABLE_NAME, conditions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark entry to be removed.
|
||||
*
|
||||
* @param id Entry ID.
|
||||
* @param siteId Site ID.
|
||||
*
|
||||
* @returns Promise resolved if stored, rejected if failure.
|
||||
*/
|
||||
async markEntryAsRemoved(params: { id: number; subject: string }, siteId?: string): Promise<void> {
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
await site.getDb().insertRecord(OFFLINE_BLOG_ENTRIES_REMOVED_TABLE_NAME, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unmark entry to be removed.
|
||||
*
|
||||
* @param id Entry ID.
|
||||
* @param siteId Site ID.
|
||||
*
|
||||
* @returns Promise resolved if stored, rejected if failure.
|
||||
*/
|
||||
async unmarkEntryAsRemoved(id: number, siteId?: string): Promise<void> {
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
await site.getDb().deleteRecords(OFFLINE_BLOG_ENTRIES_REMOVED_TABLE_NAME, { id });
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves entries pending to be removed.
|
||||
*
|
||||
* @param siteId Site ID.
|
||||
*
|
||||
* @returns list of entries to remove.
|
||||
*/
|
||||
async getEntriesToRemove(siteId?: string): Promise<{ id: number; subject: string }[]> {
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
|
||||
return await site.getDb().getAllRecords<{ id: number; subject: string }>(OFFLINE_BLOG_ENTRIES_REMOVED_TABLE_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save an offline entry to be sent later.
|
||||
*
|
||||
* @param entry Entry.
|
||||
* @param siteId Site ID.
|
||||
*
|
||||
* @returns Promise resolved if stored, rejected if failure.
|
||||
*/
|
||||
async addOfflineEntry(entry: AddonBlogOfflineEntry, siteId?: string): Promise<void> {
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
await site.getDb().insertRecord(OFFLINE_BLOG_ENTRIES_TABLE_NAME, { ...entry, id: entry.id ?? -entry.created });
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves if there are any offline entry.
|
||||
*
|
||||
* @param filter Entry id.
|
||||
*
|
||||
* @returns Has offline entries.
|
||||
*/
|
||||
async getOfflineEntry(filter: { id?: number; created?: number }, siteId?: string): Promise<AddonBlogOfflineEntry | undefined> {
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
const record = await CoreUtils.ignoreErrors(
|
||||
site.getDb().getRecord<AddonBlogOfflineEntry>(OFFLINE_BLOG_ENTRIES_TABLE_NAME, filter),
|
||||
);
|
||||
|
||||
if (record && 'id' in record && record.id && record.id < 0) {
|
||||
delete record.id;
|
||||
}
|
||||
|
||||
return record;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves offline entries.
|
||||
*
|
||||
* @param filters Filters.
|
||||
* @param siteId Site ID.
|
||||
*
|
||||
* @returns Offline entries.
|
||||
*/
|
||||
async getOfflineEntries(filters?: AddonBlogFilter, siteId?: string): Promise<AddonBlogOfflineEntry[]> {
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
const records = await site.getDb().getRecords<AddonBlogOfflineEntry>(OFFLINE_BLOG_ENTRIES_TABLE_NAME, filters);
|
||||
|
||||
return records.map(record => {
|
||||
if ('id' in record && record.id && record.id < 0) {
|
||||
delete record.id;
|
||||
}
|
||||
|
||||
return record;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get offline entry files folder path.
|
||||
*
|
||||
* @param params Entry creation date or entry ID.
|
||||
* @returns path.
|
||||
*/
|
||||
async getOfflineEntryFilesFolderPath(params: AddonBlogOfflineParams, siteId?: string): Promise<string> {
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
const siteFolderPath = CoreFile.getSiteFolder(site.id);
|
||||
const folder = 'created' in params ? 'created-' + params.created : params.id;
|
||||
|
||||
return CorePath.concatenatePaths(siteFolderPath, 'offlineblog/' + folder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a list of offline files stored.
|
||||
*
|
||||
* @param folderName Folder name.
|
||||
* @param siteId Site ID.
|
||||
* @returns Offline files for the provided folder name.
|
||||
*/
|
||||
async getOfflineFiles(folderName: AddonBlogOfflineParams, siteId?: string): Promise<CoreFileEntry[]> {
|
||||
try {
|
||||
const folderPath = await AddonBlogOffline.getOfflineEntryFilesFolderPath(folderName, siteId);
|
||||
|
||||
return await CoreFileUploader.getStoredFiles(folderPath);
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export type AddonBlogOfflineParams = { id: number } | { created: number };
|
||||
|
||||
export type AddonBlogOfflineEntry = Omit<AddonBlogOfflineEntryDBRecord, 'id'> & { id?: number };
|
||||
|
||||
export const AddonBlogOffline = makeSingleton(AddonBlogOfflineService);
|
|
@ -0,0 +1,251 @@
|
|||
// (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 { CoreSyncBaseProvider, CoreSyncBlockedError } from '@classes/base-sync';
|
||||
import { CoreFileUploader } from '@features/fileuploader/services/fileuploader';
|
||||
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
|
||||
import { CoreSync, CoreSyncResult } from '@services/sync';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { makeSingleton, Translate } from '@singletons';
|
||||
import { CoreEvents } from '@singletons/events';
|
||||
import { ADDON_BLOG_AUTO_SYNCED, ADDON_BLOG_SYNC_ID } from '../constants';
|
||||
import { AddonBlog, AddonBlogAddEntryOption, AddonBlogAddEntryWSParams, AddonBlogProvider } from './blog';
|
||||
import { AddonBlogOffline, AddonBlogOfflineEntry } from './blog-offline';
|
||||
import { AddonBlogOfflineEntryDBRecord } from './database/blog';
|
||||
|
||||
/**
|
||||
* Service to sync blog.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AddonBlogSyncProvider extends CoreSyncBaseProvider<AddonBlogSyncResult> {
|
||||
|
||||
protected componentTranslatableString = 'addon.blog.blog';
|
||||
|
||||
constructor() {
|
||||
super('AddonBlogSyncService');
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to synchronize all the entries in a certain site or in all sites.
|
||||
*
|
||||
* @param siteId Site ID to sync. If not defined, sync all sites.
|
||||
* @param force Force sync.
|
||||
* @returns Promise resolved if sync is successful, rejected if sync fails.
|
||||
*/
|
||||
async syncAllEntries(siteId?: string, force?: boolean): Promise<void> {
|
||||
await this.syncOnSites('All entries', (siteId) => this.syncAllEntriesFunc(siteId, !!force), siteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync all entries on a site.
|
||||
*
|
||||
* @param siteId Site ID to sync.
|
||||
* @param force Force sync.
|
||||
*/
|
||||
protected async syncAllEntriesFunc(siteId: string, force = false): Promise<void> {
|
||||
const needed = force ? true : await this.isSyncNeeded(ADDON_BLOG_SYNC_ID, siteId);
|
||||
|
||||
if (!needed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await this.syncEntriesForSite(siteId);
|
||||
|
||||
if (!result.updated) {
|
||||
return;
|
||||
}
|
||||
|
||||
CoreEvents.trigger(ADDON_BLOG_AUTO_SYNCED, undefined, siteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform entries syncronization for specified site.
|
||||
*
|
||||
* @param siteId Site id.
|
||||
* @returns Syncronization result.
|
||||
*/
|
||||
async syncEntriesForSite(siteId: string): Promise<AddonBlogSyncResult> {
|
||||
const currentSyncPromise = this.getOngoingSync(ADDON_BLOG_SYNC_ID, siteId);
|
||||
|
||||
if (currentSyncPromise) {
|
||||
return currentSyncPromise;
|
||||
}
|
||||
|
||||
this.logger.debug('Try to sync ' + ADDON_BLOG_SYNC_ID + ' in site ' + siteId);
|
||||
|
||||
return await this.addOngoingSync(ADDON_BLOG_SYNC_ID, this.performEntriesSync(siteId), siteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs entries syncronization.
|
||||
*
|
||||
* @param siteId Site ID.
|
||||
* @returns Syncronization result.
|
||||
*/
|
||||
async performEntriesSync(siteId: string): Promise<AddonBlogSyncResult> {
|
||||
const result: AddonBlogSyncResult = { updated: false, warnings: [] };
|
||||
const entriesToSync = await this.syncEntriesToRemove(siteId);
|
||||
|
||||
for (const entry of entriesToSync.entries) {
|
||||
if (CoreSync.isBlocked(AddonBlogProvider.COMPONENT, entry.id ?? entry.created, siteId)) {
|
||||
this.logger.debug('Cannot sync entry ' + entry.created + ' because it is blocked.');
|
||||
|
||||
throw new CoreSyncBlockedError(Translate.instant('core.errorsyncblocked', { $a: this.componentTranslate }));
|
||||
}
|
||||
|
||||
const formattedEntry: AddonBlogAddEntryWSParams = {
|
||||
subject: entry.subject,
|
||||
summary: entry.summary,
|
||||
summaryformat: entry.summaryformat,
|
||||
options: JSON.parse(entry.options),
|
||||
};
|
||||
|
||||
try {
|
||||
if (entry.id) {
|
||||
await this.syncUpdatedEntry({ ...entry, id: entry.id, options: formattedEntry.options }, siteId);
|
||||
result.updated = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
const draftId = await this.uploadAttachments({ created: entry.created, options: formattedEntry.options }, siteId);
|
||||
const option = formattedEntry.options.find(option => option.name === 'attachmentsid');
|
||||
|
||||
if (draftId) {
|
||||
option ? option.value = draftId : formattedEntry.options.push({ name: 'attachmentsid', value: draftId });
|
||||
}
|
||||
|
||||
await AddonBlog.addEntryOnline(formattedEntry, siteId);
|
||||
await AddonBlogOffline.deleteOfflineEntryRecord({ created: entry.created }, siteId);
|
||||
result.updated = true;
|
||||
} catch (error) {
|
||||
if (!error || !CoreUtils.isWebServiceError(error)) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
await AddonBlogOffline.deleteOfflineEntryRecord(entry.id ? { id: entry.id } : { created: entry.created }, siteId);
|
||||
this.addOfflineDataDeletedWarning(result.warnings, entry.subject, error);
|
||||
result.updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync offline blog entry.
|
||||
*
|
||||
* @param entry Entry to update.
|
||||
* @param siteId Site ID.
|
||||
*/
|
||||
protected async syncUpdatedEntry(entry: AddonBlogSyncEntryToSync, siteId?: string): Promise<void> {
|
||||
const { attachmentsid } = await AddonBlog.prepareEntryForEdition({ entryid: entry.id }, siteId);
|
||||
await this.uploadAttachments({ entryId: entry.id, attachmentsId: attachmentsid, options: entry.options }, siteId);
|
||||
const optionsAttachmentsId = entry.options.find(option => option.name === 'attachmentsid');
|
||||
|
||||
if (optionsAttachmentsId) {
|
||||
optionsAttachmentsId.value = attachmentsid;
|
||||
} else {
|
||||
entry.options.push({ name: 'attachmentsid', value: attachmentsid });
|
||||
}
|
||||
|
||||
const { options, subject, summary, summaryformat, id } = entry;
|
||||
await AddonBlog.updateEntryOnline({ options, subject, summary, summaryformat, entryid: id }, siteId);
|
||||
await AddonBlogOffline.deleteOfflineEntryRecord({ id }, siteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload attachments.
|
||||
*
|
||||
* @param params entry creation date or entry ID and attachments ID.
|
||||
*
|
||||
* @returns draftId.
|
||||
*/
|
||||
protected async uploadAttachments(params: AddonBlogSyncUploadAttachmentsParams, siteId?: string): Promise<number | undefined> {
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
const folder = 'created' in params ? { created: params.created } : { id: params.entryId };
|
||||
const offlineFiles = await AddonBlogOffline.getOfflineFiles(folder, site.id);
|
||||
|
||||
if ('created' in params) {
|
||||
return await CoreFileUploader.uploadOrReuploadFiles(
|
||||
offlineFiles,
|
||||
AddonBlogProvider.COMPONENT,
|
||||
params.created,
|
||||
site.id,
|
||||
);
|
||||
}
|
||||
|
||||
const { entries } = await AddonBlog.getEntries(
|
||||
{ entryid: params.entryId },
|
||||
{ readingStrategy: CoreSitesReadingStrategy.PREFER_NETWORK, siteId: site.id },
|
||||
);
|
||||
|
||||
const onlineEntry = entries.find(entry => entry.id === params.entryId);
|
||||
const attachments = AddonBlog.getAttachmentFilesFromOptions(params.options);
|
||||
const filesToDelete = CoreFileUploader.getFilesToDelete(onlineEntry?.attachmentfiles ?? [], attachments.online);
|
||||
|
||||
if (filesToDelete.length) {
|
||||
await CoreFileUploader.deleteDraftFiles(params.attachmentsId, filesToDelete, site.id);
|
||||
}
|
||||
|
||||
await CoreFileUploader.uploadFiles(params.attachmentsId, [...attachments.online, ...offlineFiles], site.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync entries to remove.
|
||||
*
|
||||
* @param siteId Site ID.
|
||||
* @returns Entries to remove and result.
|
||||
*/
|
||||
protected async syncEntriesToRemove(siteId?: string): Promise<AddonBlogSyncGetPendingToSyncEntries> {
|
||||
let entriesToSync = await AddonBlogOffline.getOfflineEntries(undefined, siteId);
|
||||
const entriesToBeRemoved = await AddonBlogOffline.getEntriesToRemove(siteId);
|
||||
const warnings = [];
|
||||
|
||||
await Promise.all(entriesToBeRemoved.map(async (entry) => {
|
||||
try {
|
||||
await AddonBlog.deleteEntryOnline({ entryid: entry.id }, siteId);
|
||||
const entriesPendingToSync = entriesToSync.filter(entryToSync => entryToSync.id !== entry.id);
|
||||
|
||||
if (entriesPendingToSync.length !== entriesToSync.length) {
|
||||
await AddonBlogOffline.deleteOfflineEntryRecord({ id: entry.id }, siteId);
|
||||
entriesToSync = entriesPendingToSync;
|
||||
}
|
||||
} catch (error) {
|
||||
if (!CoreUtils.isWebServiceError(error)) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
await AddonBlogOffline.unmarkEntryAsRemoved(entry.id, siteId);
|
||||
this.addOfflineDataDeletedWarning(warnings, entry.subject, error);
|
||||
}
|
||||
}));
|
||||
|
||||
return { entries: entriesToSync, result: { updated: entriesToBeRemoved.length > 0, warnings } };
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const AddonBlogSync = makeSingleton(AddonBlogSyncProvider);
|
||||
|
||||
export type AddonBlogSyncResult = CoreSyncResult;
|
||||
|
||||
export type AddonBlogSyncUploadAttachmentsParams =
|
||||
({ entryId: number; attachmentsId: number } | { created: number })
|
||||
& { options: AddonBlogAddEntryOption[] };
|
||||
|
||||
export type AddonBlogSyncEntryToSync = Omit<AddonBlogOfflineEntryDBRecord, 'id'|'options'>
|
||||
& { options: AddonBlogAddEntryOption[]; id: number };
|
||||
|
||||
export type AddonBlogSyncGetPendingToSyncEntries = { entries: AddonBlogOfflineEntry[]; result: AddonBlogSyncResult };
|
|
@ -12,14 +12,21 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { ContextLevel } from '@/core/constants';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { CoreSiteWSPreSets } from '@classes/sites/authenticated-site';
|
||||
import { CoreSite } from '@classes/sites/site';
|
||||
import { CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader';
|
||||
import { CoreTagItem } from '@features/tag/services/tag';
|
||||
import { CoreUser, CoreUserProfile } from '@features/user/services/user';
|
||||
import { CoreFileEntry, CoreFileHelper } from '@services/file-helper';
|
||||
import { CoreNetwork } from '@services/network';
|
||||
import { CoreSites, CoreSitesCommonWSOptions } from '@services/sites';
|
||||
import { CoreTimeUtils } from '@services/utils/time';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { CoreStatusWithWarningsWSResponse, CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws';
|
||||
import { makeSingleton } from '@singletons';
|
||||
import { AddonBlogOffline, AddonBlogOfflineEntry } from './blog-offline';
|
||||
|
||||
const ROOT_CACHE_KEY = 'addonBlog:';
|
||||
|
||||
|
@ -91,10 +98,48 @@ export class AddonBlogProvider {
|
|||
* @returns Entry id.
|
||||
* @since 4.4
|
||||
*/
|
||||
async addEntry(params: AddonBlogAddEntryWSParams, siteId?: string): Promise<number> {
|
||||
async addEntry(
|
||||
{ created, forceOffline, ...params }: AddonBlogAddEntryWSParams & { created: number; forceOffline?: boolean },
|
||||
siteId?: string,
|
||||
): Promise<void> {
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
|
||||
return await site.write<number>('core_blog_add_entry', params);
|
||||
const storeOffline = async (): Promise<void> => {
|
||||
await AddonBlogOffline.addOfflineEntry({
|
||||
...params,
|
||||
userid: site.getUserId(),
|
||||
lastmodified: created,
|
||||
options: JSON.stringify(params.options),
|
||||
created,
|
||||
});
|
||||
};
|
||||
|
||||
if (forceOffline || !CoreNetwork.isOnline()) {
|
||||
return await storeOffline();
|
||||
}
|
||||
|
||||
try {
|
||||
await this.addEntryOnline(params, siteId);
|
||||
} catch (error) {
|
||||
if (!CoreUtils.isWebServiceError(error)) {
|
||||
// Couldn't connect to server, store in offline.
|
||||
return await storeOffline();
|
||||
}
|
||||
|
||||
// The WebService has thrown an error, reject.
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add entry online.
|
||||
*
|
||||
* @param wsParams Params expected by the webservice.
|
||||
* @param siteId Site ID.
|
||||
*/
|
||||
async addEntryOnline(wsParams: AddonBlogAddEntryWSParams, siteId?: string): Promise<void> {
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
await site.write('core_blog_add_entry', wsParams);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -103,10 +148,54 @@ export class AddonBlogProvider {
|
|||
* @param params WS Params.
|
||||
* @param siteId Site ID of the entry.
|
||||
* @since 4.4
|
||||
* @returns void
|
||||
*/
|
||||
async updateEntry(params: AddonBlogUpdateEntryWSParams, siteId?: string): Promise<void> {
|
||||
async updateEntry(
|
||||
{ forceOffline, created, ...params }: AddonBlogUpdateEntryParams,
|
||||
siteId?: string,
|
||||
): Promise<void> {
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
await site.write('core_blog_update_entry', params);
|
||||
|
||||
const storeOffline = async (): Promise<void> => {
|
||||
const content = {
|
||||
subject: params.subject,
|
||||
summary: params.summary,
|
||||
summaryformat: params.summaryformat,
|
||||
userid: site.getUserId(),
|
||||
lastmodified: CoreTimeUtils.timestamp(),
|
||||
options: JSON.stringify(params.options),
|
||||
created,
|
||||
};
|
||||
|
||||
await AddonBlogOffline.addOfflineEntry({ ...content, id: params.entryid });
|
||||
};
|
||||
|
||||
if (forceOffline || !CoreNetwork.isOnline()) {
|
||||
return await storeOffline();
|
||||
}
|
||||
|
||||
try {
|
||||
await this.updateEntryOnline(params, siteId);
|
||||
} catch (error) {
|
||||
if (!CoreUtils.isWebServiceError(error)) {
|
||||
// Couldn't connect to server, store in offline.
|
||||
return await storeOffline();
|
||||
}
|
||||
|
||||
// The WebService has thrown an error, reject.
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update entry online.
|
||||
*
|
||||
* @param wsParams Params expected by the webservice.
|
||||
* @param siteId Site ID.
|
||||
*/
|
||||
async updateEntryOnline(wsParams: AddonBlogUpdateEntryWSParams, siteId?: string): Promise<void> {
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
await site.write('core_blog_update_entry', wsParams);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -134,10 +223,33 @@ export class AddonBlogProvider {
|
|||
* @returns Entry deleted successfully or not.
|
||||
* @since 4.4
|
||||
*/
|
||||
async deleteEntry(params: AddonBlogDeleteEntryWSParams, siteId?: string): Promise<AddonBlogDeleteEntryWSResponse> {
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
async deleteEntry({ subject, ...params }: AddonBlogDeleteEntryWSParams & { subject: string }, siteId?: string): Promise<void> {
|
||||
try {
|
||||
if (!CoreNetwork.isOnline()) {
|
||||
return await AddonBlogOffline.markEntryAsRemoved({ id: params.entryid, subject }, siteId);
|
||||
}
|
||||
|
||||
return await site.write('core_blog_delete_entry', params);
|
||||
await this.deleteEntryOnline(params, siteId);
|
||||
await CoreUtils.ignoreErrors(AddonBlogOffline.unmarkEntryAsRemoved(params.entryid));
|
||||
} catch (error) {
|
||||
if (!CoreUtils.isWebServiceError(error)) {
|
||||
// Couldn't connect to server, store in offline.
|
||||
return await AddonBlogOffline.markEntryAsRemoved({ id: params.entryid, subject }, siteId);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete entry online.
|
||||
*
|
||||
* @param wsParams Params expected by the webservice.
|
||||
* @param siteId Site ID.
|
||||
*/
|
||||
async deleteEntryOnline(wsParams: AddonBlogDeleteEntryWSParams, siteId?: string): Promise<void> {
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
await site.write('core_blog_delete_entry', wsParams);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -183,6 +295,113 @@ export class AddonBlogProvider {
|
|||
return site.write('core_blog_view_entries', data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format local stored entries to required data structure.
|
||||
*
|
||||
* @param offlineEntry Offline entry data.
|
||||
* @param entry Entry.
|
||||
* @returns Formatted entry.
|
||||
*/
|
||||
async formatOfflineEntry(
|
||||
offlineEntry: AddonBlogOfflineEntry,
|
||||
entry?: AddonBlogPostFormatted,
|
||||
): Promise<AddonBlogOfflinePostFormatted> {
|
||||
const options: AddonBlogAddEntryOption[] = JSON.parse(offlineEntry.options);
|
||||
const moduleId = options?.find(option => option.name === 'modassoc')?.value as number | undefined;
|
||||
const courseId = options?.find(option => option.name === 'courseassoc')?.value as number | undefined;
|
||||
const tags = options?.find(option => option.name === 'tags')?.value as string | undefined;
|
||||
const publishState = options?.find(option => option.name === 'publishstate')?.value as AddonBlogPublishState
|
||||
?? AddonBlogPublishState.draft;
|
||||
const user = await CoreUtils.ignoreErrors(CoreUser.getProfile(offlineEntry.userid, courseId, true));
|
||||
const folder = 'id' in offlineEntry && offlineEntry.id ? { id: offlineEntry.id } : { created: offlineEntry.created };
|
||||
const offlineFiles = await AddonBlogOffline.getOfflineFiles(folder);
|
||||
const optionsFiles = this.getAttachmentFilesFromOptions(options);
|
||||
const attachmentFiles = [...optionsFiles.online, ...offlineFiles];
|
||||
|
||||
return {
|
||||
...offlineEntry,
|
||||
publishstate: publishState,
|
||||
publishTranslated: this.getPublishTranslated(publishState),
|
||||
user,
|
||||
tags: tags?.length ? JSON.parse(tags) : [],
|
||||
coursemoduleid: moduleId ?? 0,
|
||||
courseid: courseId ?? 0,
|
||||
attachmentfiles: attachmentFiles,
|
||||
userid: user?.id ?? 0,
|
||||
moduleid: moduleId ?? 0,
|
||||
summaryfiles: [],
|
||||
uniquehash: '',
|
||||
module: entry?.module,
|
||||
groupid: 0,
|
||||
content: offlineEntry.summary,
|
||||
updatedOffline: true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves publish state translated.
|
||||
*
|
||||
* @param state Publish state.
|
||||
* @returns Translated state.
|
||||
*/
|
||||
getPublishTranslated(state?: string): string {
|
||||
switch (state) {
|
||||
case 'draft':
|
||||
return 'publishtonoone';
|
||||
case 'site':
|
||||
return 'publishtosite';
|
||||
case 'public':
|
||||
return 'publishtoworld';
|
||||
default:
|
||||
return 'privacy:unknown';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format provided entry to AddonBlogPostFormatted.
|
||||
*/
|
||||
async formatEntry(entry: AddonBlogPostFormatted): Promise<void> {
|
||||
entry.publishTranslated = this.getPublishTranslated(entry.publishstate);
|
||||
|
||||
// Calculate the context. This code was inspired by calendar events, Moodle doesn't do this for blogs.
|
||||
if (entry.moduleid || entry.coursemoduleid) {
|
||||
entry.contextLevel = ContextLevel.MODULE;
|
||||
entry.contextInstanceId = entry.moduleid || entry.coursemoduleid;
|
||||
} else if (entry.courseid) {
|
||||
entry.contextLevel = ContextLevel.COURSE;
|
||||
entry.contextInstanceId = entry.courseid;
|
||||
} else {
|
||||
entry.contextLevel = ContextLevel.USER;
|
||||
entry.contextInstanceId = entry.userid;
|
||||
}
|
||||
|
||||
entry.summary = CoreFileHelper.replacePluginfileUrls(entry.summary, entry.summaryfiles || []);
|
||||
entry.user = await CoreUtils.ignoreErrors(CoreUser.getProfile(entry.userid, entry.courseid, true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get attachments files from options object.
|
||||
*
|
||||
* @param options Entry options.
|
||||
* @returns attachmentsId.
|
||||
*/
|
||||
getAttachmentFilesFromOptions(options: AddonBlogAddEntryOption[]): CoreFileUploaderStoreFilesResult {
|
||||
const attachmentsId = options.find(option => option.name === 'attachmentsid');
|
||||
|
||||
if (!attachmentsId) {
|
||||
return { online: [], offline: 0 };
|
||||
}
|
||||
|
||||
switch(typeof attachmentsId.value) {
|
||||
case 'object':
|
||||
return attachmentsId.value;
|
||||
case 'string':
|
||||
return JSON.parse(attachmentsId.value);
|
||||
default:
|
||||
return { online: [], offline: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
export const AddonBlog = makeSingleton(AddonBlogProvider);
|
||||
|
||||
|
@ -218,7 +437,7 @@ export type CoreBlogGetEntriesWSResponse = {
|
|||
/**
|
||||
* Data returned by blog's post_exporter.
|
||||
*/
|
||||
export type AddonBlogPost = {
|
||||
export interface AddonBlogPost {
|
||||
id: number; // Post/entry id.
|
||||
module: string; // Where it was published the post (blog, blog_external...).
|
||||
userid: number; // Post author.
|
||||
|
@ -241,7 +460,7 @@ export type AddonBlogPost = {
|
|||
summaryfiles: CoreWSExternalFile[]; // Summaryfiles.
|
||||
attachmentfiles?: CoreWSExternalFile[]; // Attachmentfiles.
|
||||
tags?: CoreTagItem[]; // @since 3.7. Tags.
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Params of core_blog_view_entries WS.
|
||||
|
@ -282,14 +501,14 @@ export type AddonBlogAddEntryWSParams = {
|
|||
options: AddonBlogAddEntryOption[];
|
||||
};
|
||||
|
||||
export type AddonBlogUpdateEntryWSParams = AddonBlogAddEntryWSParams & { entryid: number };
|
||||
export type AddonBlogUpdateEntryWSParams = AddonBlogAddEntryWSParams & ({ entryid: number });
|
||||
|
||||
/**
|
||||
* Add entry options.
|
||||
*/
|
||||
export type AddonBlogAddEntryOption = {
|
||||
name: 'inlineattachmentsid' | 'attachmentsid' | 'publishstate' | 'courseassoc' | 'modassoc' | 'tags';
|
||||
value: string | number;
|
||||
value: string | number | CoreFileUploaderStoreFilesResult;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -335,6 +554,33 @@ export type AddonBlogGetEntriesOptions = CoreSitesCommonWSOptions & {
|
|||
page?: number;
|
||||
};
|
||||
|
||||
export type AddonBlogUndoDelete = { created: number } | { id: number };
|
||||
|
||||
export const AddonBlogPublishState = { draft: 'draft', site: 'site', public: 'public' } as const;
|
||||
// eslint-disable-next-line @typescript-eslint/no-redeclare
|
||||
export type AddonBlogPublishState = typeof AddonBlogPublishState[keyof typeof AddonBlogPublishState];
|
||||
|
||||
/**
|
||||
* Blog post with some calculated data.
|
||||
*/
|
||||
export type AddonBlogPostFormatted = Omit<
|
||||
AddonBlogPost, 'attachment' | 'attachmentfiles' | 'usermodified' | 'format' | 'rating' | 'module'
|
||||
> & {
|
||||
publishTranslated?: string; // Calculated in the app. Key of the string to translate the publish state of the post.
|
||||
user?: CoreUserProfile; // Calculated in the app. Data of the user that wrote the post.
|
||||
contextLevel?: ContextLevel; // Calculated in the app. The context level of the entry.
|
||||
contextInstanceId?: number; // Calculated in the app. The context instance id.
|
||||
coursemoduleid: number; // Course module id where the post was created.
|
||||
attachmentfiles?: CoreFileEntry[]; // Attachmentfiles.
|
||||
module?: string;
|
||||
deleted?: boolean;
|
||||
updatedOffline?: boolean;
|
||||
};
|
||||
|
||||
export type AddonBlogOfflinePostFormatted = Omit<AddonBlogPostFormatted, 'id'>;
|
||||
|
||||
export type AddonBlogUpdateEntryParams = AddonBlogUpdateEntryWSParams & {
|
||||
attachments?: string;
|
||||
forceOffline?: boolean;
|
||||
created: number;
|
||||
};
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
// (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 { CoreSiteSchema } from '@services/sites';
|
||||
|
||||
/**
|
||||
* Database variables for AddonBlogOfflineService.
|
||||
*/
|
||||
export const OFFLINE_BLOG_ENTRIES_TABLE_NAME = 'addon_blog_entries';
|
||||
export const OFFLINE_BLOG_ENTRIES_REMOVED_TABLE_NAME = 'addon_blog_entries_removed';
|
||||
|
||||
export const BLOG_OFFLINE_SITE_SCHEMA: CoreSiteSchema = {
|
||||
name: 'AddonBlogOfflineService',
|
||||
version: 1,
|
||||
tables: [
|
||||
{
|
||||
name: OFFLINE_BLOG_ENTRIES_TABLE_NAME,
|
||||
columns: [
|
||||
{
|
||||
name: 'id',
|
||||
type: 'INTEGER',
|
||||
},
|
||||
{
|
||||
name: 'userid',
|
||||
type: 'INTEGER',
|
||||
},
|
||||
{
|
||||
name: 'subject',
|
||||
type: 'TEXT',
|
||||
},
|
||||
{
|
||||
name: 'summary',
|
||||
type: 'TEXT',
|
||||
},
|
||||
{
|
||||
name: 'summaryformat',
|
||||
type: 'INTEGER',
|
||||
},
|
||||
{
|
||||
name: 'created',
|
||||
type: 'INTEGER',
|
||||
},
|
||||
{
|
||||
name: 'lastmodified',
|
||||
type: 'INTEGER',
|
||||
},
|
||||
{
|
||||
name: 'options',
|
||||
type: 'TEXT',
|
||||
},
|
||||
],
|
||||
primaryKeys: ['id'],
|
||||
},
|
||||
{
|
||||
name: OFFLINE_BLOG_ENTRIES_REMOVED_TABLE_NAME,
|
||||
columns: [
|
||||
{
|
||||
name: 'id',
|
||||
type: 'INTEGER',
|
||||
},
|
||||
{
|
||||
name: 'subject',
|
||||
type: 'TEXT',
|
||||
},
|
||||
],
|
||||
primaryKeys: ['id'],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* Blog offline entry.
|
||||
*/
|
||||
export type AddonBlogOfflineEntryDBRecord = {
|
||||
id: number;
|
||||
userid: number;
|
||||
subject: string;
|
||||
summary: string;
|
||||
summaryformat: number;
|
||||
created: number;
|
||||
lastmodified: number;
|
||||
options: string;
|
||||
};
|
|
@ -0,0 +1,51 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { CoreCronHandler } from '@services/cron';
|
||||
import { makeSingleton } from '@singletons';
|
||||
import { AddonBlogSync } from '../blog-sync';
|
||||
|
||||
/**
|
||||
* Synchronization cron handler.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AddonBlogSyncCronHandlerService implements CoreCronHandler {
|
||||
|
||||
name = 'AddonBlogSyncCronHandler';
|
||||
|
||||
/**
|
||||
* Execute the process.
|
||||
* Receives the ID of the site affected, undefined for all sites.
|
||||
*
|
||||
* @param siteId ID of the site affected, undefined for all sites.
|
||||
* @param force Wether the execution is forced (manual sync).
|
||||
* @returns Promise resolved when done, rejected if failure.
|
||||
*/
|
||||
execute(siteId?: string, force?: boolean): Promise<void> {
|
||||
return AddonBlogSync.syncAllEntries(siteId, force);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the time between consecutive executions.
|
||||
*
|
||||
* @returns Time between consecutive executions (in ms).
|
||||
*/
|
||||
getInterval(): number {
|
||||
return AddonBlogSync.syncInterval;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const AddonBlogSyncCronHandler = makeSingleton(AddonBlogSyncCronHandlerService);
|
Loading…
Reference in New Issue