Skip to content

Commit

Permalink
Bug #14108: Fix bugs (Incorrect size: When uploading a directory from…
Browse files Browse the repository at this point in the history
… the file explorer, the displayed size is 0 bytes, Data loss: Several pieces of data are lost after validation.)

Bug #14108: In drag & drop, handling empty folders (If the empty folder is located inside another parent directory, an error message will be displayed).

Bug #14108: Change wording.

Bug #14108: With upload files/directories, handling empty folders (If the empty folder is located inside another parent directory, an error message will be displayed).

Bug #14108: fix removing directories in drag& drop.

Bug #14108: fix TUs front.

Bug #14108: refactoring - rename custome file.
  • Loading branch information
Salim Terres committed Jan 27, 2025
1 parent f0a0bfd commit 328fa11
Show file tree
Hide file tree
Showing 12 changed files with 228 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
[multipleFiles]="true"
[maxSizeInBytes]="maxSizeInBytes"
[directoryMode]="true"
[zipFile]="zipFile"
(filesChanged)="setFilesToUpload($event)"
>
</vitamui-file-selector>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export class AddUnitsComponent implements OnInit {
protected readonly maxSizeInBytes = 10 * Math.pow(1024, 3); // 10 Gb
public stepIndex = 0;
public stepCount = 2;
public zipFile: ZipFile;

isLoading = false;

Expand All @@ -94,6 +95,7 @@ export class AddUnitsComponent implements OnInit {
}

private loadAttachementUnits() {
this.zipFile = new ZipFile(this.data.transaction.id);
const sortingCriteria = { criteria: 'Title', sorting: Direction.ASCENDANT };
const criteriaWithId: SearchCriteriaEltDto = {
criteria: 'DescriptionLevel',
Expand Down Expand Up @@ -136,17 +138,17 @@ export class AddUnitsComponent implements OnInit {

setFilesToUpload(files: File[]) {
this.filesToUpload = files;
this.zipFile = this.zipFile.addFiles(this.filesToUpload);
}

validateAndUpload() {
this.isLoading = true;
const zipFile = new ZipFile(this.data.transaction.id);
from(zipFile.addFiles(this.filesToUpload).generateZip())
from(this.zipFile.generateZip())
.pipe(
switchMap((content) =>
this.archiveCollectService.uploadZip(content, this.data.transaction.id, this.linkParentIdControl.value.included[0]),
),
tap((httpEvent) => zipFile.updateUploadingZipFileStatus(httpEvent)),
tap((httpEvent) => this.zipFile.updateUploadingZipFileStatus(httpEvent)),
last((httpEvent) => httpEvent.type === HttpEventType.Response),
finalize(() => (this.isLoading = false)),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@
[multipleFiles]="true"
[maxSizeInBytes]="uploadMaxSizeInBytes"
[directoryMode]="true"
[zipFile]="zipFile"
(filesChanged)="setFilesToUpload($event)"
>
</vitamui-file-selector>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export class CreateProjectComponent implements OnInit, AfterViewChecked {
selectedFlowType: FlowType = FlowType.FIX;
public stepIndex = 0;
public stepCount = 6;
public zipFile = new ZipFile();

projectForm: FormGroup;

Expand Down Expand Up @@ -188,6 +189,7 @@ export class CreateProjectComponent implements OnInit, AfterViewChecked {

setFilesToUpload(files: File[]) {
this.filesToUpload = files;
this.zipFile = this.zipFile.addFiles(this.filesToUpload);
}

/*** Form validator Step : Description du versement ***/
Expand Down Expand Up @@ -341,8 +343,7 @@ export class CreateProjectComponent implements OnInit, AfterViewChecked {
this.isLoading = true;
const project: Project = this.formToProject();
this.moveToNextStep();
const zipFile = new ZipFile();
this.zipFileStatus$ = zipFile.zipFileStatus$;
this.zipFileStatus$ = this.zipFile.zipFileStatus$;
this.projectsService
.create(project)
.pipe(
Expand All @@ -356,9 +357,9 @@ export class CreateProjectComponent implements OnInit, AfterViewChecked {
),
switchMap((transaction) => this.transactionsService.create(transaction)),
tap((createdTransactionResponse) => (transactionId = createdTransactionResponse.id)),
switchMap(() => zipFile.addFiles(this.filesToUpload).generateZip()),
switchMap(() => this.zipFile.generateZip()),
switchMap((content) => this.archiveCollectService.uploadZip(content, transactionId)),
tap((httpEvent) => zipFile.updateUploadingZipFileStatus(httpEvent)),
tap((httpEvent) => this.zipFile.updateUploadingZipFileStatus(httpEvent)),
last((httpEvent) => httpEvent.type === HttpEventType.Response),
finalize(() => {
this.isLoading = false;
Expand Down
1 change: 1 addition & 0 deletions ui/ui-frontend/projects/collect/src/assets/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"LOAD_MORE_RESULTS": "Load more results...",
"NO_ACCESS_CONTRACT": "No access contract is associated with the user",
"UPLOAD_FILE_ALREADY_IMPORTED": "File already imported!",
"UPLOAD_EMPTY_FOLDER_IMPORTED": "The imported folder contains at least one empty folder!",
"PROJECT_DESCRIPTION_SUB_TITLE": "Remittance description",
"PROJECT_DESCRIPTION_LABEL": "Entitled",
"PROJECT_DESCRIPTION_DESC": "Description",
Expand Down
1 change: 1 addition & 0 deletions ui/ui-frontend/projects/collect/src/assets/i18n/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"LOAD_MORE_RESULTS": "Afficher plus de résultats...",
"NO_ACCESS_CONTRACT": "Aucun contrat d'accès n'est associé à l'utilisateur",
"UPLOAD_FILE_ALREADY_IMPORTED": "Dossier déjà importer !",
"UPLOAD_EMPTY_FOLDER_IMPORTED": "Le dossier importé comporte au moins un dossier vide!",
"PROJECT_DESCRIPTION_SUB_TITLE": "Description du versement",
"PROJECT_DESCRIPTION_LABEL": "Intitulé",
"PROJECT_DESCRIPTION_DESC": "Description",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
(change)="handleFilesSelection($event.target.files)"
[accept]="extensions?.join(',')"
[multiple]="multipleFiles"
[attr.webkitdirectory]="directoryMode ? '' : null"
*ngIf="!directoryMode"
/>

@if (fileList) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import { FileSelectorComponent } from './file-selector.component';
import { TranslateModule } from '@ngx-translate/core';
import { PipesModule } from '../../pipes/pipes.module';
import { LoggerModule } from '../../logger';
import { ZipFile } from '../../../../lib/models/zip/zip-file.class';

describe('FileSelectorComponent', () => {
let component: FileSelectorComponent;
Expand All @@ -51,6 +52,7 @@ describe('FileSelectorComponent', () => {

fixture = TestBed.createComponent(FileSelectorComponent);
component = fixture.componentInstance;
component.zipFile = new ZipFile();
fixture.detectChanges();
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,21 @@
*/
import { Component, ContentChild, ElementRef, EventEmitter, Input, Output, TemplateRef, ViewChild } from '@angular/core';
import { DragAndDropDirective } from '../../directives/drag-and-drop/drag-and-drop.directive';
import { TranslateModule } from '@ngx-translate/core';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { NgForOf, NgIf, NgTemplateOutlet } from '@angular/common';
import { PipesModule } from '../../pipes/pipes.module';
import { DisplayFile } from './display-file.interface';
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
import { ZipFile } from '../../../../lib/models/zip/zip-file.class';
import { CustomFile } from '../../../../lib/models/file';
import { Logger } from '../../logger';

@Component({
selector: 'vitamui-file-selector',
templateUrl: './file-selector.component.html',
styleUrl: './file-selector.component.scss',
standalone: true,
imports: [DragAndDropDirective, TranslateModule, NgIf, NgForOf, PipesModule, NgTemplateOutlet],
imports: [DragAndDropDirective, TranslateModule, NgIf, NgForOf, PipesModule, NgTemplateOutlet, MatSnackBarModule],
})
export class FileSelectorComponent {
/**
Expand All @@ -57,6 +61,7 @@ export class FileSelectorComponent {
/** only a directory can be chosen */
@Input() directoryMode = false;
@Input() maxSizeInBytes: number; // TODO: do some control on the file size?
@Input() zipFile: ZipFile;

@ViewChild('inputFiles') inputFiles: ElementRef;

Expand All @@ -66,12 +71,25 @@ export class FileSelectorComponent {
@Output() filesChanged = new EventEmitter<File[]>();

private files: File[] = [];
private filesWithRelativePath: CustomFile[] = [];
protected displayFiles: DisplayFile[] = [];

constructor(
private translationService: TranslateService,
private snackBar: MatSnackBar,
private logger: Logger,
) {}

handleFilesSelection(files: FileList | File[]) {
if (!this.multipleFiles && this.files.length > 0) {
if (!this.multipleFiles && this.files?.length > 0) {
return;
}

if (this.zipFile.directoryExistInZipFile(files)) {
this.snackBar.open(this.translationService.instant('COLLECT.UPLOAD_FILE_ALREADY_IMPORTED'), null, { duration: 3000 });
return;
}

// Filter to keep only the ones matching extension list (useful for drag & drop and to make sure no other type has been selected)
const filteredFiles = Array.from(files)
.filter((file) => !this.extensions?.length || this.extensions.some((ext) => file.name.toLowerCase().endsWith(ext.toLowerCase())))
Expand All @@ -94,42 +112,89 @@ export class FileSelectorComponent {
this.resetInput();
}

private getDirectory(files: FileList | File[]): DisplayFile {
let path = files[0].webkitRelativePath;
if (path.indexOf('/') !== -1) {
path = path.split('/')[0];
return {
name: path,
size: Array.from(files).reduce((acc, file) => acc + file.size, 0),
directory: true,
};
}
return null;
}

openFileSelectorOSDialog() {
this.inputFiles.nativeElement.click();
this.directoryMode ? this.onSelectFolder() : this.inputFiles.nativeElement.click();
}

removeFile(displayFile: DisplayFile) {
if (displayFile.directory) {
this.files = this.files.filter((file) => !file.webkitRelativePath.startsWith(displayFile.name));
this.files = this.files.filter(
(file: CustomFile) =>
!file.costumeRelativePath?.startsWith(displayFile.name) || !file.webkitRelativePath?.startsWith(displayFile.name),
);
} else {
this.files.splice(
this.files.findIndex((file) => file.name === displayFile.name),
1,
);
}
this.filesChanged.emit(this.files);
this.zipFile.remove(displayFile.name);
this.displayFiles.splice(this.displayFiles.indexOf(displayFile), 1);
}

/**
* Reset the value to allow a new "change" event.
*/
private resetInput(): void {
this.filesWithRelativePath = [];
if (this.inputFiles) {
this.inputFiles.nativeElement.value = '';
}
}

private getDirectory(files: FileList | any[]): DisplayFile {
if (files.length === 0) return null;
const firstFile: any = files[0];
let path = firstFile?.costumeRelativePath || firstFile?.webkitRelativePath || firstFile?.relativePath || firstFile?.name;
if (path?.indexOf('/') !== -1) {
path = path?.split('/')[0];
return {
name: path,
size: Array.from(files).reduce((acc, file) => acc + file.size, 0),
directory: true,
};
}
return null;
}

async onSelectFolder(): Promise<void> {
try {
// Prompt the user to select a folder
const directoryHandle = await (window as any)?.showDirectoryPicker();

// Browse through folders and files
await this.readDirectory(directoryHandle);

this.handleFilesSelection(this.filesWithRelativePath);
} catch (err) {
this.logger.error(this, err);
}
}

private async isFolderEmpty(folderHandle: any): Promise<boolean> {
for await (const _ of folderHandle.values()) {
return false; // If an item is found, the folder is not empty
}
return true; // No item found, the folder is empty
}

private async readDirectory(directoryHandle: any, currentPath = directoryHandle?.name || ''): Promise<any> {
for await (const [name, handle] of directoryHandle.entries()) {
if (handle.kind === 'file') {
// If it's a file, add it with its relative path
const file: CustomFile = await handle.getFile();
file.costumeRelativePath = `${currentPath}/${name}`;
this.filesWithRelativePath = [...this.filesWithRelativePath, file];
} else if (handle.kind === 'directory') {
const subFolderEmpty = await this.isFolderEmpty(handle);
if (subFolderEmpty) {
this.snackBar.open(this.translationService.instant('COLLECT.UPLOAD_EMPTY_FOLDER_IMPORTED'), null, { duration: 3000 });
}

// If it's a folder, read its contents recursively
await this.readDirectory(handle, `${currentPath}/${name}`);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@
* knowledge of the CeCILL-C license and that you accept its terms.
*/
import { Directive, ElementRef, EventEmitter, HostListener, Input, Output } from '@angular/core';
import { Logger } from '../../logger/logger';
import { Logger } from '../../logger';
import { TranslateService } from '@ngx-translate/core';
import { MatSnackBar } from '@angular/material/snack-bar';

@Directive({
standalone: true,
Expand All @@ -55,6 +57,8 @@ export class DragAndDropDirective {
@Input() hoverClass = 'on-over';

constructor(
private translationService: TranslateService,
private snackBar: MatSnackBar,
private elementRef: ElementRef<HTMLElement>,
private logger: Logger,
) {}
Expand Down Expand Up @@ -82,7 +86,12 @@ export class DragAndDropDirective {
event.stopPropagation();
this.elementRef.nativeElement.classList.remove(this.hoverClass);

let fileEntries = await this.getAllFileEntries(event.dataTransfer.items);
const items = event.dataTransfer?.items;
if (!items) return;

await this.findEmptySubdirectories(items);

let fileEntries = await this.getAllFileEntries(items);
// Filter files
if (!this.enableFileDragAndDrop) {
fileEntries = fileEntries.filter((fileEntry) => fileEntry.fullPath.split('/').length - 1 !== 1);
Expand Down Expand Up @@ -173,4 +182,43 @@ export class DragAndDropDirective {
this.logger.error(this, err);
}
};

private async findEmptySubdirectories(items: DataTransferItemList) {
for (const item of Array.from(items)) {
const entry = item.webkitGetAsEntry();
if (entry && entry.isDirectory) {
this.checkSubdirectories(entry)
.then((emptyFolders) => {
if (emptyFolders?.length)
this.snackBar.open(this.translationService.instant('COLLECT.UPLOAD_EMPTY_FOLDER_IMPORTED'), null, { duration: 3000 });
})
.catch((err) => this.logger.error(this, err));
}
}
}

private async checkSubdirectories(directoryEntry: any) {
let emptyFolders: string[] = [];
const reader = directoryEntry.createReader();

reader.readEntries(async (entries: any[]) => {
for (const entry of entries) {
if (entry.isDirectory) {
const isEmpty = await this.isDirectoryEmpty(entry);
if (isEmpty) emptyFolders.push(entry.name);
}
}
});
return emptyFolders;
}

private isDirectoryEmpty(directoryEntry: any): Promise<boolean> {
return new Promise((resolve) => {
const reader = directoryEntry.createReader();
reader.readEntries((entries: any[]) => {
// A folder is empty if it contains neither files nor subfolders
resolve(entries.length === 0);
});
});
}
}
Loading

0 comments on commit 328fa11

Please sign in to comment.