From 491cc8dbcf44aace7f96fe197e9293bb80e8be48 Mon Sep 17 00:00:00 2001 From: Alfonso Salces Date: Fri, 20 Sep 2024 14:39:55 +0200 Subject: [PATCH] MOBILE-2256 privatefiles: Remove private files --- scripts/langindex.json | 7 +- .../components/file-actions/file-actions.html | 30 ++++ .../components/file-actions/file-actions.scss | 8 + .../components/file-actions/file-actions.ts | 41 +++++ .../privatefiles/components/file/file.html | 53 ++++++ .../privatefiles/components/file/file.scss | 14 ++ .../privatefiles/components/file/file.ts | 56 ++++++ .../privatefiles/pages/index/index.html | 37 +++- .../privatefiles/pages/index/index.scss | 10 ++ src/addons/privatefiles/pages/index/index.ts | 163 +++++++++++++++++- .../privatefiles/privatefiles-lazy.module.ts | 2 + .../privatefiles/services/privatefiles.ts | 46 +++++ .../fonts/moodle/font-awesome/cloud-x.svg | 3 + src/core/features/emulator/services/file.ts | 8 +- src/core/lang.json | 5 + src/core/services/modals.ts | 16 +- 16 files changed, 490 insertions(+), 9 deletions(-) create mode 100644 src/addons/privatefiles/components/file-actions/file-actions.html create mode 100644 src/addons/privatefiles/components/file-actions/file-actions.scss create mode 100644 src/addons/privatefiles/components/file-actions/file-actions.ts create mode 100644 src/addons/privatefiles/components/file/file.html create mode 100644 src/addons/privatefiles/components/file/file.scss create mode 100644 src/addons/privatefiles/components/file/file.ts create mode 100644 src/addons/privatefiles/pages/index/index.scss create mode 100644 src/assets/fonts/moodle/font-awesome/cloud-x.svg diff --git a/scripts/langindex.json b/scripts/langindex.json index f9a8e8b3c83..ac33a29f89b 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -90,8 +90,8 @@ "addon.blog.associatewithmodule": "blog", "addon.blog.associations": "blog", "addon.blog.blog": "blog", - "addon.blog.blogentries": "blog", "addon.blog.blogdeleteconfirm": "blog", + "addon.blog.blogentries": "blog", "addon.blog.entrybody": "blog", "addon.blog.entrytitle": "blog", "addon.blog.errorloadentries": "local_moodlemobileapp", @@ -1566,6 +1566,8 @@ "core.confirmleaveunknownchanges": "local_moodlemobileapp", "core.confirmloss": "local_moodlemobileapp", "core.confirmopeninbrowser": "local_moodlemobileapp", + "core.confirmremoveselectedfile": "local_moodlemobileapp", + "core.confirmremoveselectedfiles": "local_moodlemobileapp", "core.connectionlost": "local_moodlemobileapp", "core.considereddigitalminor": "moodle", "core.contactsupport": "local_moodlemobileapp", @@ -1809,6 +1811,7 @@ "core.expand": "moodle", "core.explanationdigitalminor": "moodle", "core.favourites": "moodle", + "core.filedeletedsuccessfully": "local_moodlemobileapp", "core.filename": "repository", "core.filenameexist": "local_moodlemobileapp", "core.filenotfound": "resource", @@ -2390,6 +2393,7 @@ "core.reminders.units": "qtype_numerical", "core.reminders.value": "local_moodlemobileapp", "core.remove": "moodle", + "core.removedownloadeddata": "local_moodlemobileapp", "core.removefiles": "local_moodlemobileapp", "core.reportbuilder.filtersapplied": "local_moodlemobileapp", "core.reportbuilder.hidecolumns": "local_moodlemobileapp", @@ -2431,6 +2435,7 @@ "core.selectacategory": "moodle", "core.selectacourse": "moodle", "core.selectagroup": "moodle", + "core.selectall": "local_moodlemobileapp", "core.send": "message", "core.sending": "chat", "core.serverconnection": "local_moodlemobileapp", diff --git a/src/addons/privatefiles/components/file-actions/file-actions.html b/src/addons/privatefiles/components/file-actions/file-actions.html new file mode 100644 index 00000000000..b516ef80ea2 --- /dev/null +++ b/src/addons/privatefiles/components/file-actions/file-actions.html @@ -0,0 +1,30 @@ +
+ + + + + + {{ filename }} + + + + + +
+ +
+ + + @if (isDownloaded) { + + + } + + + + diff --git a/src/addons/privatefiles/components/file-actions/file-actions.scss b/src/addons/privatefiles/components/file-actions/file-actions.scss new file mode 100644 index 00000000000..da555c7bcfe --- /dev/null +++ b/src/addons/privatefiles/components/file-actions/file-actions.scss @@ -0,0 +1,8 @@ +hr { + background: var(--gray-300); +} + +ion-thumbnail { + --size: 1.5rem; + margin-inline-end: 0.5rem; +} diff --git a/src/addons/privatefiles/components/file-actions/file-actions.ts b/src/addons/privatefiles/components/file-actions/file-actions.ts new file mode 100644 index 00000000000..3f4b70df50d --- /dev/null +++ b/src/addons/privatefiles/components/file-actions/file-actions.ts @@ -0,0 +1,41 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { CoreSharedModule } from '@/core/shared.module'; +import { ChangeDetectionStrategy, Component, ElementRef, Input } from '@angular/core'; +import { CoreModalComponent } from '@classes/modal-component'; + +@Component({ + selector: 'addon-privatefiles-file-actions', + styleUrl: './file-actions.scss', + templateUrl: 'file-actions.html', + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [CoreSharedModule], +}) +export class AddonPrivateFilesFileActionsComponent extends CoreModalComponent { + + @Input({ required: false }) isDownloaded = false; + @Input({ required: true }) filename = ''; + @Input({ required: true }) icon = ''; + + constructor(elementRef: ElementRef) { + super(elementRef); + } + +} + +export type AddonPrivateFilesFileActionsComponentParams = { + status: 'cancel' | 'deleteOnline' | 'deleteOffline' | 'download'; +}; diff --git a/src/addons/privatefiles/components/file/file.html b/src/addons/privatefiles/components/file/file.html new file mode 100644 index 00000000000..0c3e812cb37 --- /dev/null +++ b/src/addons/privatefiles/components/file/file.html @@ -0,0 +1,53 @@ + + + @if (file) { + + + @if (showCheckbox) { + + } @else { + + + + } + + +

+ {{fileName}} + + @if (state === statusDownloaded) { + + } +

+ + +

+ {{ fileSizeReadable }} + ยท + {{ timemodified * 1000 | coreFormatDate }} +

+
+
+ + + + @if (!showCheckbox) { + + @if (!isDownloaded && state !== statusDownloaded) { + + } + + @if (canDeleteFiles) { + + + } + } +
+
+ } + +
diff --git a/src/addons/privatefiles/components/file/file.scss b/src/addons/privatefiles/components/file/file.scss new file mode 100644 index 00000000000..5fed5bdbfce --- /dev/null +++ b/src/addons/privatefiles/components/file/file.scss @@ -0,0 +1,14 @@ +:host { + ion-checkbox { + flex: none; + width: 3rem; + } + + ion-item.item.item-file { + &.file-selected { + --ion-item-background: var(--primary-tint); + } + + --inner-border-width: 0 !important; + } +} diff --git a/src/addons/privatefiles/components/file/file.ts b/src/addons/privatefiles/components/file/file.ts new file mode 100644 index 00000000000..eaa11722cd8 --- /dev/null +++ b/src/addons/privatefiles/components/file/file.ts @@ -0,0 +1,56 @@ +// (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 { DownloadStatus } from '@/core/constants'; +import { CoreSharedModule } from '@/core/shared.module'; +import { toBoolean } from '@/core/transforms/boolean'; +import { AddonPrivateFiles } from '@addons/privatefiles/services/privatefiles'; +import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; +import { CoreFileComponent } from '@components/file/file'; +import { CoreSites } from '@services/sites'; + +@Component({ + selector: 'addon-privatefiles-file', + templateUrl: 'file.html', + standalone: true, + styleUrls: ['file.scss'], + imports: [CoreSharedModule], +}) +export class AddonPrivateFilesFileComponent extends CoreFileComponent implements OnInit, OnDestroy { + + @Input({ transform: toBoolean }) showCheckbox = true; // Show checkbox + @Input({ transform: toBoolean, required: false }) selected = false; // Selected file. + + @Output() onSelectedFileChange: EventEmitter; // Will notify when the checkbox value changes. + @Output() onOpenMenuClick: EventEmitter; // Will notify when menu clicked. + + statusDownloaded = DownloadStatus.DOWNLOADED; + canDeleteFiles = false; + + constructor() { + super(); + this.onSelectedFileChange = new EventEmitter(); + this.onOpenMenuClick = new EventEmitter(); + } + + async ngOnInit(): Promise { + const siteId = CoreSites.getCurrentSiteId(); + this.canDeleteFiles = await AddonPrivateFiles.canDeletePrivateFiles(siteId); + await super.ngOnInit(); + } + + openMenuClick(): void { + this.onOpenMenuClick.emit(this); + } + +} diff --git a/src/addons/privatefiles/pages/index/index.html b/src/addons/privatefiles/pages/index/index.html index 14080063a6d..2a6be198c46 100644 --- a/src/addons/privatefiles/pages/index/index.html +++ b/src/addons/privatefiles/pages/index/index.html @@ -1,11 +1,24 @@ + @if (selectFilesEnabled()) { + + + } @else { + } -

