diff --git a/src/addons/mod/data/components/search/search.ts b/src/addons/mod/data/components/search/search.ts index 69d06f1cc..4221b769a 100644 --- a/src/addons/mod/data/components/search/search.ts +++ b/src/addons/mod/data/components/search/search.ts @@ -71,7 +71,7 @@ export class AddonModDataSearchComponent implements OnInit { this.search.advanced?.forEach((field) => { if (typeof field != 'undefined') { this.advancedIndexed[field.name] = field.value - ? CoreTextUtils.parseJSON(field.value) + ? CoreTextUtils.parseJSON(field.value, '') : ''; } }); diff --git a/src/addons/mod/data/fields/file/services/handler.ts b/src/addons/mod/data/fields/file/services/handler.ts index cdf11047c..016f70c06 100644 --- a/src/addons/mod/data/fields/file/services/handler.ts +++ b/src/addons/mod/data/fields/file/services/handler.ts @@ -112,7 +112,8 @@ export class AddonModDataFieldFileHandlerService implements AddonModDataFieldHan offlineContent: CoreFormFields, offlineFiles?: FileEntry[], ): AddonModDataEntryField { - const uploadedFilesResult: CoreFileUploaderStoreFilesResult = offlineContent?.file; + const uploadedFilesResult: CoreFileUploaderStoreFilesResult | undefined = + offlineContent?.file; if (uploadedFilesResult && uploadedFilesResult.offline > 0 && offlineFiles && offlineFiles?.length > 0) { originalContent.content = offlineFiles[0].name; diff --git a/src/addons/mod/data/fields/latlong/services/handler.ts b/src/addons/mod/data/fields/latlong/services/handler.ts index c55606f22..4eed24cc1 100644 --- a/src/addons/mod/data/fields/latlong/services/handler.ts +++ b/src/addons/mod/data/fields/latlong/services/handler.ts @@ -121,8 +121,8 @@ export class AddonModDataFieldLatlongHandlerService implements AddonModDataField * @inheritdoc */ overrideData(originalContent: AddonModDataEntryField, offlineContent: CoreFormFields): AddonModDataEntryField { - originalContent.content = offlineContent[0] || ''; - originalContent.content1 = offlineContent[1] || ''; + originalContent.content = offlineContent['0'] || ''; + originalContent.content1 = offlineContent['1'] || ''; return originalContent; } diff --git a/src/addons/mod/data/fields/picture/services/handler.ts b/src/addons/mod/data/fields/picture/services/handler.ts index b8a8a89da..d0b503c13 100644 --- a/src/addons/mod/data/fields/picture/services/handler.ts +++ b/src/addons/mod/data/fields/picture/services/handler.ts @@ -155,7 +155,8 @@ export class AddonModDataFieldPictureHandlerService implements AddonModDataField offlineContent: CoreFormFields, offlineFiles?: FileEntry[], ): AddonModDataEntryField { - const uploadedFilesResult: CoreFileUploaderStoreFilesResult = offlineContent?.file; + const uploadedFilesResult: CoreFileUploaderStoreFilesResult | undefined = + offlineContent?.file; if (uploadedFilesResult && uploadedFilesResult.offline > 0 && offlineFiles && offlineFiles?.length > 0) { originalContent.content = offlineFiles[0].name; @@ -165,7 +166,7 @@ export class AddonModDataFieldPictureHandlerService implements AddonModDataField originalContent.files = [uploadedFilesResult.online[0]]; } - originalContent.content1 = offlineContent.alttext || ''; + originalContent.content1 = offlineContent.alttext || ''; return originalContent; } diff --git a/src/addons/mod/data/fields/url/services/handler.ts b/src/addons/mod/data/fields/url/services/handler.ts index 77ecbca8d..ae36f63a5 100644 --- a/src/addons/mod/data/fields/url/services/handler.ts +++ b/src/addons/mod/data/fields/url/services/handler.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { AddonModDataField, AddonModDataSubfieldData } from '@addons/mod/data/services/data'; +import { AddonModDataEntryField, AddonModDataField, AddonModDataSubfieldData } from '@addons/mod/data/services/data'; import { Injectable, Type } from '@angular/core'; import { CoreFormFields } from '@singletons/form'; import { Translate, makeSingleton } from '@singletons'; @@ -59,5 +59,14 @@ export class AddonModDataFieldUrlHandlerService extends AddonModDataFieldTextHan } } + /** + * @inheritdoc + */ + overrideData(originalContent: AddonModDataEntryField, offlineContent: CoreFormFields): AddonModDataEntryField { + originalContent.content = offlineContent['0'] || ''; + + return originalContent; + } + } export const AddonModDataFieldUrlHandler = makeSingleton(AddonModDataFieldUrlHandlerService); diff --git a/src/addons/mod/data/services/data-helper.ts b/src/addons/mod/data/services/data-helper.ts index 9fc836dda..5951a382d 100644 --- a/src/addons/mod/data/services/data-helper.ts +++ b/src/addons/mod/data/services/data-helper.ts @@ -92,9 +92,9 @@ export class AddonModDataHelperProvider { if (offlineContent.subfield) { offlineContents[offlineContent.fieldid][offlineContent.subfield] = - CoreTextUtils.parseJSON(offlineContent.value); + CoreTextUtils.parseJSON(offlineContent.value, ''); } else { - offlineContents[offlineContent.fieldid][''] = CoreTextUtils.parseJSON(offlineContent.value); + offlineContents[offlineContent.fieldid][''] = CoreTextUtils.parseJSON(offlineContent.value, ''); } }); @@ -632,7 +632,7 @@ export class AddonModDataHelperProvider { const folderPath = await AddonModDataOffline.getEntryFieldFolder(dataId, entryId, fieldId, siteId); try { - return CoreFileUploader.getStoredFiles(folderPath); + return await CoreFileUploader.getStoredFiles(folderPath); } catch { // Ignore not found files. return []; diff --git a/src/addons/mod/data/services/data-offline.ts b/src/addons/mod/data/services/data-offline.ts index ffc317d4d..19d8638fe 100644 --- a/src/addons/mod/data/services/data-offline.ts +++ b/src/addons/mod/data/services/data-offline.ts @@ -88,9 +88,9 @@ export class AddonModDataOfflineProvider { const promises: Promise[] = []; entry.fields.forEach((field) => { - const value = CoreTextUtils.parseJSON(field.value); + const value = CoreTextUtils.parseJSON(field.value, null); - if (!value.offline) { + if (!value || !value.offline) { return; } diff --git a/src/addons/mod/data/services/data-sync.ts b/src/addons/mod/data/services/data-sync.ts index 889119427..a010247c4 100644 --- a/src/addons/mod/data/services/data-sync.ts +++ b/src/addons/mod/data/services/data-sync.ts @@ -345,8 +345,8 @@ export class AddonModDataSyncProvider extends CoreCourseActivitySyncBaseProvider try { await Promise.all(editAction.fields.map(async (field) => { // Upload Files if asked. - const value = CoreTextUtils.parseJSON(field.value || ''); - if (value.online || value.offline) { + const value = CoreTextUtils.parseJSON(field.value || '', null); + if (value && (value.online || value.offline)) { let files: CoreFileEntry[] = value.online || []; const offlineFiles = value.offline diff --git a/src/addons/mod/feedback/pages/form/form.html b/src/addons/mod/feedback/pages/form/form.html index c470f8ac2..3f67b5a17 100644 --- a/src/addons/mod/feedback/pages/form/form.html +++ b/src/addons/mod/feedback/pages/form/form.html @@ -155,7 +155,7 @@ - + diff --git a/src/addons/mod/glossary/components/index/addon-mod-glossary-index.html b/src/addons/mod/glossary/components/index/addon-mod-glossary-index.html index 6906bf761..ce7de9aaf 100644 --- a/src/addons/mod/glossary/components/index/addon-mod-glossary-index.html +++ b/src/addons/mod/glossary/components/index/addon-mod-glossary-index.html @@ -98,7 +98,8 @@ - + diff --git a/src/addons/mod/glossary/components/index/index.ts b/src/addons/mod/glossary/components/index/index.ts index d79b755ea..fac7c7507 100644 --- a/src/addons/mod/glossary/components/index/index.ts +++ b/src/addons/mod/glossary/components/index/index.ts @@ -64,6 +64,7 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity moduleName = 'glossary'; isSearch = false; + hasSearched = false; canAdd = false; loadMoreError = false; loadingMessage?: string; @@ -429,6 +430,7 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity toggleSearch(): void { if (this.isSearch) { this.isSearch = false; + this.hasSearched = false; this.entries.setOnlineEntries(this.fetchedEntries, this.fetchedEntriesCanLoadMore); this.switchMode(this.fetchMode!); } else { @@ -488,6 +490,7 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity 'ASC', ); this.loaded = false; + this.hasSearched = true; this.loadContent(); } diff --git a/src/core/directives/directives.module.ts b/src/core/directives/directives.module.ts index f60c2d19d..84a102748 100644 --- a/src/core/directives/directives.module.ts +++ b/src/core/directives/directives.module.ts @@ -26,6 +26,7 @@ import { CoreSupressEventsDirective } from './supress-events'; import { CoreUserLinkDirective } from './user-link'; import { CoreAriaButtonClickDirective } from './aria-button'; import { CoreOnResizeDirective } from './on-resize'; +import { CoreDownloadFileDirective } from './download-file'; @NgModule({ declarations: [ @@ -41,6 +42,7 @@ import { CoreOnResizeDirective } from './on-resize'; CoreUserLinkDirective, CoreAriaButtonClickDirective, CoreOnResizeDirective, + CoreDownloadFileDirective, ], exports: [ CoreAutoFocusDirective, @@ -55,6 +57,7 @@ import { CoreOnResizeDirective } from './on-resize'; CoreUserLinkDirective, CoreAriaButtonClickDirective, CoreOnResizeDirective, + CoreDownloadFileDirective, ], }) export class CoreDirectivesModule {} diff --git a/src/core/directives/download-file.ts b/src/core/directives/download-file.ts new file mode 100644 index 000000000..b0d798584 --- /dev/null +++ b/src/core/directives/download-file.ts @@ -0,0 +1,63 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Directive, Input, OnInit, ElementRef } from '@angular/core'; +import { CoreFileHelper } from '@services/file-helper'; +import { CoreDomUtils } from '@services/utils/dom'; +import { CoreWSFile } from '@services/ws'; + +/** + * Directive to allow downloading and open a file. When the item with this directive is clicked, the file will be + * downloaded (if needed) and opened. + */ +@Directive({ + selector: '[core-download-file]', +}) +export class CoreDownloadFileDirective implements OnInit { + + @Input('core-download-file') file?: CoreWSFile; // The file to download. + @Input() component?: string; // Component to link the file to. + @Input() componentId?: string | number; // Component ID to use in conjunction with the component. + + protected element: HTMLElement; + + constructor(element: ElementRef) { + this.element = element.nativeElement; + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + this.element.addEventListener('click', async (ev: Event) => { + if (!this.file) { + return; + } + + ev.preventDefault(); + ev.stopPropagation(); + + const modal = await CoreDomUtils.showModalLoading(); + + try { + await CoreFileHelper.downloadAndOpenFile(this.file, this.component, this.componentId); + } catch (error) { + CoreDomUtils.showErrorModalDefault(error, 'core.errordownloading', true); + } finally { + modal.dismiss(); + } + }); + } + +} diff --git a/src/core/services/utils/utils.ts b/src/core/services/utils/utils.ts index 7777cd262..ddf711a7e 100644 --- a/src/core/services/utils/utils.ts +++ b/src/core/services/utils/utils.ts @@ -277,7 +277,10 @@ export class CoreUtilsProvider { return source; } - if (Array.isArray(source)) { + if (this.valueIsFileEntry(source)) { + // Don't clone FileEntry. It has a lot of depth and they shouldn't be modified. + return source; + } else if (Array.isArray(source)) { // Clone the array and all the entries. const newArray = [] as unknown as T; for (let i = 0; i < source.length; i++) { @@ -750,6 +753,18 @@ export class CoreUtilsProvider { return 'isFile' in file; } + /** + * Check if an unknown value is a FileEntry. + * + * @param value Value to check. + * @return Type guard indicating if the file is a FileEntry. + */ + valueIsFileEntry(file: unknown): file is FileEntry { + // We cannot use instanceof because FileEntry is a type. Check some of the properties. + return !!(file && typeof file == 'object' && 'isFile' in file && 'filesystem' in file && + 'toInternalURL' in file && 'copyTo' in file); + } + /** * Check if a value is an object. *