Merge pull request #4165 from alfonso-salces/MOBILE-2256
MOBILE-2256 privatefiles: Remove private filesmain
commit
4c9b69879e
|
@ -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",
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
<header>
|
||||
<ion-item button="false" detail="false" class="ion-text-wrap">
|
||||
<ion-thumbnail slot="start">
|
||||
<img [src]="icon" alt="" role="presentation" />
|
||||
</ion-thumbnail>
|
||||
|
||||
<ion-label>{{ filename }}</ion-label>
|
||||
|
||||
<ion-button shape="round" size="default" slot="end" fill="clear" [ariaLabel]="'core.close' | translate"
|
||||
(click)="close({ status: 'cancel' })">
|
||||
<ion-icon slot="icon-only" name="close" aria-label="hidden" />
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
</header>
|
||||
|
||||
<hr>
|
||||
|
||||
<ion-list>
|
||||
@if (isDownloaded) {
|
||||
<ion-item button detail="false" lines="none" (click)="close({ status: 'deleteOffline' })">
|
||||
<ion-icon slot="start" name="fam-cloud-x" aria-hidden="true" />
|
||||
<ion-label>{{ 'core.removedownloadeddata' | translate }}</ion-label>
|
||||
</ion-item>
|
||||
}
|
||||
|
||||
<ion-item button detail="false" (click)="close({ status: 'deleteOnline' })" lines="none">
|
||||
<ion-icon slot="start" name="fas-trash" aria-hidden="true" color="danger" />
|
||||
<ion-label> {{ 'core.delete' | translate }} </ion-label>
|
||||
</ion-item>
|
||||
</ion-list>
|
|
@ -0,0 +1,8 @@
|
|||
hr {
|
||||
background: var(--gray-300);
|
||||
}
|
||||
|
||||
ion-thumbnail {
|
||||
--size: 1.5rem;
|
||||
margin-inline-end: 0.5rem;
|
||||
}
|
|
@ -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<AddonPrivateFilesFileActionsComponentParams> {
|
||||
|
||||
@Input({ required: false }) isDownloaded = false;
|
||||
@Input({ required: true }) filename = '';
|
||||
@Input({ required: true }) icon = '';
|
||||
|
||||
constructor(elementRef: ElementRef<HTMLElement>) {
|
||||
super(elementRef);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export type AddonPrivateFilesFileActionsComponentParams = {
|
||||
status: 'cancel' | 'deleteOnline' | 'deleteOffline' | 'download';
|
||||
};
|
|
@ -0,0 +1,53 @@
|
|||
<ion-card class="card-file">
|
||||
|
||||
@if (file) {
|
||||
<ion-item button [ngClass]="{ 'file-selected': showCheckbox && selected }" class="ion-text-wrap item-file" [detail]="false">
|
||||
|
||||
@if (showCheckbox) {
|
||||
<ion-checkbox labelPlacement="start" [(ngModel)]="selected" (ngModelChange)="onSelectedFileChange.emit($event)" />
|
||||
} @else {
|
||||
<ion-thumbnail slot="start" (click)="download($event, true)">
|
||||
<img [src]="fileIcon" alt="" role="presentation" />
|
||||
</ion-thumbnail>
|
||||
}
|
||||
|
||||
<ion-label (click)="download($event, true)">
|
||||
<p class="item-heading">
|
||||
{{fileName}}
|
||||
|
||||
@if (state === statusDownloaded) {
|
||||
<ion-icon class="core-icon-downloaded" color="success" name="fam-cloud-done"
|
||||
[attr.aria-label]="'core.downloaded' | translate" role="status" />
|
||||
}
|
||||
</p>
|
||||
|
||||
|
||||
<p *ngIf="fileSizeReadable || showTime">
|
||||
<ng-container *ngIf="fileSizeReadable">{{ fileSizeReadable }}</ng-container>
|
||||
<ng-container *ngIf="fileSizeReadable && showTime"> · </ng-container>
|
||||
<ng-container *ngIf="showTime">{{ timemodified * 1000 | coreFormatDate }}</ng-container>
|
||||
</p>
|
||||
</ion-label>
|
||||
<div slot="end" class="flex-row">
|
||||
<ion-button fill="clear" *ngIf="isDownloaded && isIOS" (click)="openFile($event, true)" [title]="openButtonLabel | translate">
|
||||
<ion-icon slot="icon-only" [name]="openButtonIcon" aria-hidden="true" />
|
||||
</ion-button>
|
||||
|
||||
@if (!showCheckbox) {
|
||||
|
||||
@if (state !== statusDownloaded) {
|
||||
<core-download-refresh [status]="state" [enabled]="canDownload" [loading]="isDownloading" [canTrustDownload]="!alwaysDownload"
|
||||
(action)="download()" />
|
||||
}
|
||||
|
||||
@if (canDelete) {
|
||||
<ion-button (click)="openMenuClick()" fill="clear">
|
||||
<ion-icon name="ellipsis-vertical" slot="icon-only" aria-hidden="true" />
|
||||
</ion-button>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</ion-item>
|
||||
}
|
||||
|
||||
</ion-card>
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
// (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 { Component, EventEmitter, Input, OnDestroy, Output } from '@angular/core';
|
||||
import { CoreFileComponent } from '@components/file/file';
|
||||
|
||||
@Component({
|
||||
selector: 'addon-privatefiles-file',
|
||||
templateUrl: 'file.html',
|
||||
standalone: true,
|
||||
styleUrls: ['file.scss'],
|
||||
imports: [CoreSharedModule],
|
||||
})
|
||||
export class AddonPrivateFilesFileComponent extends CoreFileComponent implements OnDestroy {
|
||||
|
||||
@Input({ transform: toBoolean }) showCheckbox = true; // Show checkbox
|
||||
@Input({ transform: toBoolean, required: false }) selected = false; // Selected file.
|
||||
|
||||
@Output() onSelectedFileChange: EventEmitter<boolean>; // Will notify when the checkbox value changes.
|
||||
@Output() onOpenMenuClick: EventEmitter<CoreFileComponent>; // Will notify when menu clicked.
|
||||
|
||||
statusDownloaded = DownloadStatus.DOWNLOADED;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.onSelectedFileChange = new EventEmitter<boolean>();
|
||||
this.onOpenMenuClick = new EventEmitter<CoreFileComponent>();
|
||||
}
|
||||
|
||||
openMenuClick(): void {
|
||||
this.onOpenMenuClick.emit(this);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,11 +1,24 @@
|
|||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
@if (selectFilesEnabled()) {
|
||||
<ion-button fill="clear" [ariaLabel]="'core.close' | translate" (click)="cancelFileSelection()">
|
||||
<ion-icon slot="icon-only" name="fas-xmark" aria-hidden="true" />
|
||||
</ion-button>
|
||||
} @else {
|
||||
<ion-back-button [text]="'core.back' | translate" />
|
||||
}
|
||||
</ion-buttons>
|
||||
<ion-title>
|
||||
<h1>{{ title }}</h1>
|
||||
<h1>{{ selectFilesEnabled() ? (selectedFiles.length + ' ' + title) : title }}</h1>
|
||||
</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
@if (selectFilesEnabled()) {
|
||||
<ion-button fill="clear" (click)="deleteSelectedFiles(true)" [ariaLabel]="'core.delete' | translate" color="danger">
|
||||
<ion-icon slot="icon-only" name="fas-trash" aria-hidden="true" />
|
||||
</ion-button>
|
||||
}
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content class="limited-width">
|
||||
|
@ -28,6 +41,7 @@
|
|||
<!-- Display info about space used and space left. -->
|
||||
<ion-card class="core-info-card" *ngIf="userQuota && filesInfo && filesInfo.filecount > 0">
|
||||
<ion-item>
|
||||
<ion-icon slot="start" aria-label="hidden" name="fas-cloud" />
|
||||
<ion-label>
|
||||
{{ 'core.quotausage' | translate:{$a: {used: spaceUsed, total: userQuotaReadable} } }}
|
||||
</ion-label>
|
||||
|
@ -42,7 +56,15 @@
|
|||
<ion-icon name="fas-folder" slot="start" [attr.aria-label]="'core.folder' | translate" />
|
||||
<ion-label>{{file.filename}}</ion-label>
|
||||
</ion-item>
|
||||
<core-file *ngIf="!file.isdir" [file]="file" [component]="component" [componentId]="file.contextid" />
|
||||
|
||||
@if (!file.isdir) {
|
||||
<addon-privatefiles-file [file]="file" [component]="component" [componentId]="file.contextid"
|
||||
(onOpenMenuClick)="root === 'my' && openManagementFileMenu($event, file)"
|
||||
(longPress)="canDeleteFiles && root === 'my' && enableMultipleSelection(file)"
|
||||
[showCheckbox]="canDeleteFiles && root === 'my' && selectFilesEnabled()"
|
||||
(onSelectedFileChange)="root === 'my' && selectedFileValueChanged($event, file)" showDownloadStatus="true"
|
||||
[selected]="file.selected" [canDelete]="canDeleteFiles && root === 'my'" />
|
||||
}
|
||||
</ng-container>
|
||||
</ion-list>
|
||||
|
||||
|
@ -51,10 +73,20 @@
|
|||
</core-loading>
|
||||
|
||||
<!-- Upload a private file. -->
|
||||
<ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end" *ngIf="showUpload && root !== 'site' && !path">
|
||||
@if (showUpload && root !== 'site' && !path && !selectFilesEnabled()) {
|
||||
<ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end">
|
||||
<ion-fab-button (click)="uploadFile()" [attr.aria-label]="'core.fileuploader.uploadafile' | translate">
|
||||
<ion-icon name="fas-plus" aria-hidden="true" />
|
||||
<span class="sr-only">{{ 'core.fileuploader.uploadafile' | translate }}</span>
|
||||
</ion-fab-button>
|
||||
</ion-fab>
|
||||
}
|
||||
</ion-content>
|
||||
|
||||
@if (selectFilesEnabled()) {
|
||||
<div class="ion-padding addons-privatefiles-index-select-all">
|
||||
<ion-checkbox labelPlacement="end" [(ngModel)]="selectAll" (ngModelChange)="onSelectAllChanges($event)">
|
||||
{{ 'core.selectall' | translate }}
|
||||
</ion-checkbox>
|
||||
</div>
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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,10 @@ 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;
|
||||
canDeleteFiles = false;
|
||||
|
||||
protected updateSiteObserver: CoreEventObserver;
|
||||
protected logView: () => void;
|
||||
|
@ -80,7 +90,7 @@ export class AddonPrivateFilesIndexPage implements OnInit, OnDestroy {
|
|||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
async ngOnInit(): Promise<void> {
|
||||
try {
|
||||
this.root = CoreNavigator.getRouteParam('root');
|
||||
const contextId = CoreNavigator.getRouteNumberParam('contextid');
|
||||
|
@ -123,6 +133,8 @@ export class AddonPrivateFilesIndexPage implements OnInit, OnDestroy {
|
|||
} else {
|
||||
this.filesLoaded = true;
|
||||
}
|
||||
|
||||
this.canDeleteFiles = await AddonPrivateFiles.canDeletePrivateFiles();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -160,6 +172,8 @@ export class AddonPrivateFilesIndexPage implements OnInit, OnDestroy {
|
|||
this.fetchFiles().finally(() => {
|
||||
this.filesLoaded = true;
|
||||
});
|
||||
|
||||
this.cancelFileSelection();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -260,6 +274,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 +294,154 @@ export class AddonPrivateFilesIndexPage implements OnInit, OnDestroy {
|
|||
this.updateSiteObserver?.off();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete private files.
|
||||
*/
|
||||
async deleteSelectedFiles(showConfirmation = false): Promise<void> {
|
||||
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<void> {
|
||||
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 },
|
||||
true,
|
||||
);
|
||||
|
||||
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<void> {
|
||||
try {
|
||||
await CoreFilepool.removeFileByUrl(siteId, file.fileurl);
|
||||
} catch (error) {
|
||||
CoreDomUtils.showErrorModalDefault(error, 'core.errordeletefile', true);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<void> {
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
|
||||
const { draftitemid } = await site.write<AddonPrivateFilesPreparePrivateFilesForEditionWSResponse>(
|
||||
'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<boolean> {
|
||||
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[];
|
||||
};
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="31" height="25" viewBox="0 0 31 25" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.33887 9.82812C2.71387 10.7188 0.838867 13.25 0.838867 16.1562C0.838867 19.9062 3.83887 22.9062 7.58887 22.9062H24.8389C28.1201 22.9062 30.8389 20.2344 30.8389 16.9062C30.8389 14.0469 28.7764 11.6094 26.0107 11.0469C26.1982 10.5312 26.3389 10.0156 26.3389 9.40625C26.3389 6.92188 24.3232 4.90625 21.8389 4.90625C20.9014 4.90625 20.0107 5.1875 19.3076 5.70312C18.042 3.45312 15.6045 1.90625 12.8389 1.90625C8.66699 1.90625 5.33887 5.28125 5.33887 9.40625V9.82812ZM12.0448 9.88663C12.4842 9.44729 13.1965 9.44729 13.6358 9.88663L15.847 12.0978L18.0581 9.88673C18.4975 9.44739 19.2098 9.44739 19.6491 9.88673C20.0884 10.3261 20.0884 11.0384 19.6491 11.4777L17.438 13.6888L19.6489 15.8997C20.0883 16.3391 20.0883 17.0514 19.6489 17.4907C19.2096 17.9301 18.4973 17.9301 18.0579 17.4907L15.847 15.2798L13.636 17.4908C13.1967 17.9302 12.4843 17.9302 12.045 17.4908C11.6057 17.0515 11.6057 16.3392 12.045 15.8998L14.256 13.6888L12.0448 11.4776C11.6055 11.0383 11.6055 10.326 12.0448 9.88663Z" fill="#1D2125"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
|
@ -685,9 +685,13 @@ export class FileMock extends File {
|
|||
async removeFile(path: string, fileName: string): Promise<RemoveResult> {
|
||||
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' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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.<br>{{$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}}",
|
||||
|
|
|
@ -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<T>(component: Constructor<CoreModalComponent<T>>): Promise<T> {
|
||||
async openSheet<T>(
|
||||
component: Constructor<CoreModalComponent<T>>,
|
||||
componentProps: Record<string, unknown> = {},
|
||||
backdropDismiss = false,
|
||||
): Promise<T> {
|
||||
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<CoreSheetModalComponent<CoreModalComponent<T>>>(
|
||||
element,
|
||||
|
@ -85,6 +92,11 @@ export class CoreModalsService {
|
|||
|
||||
viewContainer?.setAttribute('aria-hidden', 'true');
|
||||
|
||||
if (backdropDismiss) {
|
||||
const backdrop = element.querySelector('ion-backdrop');
|
||||
backdrop?.addEventListener('ionBackdropTap', () => modal.close(new Error('Backdrop clicked')), { once: true });
|
||||
}
|
||||
|
||||
modal.result.finally(async () => {
|
||||
await sheetModal.hide();
|
||||
await AngularFrameworkDelegate.removeViewFromDom(container, element);
|
||||
|
|
Loading…
Reference in New Issue