Skip to content

Commit

Permalink
Bug #14108 [Collection] - [Issues with Adding Directory to a Deposit …
Browse files Browse the repository at this point in the history
…Project]
  • Loading branch information
Salim Terres committed Feb 4, 2025
1 parent 3d3c8db commit 61e0128
Show file tree
Hide file tree
Showing 6 changed files with 195 additions and 22 deletions.
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
3 changes: 2 additions & 1 deletion ui/ui-frontend/projects/collect/src/assets/i18n/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@
"NO_RESULT": "Aucun résultat",
"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_FILE_ALREADY_IMPORTED": "Dossier déjà importé !",
"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 @@ -39,14 +39,16 @@ import { FileSelectorComponent } from './file-selector.component';
import { TranslateModule } from '@ngx-translate/core';
import { PipesModule } from '../../pipes/pipes.module';
import { LoggerModule } from '../../logger';
import { MatLegacySnackBarModule as MatSnackBarModule } from '@angular/material/legacy-snack-bar';
import { CustomFile } from '../../../../lib/models/custom-file';

describe('FileSelectorComponent', () => {
let component: FileSelectorComponent;
let fixture: ComponentFixture<FileSelectorComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [FileSelectorComponent, TranslateModule.forRoot(), PipesModule, LoggerModule.forRoot()],
imports: [FileSelectorComponent, TranslateModule.forRoot(), PipesModule, LoggerModule.forRoot(), MatSnackBarModule],
}).compileComponents();

fixture = TestBed.createComponent(FileSelectorComponent);
Expand All @@ -64,4 +66,97 @@ describe('FileSelectorComponent', () => {
component.handleFilesSelection(mockFiles);
expect(component['inputFiles'].nativeElement.value).toBe('');
});

it('should filter files based on allowed extensions', () => {
const file1 = new File([''], 'file1.json', { type: 'application/json' });
const file2 = new File([''], 'file2.txt', { type: 'text/plain' });
component.extensions = ['.json'];
component.handleFilesSelection([file1, file2]);
expect(component.files.length).toBe(1);
expect(component.files[0].name).toBe('file1.json');
});

it('should limit files to one if multipleFiles is false', () => {
const file1 = new File([''], 'file1.json', { type: 'application/json' });
const file2 = new File([''], 'file2.json', { type: 'application/json' });
component.multipleFiles = false;
component.handleFilesSelection([file1, file2]);
expect(component.files.length).toBe(1);
});

it('should emit filesChanged event with updated files', () => {
const file1 = new File([''], 'file1.json', { type: 'application/json' });
spyOn(component.filesChanged, 'emit');
component.handleFilesSelection([file1]);
expect(component.filesChanged.emit).toHaveBeenCalledWith([file1]);
});

it('should emit filesChanged event with updated files when adding to an existing list', () => {
const file1 = new File([''], 'file1.json', { type: 'application/json' });
const file2 = new File([''], 'file2.json', { type: 'application/json' });
component.files = [file1];
spyOn(component.filesChanged, 'emit');
component.handleFilesSelection([file2]);
[file1, file2].forEach((file) => {
expect(component.files).toContain(file);
});
});

it('should remove a file from files and displayFiles arrays', () => {
const file1 = new File([''], 'file1.json', { type: 'application/json' });
component.files = [file1];
component.displayFiles = [{ name: 'file1.json', size: 0, directory: false }];
component.removeFile(component.displayFiles[0]);
expect(component.files.length).toBe(0);
expect(component.displayFiles.length).toBe(0);
});

it('should remove all files within a directory', () => {
const file1 = new CustomFile([''], 'file1.json', { type: 'application/json' });
file1.relativePath = 'dir1';
component.files = [file1];
component.displayFiles = [{ name: 'dir1', size: 0, directory: true }];
component.removeFile(component.displayFiles[0]);
expect(component.displayFiles.length).toBe(0);
});

it('should remove only the specified directory and keep others', () => {
const file1 = new CustomFile([''], 'file1.json', { type: 'application/json' });
const file2 = new CustomFile([''], 'file2.json', { type: 'application/json' });
file1.relativePath = 'dir1';
file2.relativePath = 'dir2';

component.files = [file1, file2];
component.displayFiles = [
{ name: 'dir1', size: 0, directory: true },
{ name: 'dir2', size: 0, directory: true },
];

component.removeFile(component.displayFiles[0]);

expect(component.displayFiles.length).toBe(1);
expect(component.displayFiles[0].name).toBe('dir2');
});