{{ title }}

+

{{ selectFilesEnabled() ? (selectedFiles.length + ' ' + title) : title }}

+ + @if (selectFilesEnabled()) { + + + } +
@@ -28,6 +41,7 @@

{{ title }}

+ {{ 'core.quotausage' | translate:{$a: {used: spaceUsed, total: userQuotaReadable} } }} @@ -42,7 +56,14 @@

{{ title }}

{{file.filename}}
- + + @if (!file.isdir) { + + } @@ -51,10 +72,20 @@

{{ title }}

- + @if (showUpload && root !== 'site' && !path && !selectFilesEnabled()) { + + }
+ +@if (selectFilesEnabled()) { +
+ + {{ 'core.selectall' | translate }} + +
+} diff --git a/src/addons/privatefiles/pages/index/index.scss b/src/addons/privatefiles/pages/index/index.scss new file mode 100644 index 00000000000..6631178acfd --- /dev/null +++ b/src/addons/privatefiles/pages/index/index.scss @@ -0,0 +1,10 @@ + + +:host { + --addons-privatefiles-index-select-all-shadow: 0px 8px 10px 0px #282828; + + .addons-privatefiles-index-select-all { + box-shadow: var(--addons-privatefiles-index-select-all-shadow); + z-index: 3; + } +} diff --git a/src/addons/privatefiles/pages/index/index.ts b/src/addons/privatefiles/pages/index/index.ts index 0dc3cc7202c..a0a36fe75bb 100644 --- a/src/addons/privatefiles/pages/index/index.ts +++ b/src/addons/privatefiles/pages/index/index.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, OnDestroy, OnInit } from '@angular/core'; +import { Component, OnDestroy, OnInit, signal } from '@angular/core'; import { Md5 } from 'ts-md5/dist/md5'; import { CoreNetwork } from '@services/network'; @@ -33,6 +33,11 @@ import { CoreUtils } from '@services/utils/utils'; import { CoreNavigator } from '@services/navigator'; import { CoreTime } from '@singletons/time'; import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; +import { CoreModals } from '@services/modals'; +import { CoreFilepool } from '@services/filepool'; +import { CoreToasts, ToastDuration } from '@services/toasts'; +import { CoreLoadings } from '@services/loadings'; +import { AddonPrivateFilesFileComponent } from '@addons/privatefiles/components/file/file'; /** * Page that displays the list of files. @@ -40,6 +45,7 @@ import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; @Component({ selector: 'page-addon-privatefiles-index', templateUrl: 'index.html', + styleUrls: ['./index.scss'], }) export class AddonPrivateFilesIndexPage implements OnInit, OnDestroy { @@ -56,6 +62,9 @@ export class AddonPrivateFilesIndexPage implements OnInit, OnDestroy { files?: AddonPrivateFilesFile[]; // List of files. component!: string; // Component to link the file downloads to. filesLoaded = false; // Whether the files are loaded. + selectFilesEnabled = signal(false); + selectedFiles: AddonPrivateFilesFile[] = []; + selectAll = false; protected updateSiteObserver: CoreEventObserver; protected logView: () => void; @@ -160,6 +169,8 @@ export class AddonPrivateFilesIndexPage implements OnInit, OnDestroy { this.fetchFiles().finally(() => { this.filesLoaded = true; }); + + this.cancelFileSelection(); } /** @@ -260,6 +271,7 @@ export class AddonPrivateFilesIndexPage implements OnInit, OnDestroy { itemid: folder.itemid || 0, filepath: folder.filepath || '', filename: folder.filename || '', + root: this.root, }; if (folder.component) { @@ -279,4 +291,153 @@ export class AddonPrivateFilesIndexPage implements OnInit, OnDestroy { this.updateSiteObserver?.off(); } + /** + * Delete private files. + */ + async deleteSelectedFiles(showConfirmation = false): Promise { + if (showConfirmation) { + try { + await CoreDomUtils.showDeleteConfirm('core.confirmremoveselectedfiles'); + } catch { + return; + } + } + + const siteId = CoreSites.getCurrentSiteId(); + const loading = await CoreLoadings.show(); + + try { + await AddonPrivateFiles.deleteFiles(this.selectedFiles); + } catch (error) { + loading.dismiss(); + await CoreDomUtils.showErrorModalDefault(error, 'An error occourred while file was being deleted.'); + throw error; + } + + for (const file of this.selectedFiles) { + await this.deleteOfflineFile(file, siteId); + } + + await this.refreshFiles(); + loading.dismiss(); + + const message = Translate.instant( + 'core.filedeletedsuccessfully', + { + filename: this.selectedFiles.length === 1 + ? this.selectedFiles[0].filename + : (this.selectedFiles.length + ' ' + Translate.instant('addon.privatefiles.files')), + }, + ); + + this.selectedFiles = []; + this.selectFilesEnabled.set(false); + await CoreToasts.show({ message, translateMessage: false, duration: ToastDuration.SHORT }); + } + + /** + * File selection changes. + * + * @param selected selection value. + * @param file File selection. + */ + selectedFileValueChanged(selected: boolean, file: AddonPrivateFilesFile): void { + if (selected) { + this.selectedFiles.push(file); + + return; + } + + this.selectedFiles = this.selectedFiles.filter(selectedFile => selectedFile !== file); + } + + /** + * Cancel file selection. + */ + cancelFileSelection(): void { + this.selectFilesEnabled.set(false); + this.selectedFiles = []; + } + + /** + * Open file management menu. + * + * @param instance AddonPrivateFilesFileComponent instance. + * @param file File to manage. + * + * @returns Promise done. + */ + async openManagementFileMenu(instance: AddonPrivateFilesFileComponent, file: AddonPrivateFilesFile): Promise { + const siteId = CoreSites.getCurrentSiteId(); + const { AddonPrivateFilesFileActionsComponent } = await import('@addons/privatefiles/components/file-actions/file-actions'); + + try { + const { status } = await CoreModals.openSheet( + AddonPrivateFilesFileActionsComponent, + { isDownloaded: instance.isDownloaded, filename: file.filename, icon: instance.fileIcon }, + ); + + if (status === 'cancel') { + return; + } + + if (status === 'deleteOnline') { + await CoreDomUtils.showDeleteConfirm('core.confirmremoveselectedfile', { filename: file.filename }); + this.selectedFiles = [file]; + + return await this.deleteSelectedFiles(); + } + + if (status === 'deleteOffline') { + return await this.deleteOfflineFile(file, siteId); + } + + await instance.download(); + } catch { + return; + } + } + + /** + * Select all changes + * + * @param checked Select all toggle value. + */ + onSelectAllChanges(checked: boolean): void { + if (!this.files) { + return; + } + + for (const file of this.files) { + file.selected = checked; + } + + this.selectedFiles = checked ? [...this.files] : []; + } + + /** + * Enables multiple file selection and mark as selected the passed file. + * + * @param file File to be selected. + */ + enableMultipleSelection(file: AddonPrivateFilesFile): void { + this.selectFilesEnabled.set(true); + this.selectedFiles.push(file); + file.selected = true; + } + + /** + * Remove offline file. + * + * @param file File to remove. + * @param siteId Site ID. + */ + async deleteOfflineFile(file: AddonPrivateFilesFile, siteId: string): Promise { + try { + await CoreFilepool.removeFileByUrl(siteId, file.fileurl); + } catch (error) { + CoreDomUtils.showErrorModalDefault(error, 'core.errordeletefile', true); + } + } + } diff --git a/src/addons/privatefiles/privatefiles-lazy.module.ts b/src/addons/privatefiles/privatefiles-lazy.module.ts index b3a68659b84..8677298e31b 100644 --- a/src/addons/privatefiles/privatefiles-lazy.module.ts +++ b/src/addons/privatefiles/privatefiles-lazy.module.ts @@ -18,6 +18,7 @@ import { Injector, NgModule } from '@angular/core'; import { ROUTES, Routes } from '@angular/router'; import { buildTabMainRoutes } from '@features/mainmenu/mainmenu-tab-routing.module'; +import { AddonPrivateFilesFileComponent } from './components/file/file'; /** * Build module routes. @@ -45,6 +46,7 @@ function buildRoutes(injector: Injector): Routes { @NgModule({ imports: [ CoreSharedModule, + AddonPrivateFilesFileComponent, ], declarations: [ AddonPrivateFilesIndexPage, diff --git a/src/addons/privatefiles/services/privatefiles.ts b/src/addons/privatefiles/services/privatefiles.ts index c638bda9c2f..3cb2cd9b2aa 100644 --- a/src/addons/privatefiles/services/privatefiles.ts +++ b/src/addons/privatefiles/services/privatefiles.ts @@ -20,6 +20,7 @@ import { CoreWSExternalWarning } from '@services/ws'; import { CoreSite } from '@classes/sites/site'; import { makeSingleton } from '@singletons'; import { ContextLevel } from '@/core/constants'; +import { CoreFileUploader } from '@features/fileuploader/services/fileuploader'; const ROOT_CACHE_KEY = 'mmaFiles:'; @@ -388,6 +389,41 @@ export class AddonPrivateFilesProvider { return site.write('core_user_add_user_private_files', params, preSets); } + /** + * Delete a private file. + * + * @param files Private files to remove. + * @param siteId Site ID. + */ + async deleteFiles(files: AddonPrivateFilesFile[], siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + const { draftitemid } = await site.write( + 'core_user_prepare_private_files_for_edition', + {}, + ); + + await CoreFileUploader.deleteDraftFiles(draftitemid, files.map(file => ({ + filename: file.filename, + filepath: file.filepath, + }))); + + await site.write('core_user_update_private_files', { draftitemid }); + } + + /** + * Can delete private files in site. + * + * @param siteId Site ID + * + * @returns true or false. + */ + async canDeletePrivateFiles(siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + return site.wsAvailable('core_user_update_private_files') && site.canUseAdvancedFeature('privatefiles'); + } + } export const AddonPrivateFiles = makeSingleton(AddonPrivateFilesProvider); @@ -417,6 +453,7 @@ export type AddonPrivateFilesFile = { export type AddonPrivateFilesFileCalculatedData = { fileurl: string; // File URL, using same name as CoreWSExternalFile. imgPath?: string; // Path to file icon's image. + selected?: boolean; }; /** * Params of WS core_files_get_files. @@ -472,3 +509,12 @@ export type AddonPrivateFilesGetUserInfoWSResult = { type AddonPrivateFilesAddUserPrivateFilesWSParams = { draftid: number; // Draft area id. }; + +/** + * Body of core_user_prepare_private_files_for_edition WS response. + */ +type AddonPrivateFilesPreparePrivateFilesForEditionWSResponse = { + areaoptions: { name: string; value: string | number }[]; + draftitemid: number; + warnings?: CoreWSExternalWarning[]; +}; diff --git a/src/assets/fonts/moodle/font-awesome/cloud-x.svg b/src/assets/fonts/moodle/font-awesome/cloud-x.svg new file mode 100644 index 00000000000..d8059e7e6f0 --- /dev/null +++ b/src/assets/fonts/moodle/font-awesome/cloud-x.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/core/features/emulator/services/file.ts b/src/core/features/emulator/services/file.ts index 8dd430a36e9..ba81d71e571 100644 --- a/src/core/features/emulator/services/file.ts +++ b/src/core/features/emulator/services/file.ts @@ -685,9 +685,13 @@ export class FileMock extends File { async removeFile(path: string, fileName: string): Promise { const parentDir = await this.resolveDirectoryUrl(path); - const fileEntry = await this.getFile(parentDir, fileName, { create: false }); + try { + const fileEntry = await this.getFile(parentDir, fileName, { create: false }); - return this.removeMock(fileEntry); + return this.removeMock(fileEntry); + } catch { + throw { code: 1, message: 'NOT_FOUND_ERR' }; + } } /** diff --git a/src/core/lang.json b/src/core/lang.json index 749bea4b696..890ed033172 100644 --- a/src/core/lang.json +++ b/src/core/lang.json @@ -53,6 +53,8 @@ "confirmleaveunknownchanges": "Are you sure you want to leave this page? If you have unsaved changes they will be lost.", "confirmloss": "Are you sure? All changes will be lost.", "confirmopeninbrowser": "Do you want to open it in a web browser?", + "confirmremoveselectedfile": "This will permanently delete '{{filename}}'. You can't undo this.", + "confirmremoveselectedfiles": "This will permanently delete selected files. You can't undo this.", "connectionlost": "Connection to site lost", "considereddigitalminor": "You are too young to create an account on this site.", "contactsupport": "Contact support", @@ -131,6 +133,7 @@ "expand": "Expand", "explanationdigitalminor": "This information is required to determine if your age is over the digital age of consent. This is the age when an individual can consent to terms and conditions and their data being legally stored and processed.", "favourites": "Starred", + "filedeletedsuccessfully": "You have deleted '{{filename}}' succesfully", "filename": "Filename", "filenameexist": "File name already exists: {{$a}}", "filenotfound": "File not found, sorry.", @@ -264,6 +267,7 @@ "redirectingtosite": "You will be redirected to the site.", "refresh": "Refresh", "remove": "Remove", + "removedownloadeddata": "Remove downloaded data", "removefiles": "Remove files {{$a}}", "required": "Required", "requireduserdatamissing": "This user lacks some required profile data. Please enter the data in your site and try again.
{{$a}}", @@ -287,6 +291,7 @@ "selectacategory": "Please select a category", "selectacourse": "Select a course", "selectagroup": "Select a group", + "selectall": "Select all", "send": "Send", "sending": "Sending", "serverconnection": "Error connecting to the server: {{details}}", diff --git a/src/core/services/modals.ts b/src/core/services/modals.ts index 91d9a67b424..4bfb6b2bbb0 100644 --- a/src/core/services/modals.ts +++ b/src/core/services/modals.ts @@ -67,15 +67,22 @@ export class CoreModalsService { * Open a sheet modal component. * * @param component Component to render inside the modal. + * @param componentProps Component to render inside the modal. + * @param backdropDismiss Dismiss on backdrop click. + * * @returns Modal result once it's been closed. */ - async openSheet(component: Constructor>): Promise { + async openSheet( + component: Constructor>, + componentProps: Record = {}, + backdropDismiss = true, + ): Promise { const container = document.querySelector('ion-app') ?? document.body; const viewContainer = container.querySelector('ion-router-outlet, ion-nav, #ion-view-container-root'); const element = await AngularFrameworkDelegate.attachViewToDom( container, CoreSheetModalComponent, - { component }, + { component, componentProps }, ); const sheetModal = CoreDirectivesRegistry.require>>( element, @@ -85,6 +92,11 @@ export class CoreModalsService { viewContainer?.setAttribute('aria-hidden', 'true'); + if (backdropDismiss) { + const backdrop = document.getElementsByTagName('ion-backdrop')?.[0]; + backdrop?.addEventListener('ionBackdropTap', () => modal.close(new Error('Backdrop clicked')), { once: true }); + } + modal.result.finally(async () => { await sheetModal.hide(); await AngularFrameworkDelegate.removeViewFromDom(container, element);