it('should skip adding files if directory already exists', () => {
const mockFiles = [
new CustomFile(['content'], 'file12.json', { type: 'application/json' }),
new CustomFile(['content'], 'file2.json', { type: 'application/json' }),
];
mockFiles.forEach((file) => (file.relativePath = 'folder1'));

const mockDisplayFile = { name: 'folder1', size: 1000, directory: true };

component.displayFiles = [mockDisplayFile];

spyOn(component.snackBar, 'open');

component.handleFilesSelection(mockFiles);

mockFiles.forEach((file) => {
expect(component.files).not.toContain(file);
});

expect(component.snackBar.open).toHaveBeenCalledWith(jasmine.any(String), null, { panelClass: 'vitamui-snack-bar', duration: 10000 });
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,12 @@
*/
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 { MatLegacySnackBar as MatSnackBar } from '@angular/material/legacy-snack-bar';
import { CustomFile } from '../../../../lib/models/custom-file';

@Component({
selector: 'vitamui-file-selector',
Expand All @@ -65,13 +67,34 @@ export class FileSelectorComponent {

@Output() filesChanged = new EventEmitter<File[]>();

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

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

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

const folderNames = Array.from(files)
.map((file: CustomFile) => (file?.webkitRelativePath || file?.relativePath)?.split('/')[0] || file.name)
.filter((value, index, self) => self.indexOf(value) === index)
.map((folder) => folder.toLowerCase());

if (this.isDirectoryAlreadyExists(folderNames)) {
this.snackBar.open(this.translationService.instant('COLLECT.UPLOAD_FILE_ALREADY_IMPORTED'), null, {
panelClass: 'vitamui-snack-bar',
duration: 10000,
});
this.resetInput();
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,26 +117,13 @@ 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();
}

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?.webkitRelativePath || file?.relativePath).startsWith(displayFile.name));
} else {
this.files.splice(
this.files.findIndex((file) => file.name === displayFile.name),
Expand All @@ -132,4 +142,27 @@ export class FileSelectorComponent {
this.inputFiles.nativeElement.value = '';
}
}

private getDirectory(files: FileList | File[]): DisplayFile {
if (files.length === 0) return null;
const firstFile: CustomFile = files[0];
/**
* We need the file path, so we use the webkitRelativePath attribute when loading a folder via the native HTML file selector,
* the relativePath attribute in the case of drag & drop, and the name attribute when uploading a file.
*/
let path = 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;
}

private isDirectoryAlreadyExists(folderNames: string[]): boolean {
return folderNames.some((folderName) => this.displayFiles.some((dir) => dir.name.toLowerCase() === folderName));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright French Prime minister Office/SGMAP/DINSIC/Vitam Program (2019-2022)
* and the signatories of the "VITAM - Accord du Contributeur" agreement.
*
* contact@programmevitam.fr
*
* This software is a computer program whose purpose is to implement
* implement a digital archiving front-office system for the secure and
* efficient high volumetry VITAM solution.
*
* This software is governed by the CeCILL-C license under French law and
* abiding by the rules of distribution of free software. You can use,
* modify and/ or redistribute the software under the terms of the CeCILL-C
* license as circulated by CEA, CNRS and INRIA at the following URL
* "http://www.cecill.info".
*
* As a counterpart to the access to the source code and rights to copy,
* modify and redistribute granted by the license, users are provided only
* with a limited warranty and the software's author, the holder of the
* economic rights, and the successive licensors have only limited
* liability.
*
* In this respect, the user's attention is drawn to the risks associated
* with loading, using, modifying and/or developing or reproducing the
* software by the user in light of its specific status of free software,
* that may mean that it is complicated to manipulate, and that also
* therefore means that it is reserved for developers and experienced
* professionals having in-depth computer knowledge. Users are therefore
* encouraged to load and test the software's suitability as regards their
* requirements in conditions enabling the security of their systems and/or
* data to be ensured and, more generally, to use and operate it in the
* same conditions as regards security.
*
* The fact that you are presently reading this means that you have had
* knowledge of the CeCILL-C license and that you accept its terms.
*/

export class CustomFile extends File {
// Used to retrieve the path when we upload via drag&drop
relativePath?: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import JSZip from 'jszip';
import { ZipFileStatus } from './zip-file-status.interface';
import { HttpEvent, HttpEventType } from '@angular/common/http';
import { BehaviorSubject } from 'rxjs';
import { CustomFile } from '../custom-file';

export class ZipFile {
private zipFile: JSZip;
Expand All @@ -64,8 +65,9 @@ export class ZipFile {
return this;
}
for (let i = 0; i < files.length; i++) {
const item = files[i];
this.zipFile.file(item.webkitRelativePath, item);
const item: CustomFile = files[i];
const filePath = item?.webkitRelativePath || item?.relativePath || item?.name;
this.zipFile.file(filePath, item);
this.zipFileStatus.size += item.size;
}
return this;
Expand Down

0 comments on commit 61e0128

Please sign in to comment.