diff --git a/angular.json b/angular.json index 4dc02556..6ea4efe0 100644 --- a/angular.json +++ b/angular.json @@ -44,6 +44,7 @@ } ], "outputHashing": "none", + "optimization": true, "sourceMap": false, "namedChunks": false, "extractLicenses": true, diff --git a/projects/sonar/src/app/app.module.ts b/projects/sonar/src/app/app.module.ts index 3eedb5b5..0d032f51 100644 --- a/projects/sonar/src/app/app.module.ts +++ b/projects/sonar/src/app/app.module.ts @@ -31,11 +31,13 @@ import { TooltipModule } from 'ngx-bootstrap/tooltip'; import { NgxDropzoneModule } from 'ngx-dropzone'; import { ToastrModule } from 'ngx-toastr'; import { DividerModule } from 'primeng/divider'; +import { CarouselModule } from 'primeng/carousel'; import { DropdownModule } from 'primeng/dropdown'; import { FileUploadModule } from 'primeng/fileupload'; import { InputTextModule } from 'primeng/inputtext'; import { OrderListModule } from 'primeng/orderlist'; import { PanelModule } from 'primeng/panel'; +import { PaginatorModule } from 'primeng/paginator'; import { AdminComponent } from './_layout/admin/admin.component'; import { AppConfigService } from './app-config.service'; import { AppInitializerService } from './app-initializer.service'; @@ -78,6 +80,9 @@ import { DetailComponent as UserDetailComponent } from './record/user/detail/det import { UserComponent } from './record/user/user.component'; import { ValidationComponent } from './record/validation/validation.component'; import { UserService } from './user.service'; +import { OtherFilesComponent } from './record/files/other-files/other-files.component'; +import { FaIconClassPipe } from './pipe/fa-icon-class.pipe'; +import { StatsFilesComponent } from './record/files/stats-files/stats-files.component'; export function appInitializerFactory(appInitializerService: AppInitializerService): () => Promise { return () => appInitializerService.initialize().toPromise(); @@ -124,7 +129,10 @@ export function minElementError(err: any, field: FormlyFieldConfig) { ContributionsComponent, ContributionComponent, UploadFilesComponent, - FileItemComponent + FileItemComponent, + OtherFilesComponent, + FaIconClassPipe, + StatsFilesComponent ], imports: [ BrowserModule, @@ -154,7 +162,9 @@ export function minElementError(err: any, field: FormlyFieldConfig) { OrderListModule, DropdownModule, PanelModule, - DividerModule + DividerModule, + CarouselModule, + PaginatorModule ], providers: [ { diff --git a/projects/sonar/src/app/pipe/fa-icon-class.pipe.spec.ts b/projects/sonar/src/app/pipe/fa-icon-class.pipe.spec.ts new file mode 100644 index 00000000..d44394c4 --- /dev/null +++ b/projects/sonar/src/app/pipe/fa-icon-class.pipe.spec.ts @@ -0,0 +1,25 @@ +/* + * SONAR User Interface + * Copyright (C) 2019-2024 RERO + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { FaIconClassPipe } from './fa-icon-class.pipe'; + +describe('FaIconClassPipe', () => { + it('create an instance', () => { + const pipe = new FaIconClassPipe(); + expect(pipe).toBeTruthy(); + }); +}); diff --git a/projects/sonar/src/app/pipe/fa-icon-class.pipe.ts b/projects/sonar/src/app/pipe/fa-icon-class.pipe.ts new file mode 100644 index 00000000..1c2f883b --- /dev/null +++ b/projects/sonar/src/app/pipe/fa-icon-class.pipe.ts @@ -0,0 +1,70 @@ +/* + * SONAR User Interface + * Copyright (C) 2019-2024 RERO + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'faIconClass', +}) +export class FaIconClassPipe implements PipeTransform { + + /** + * Get the font awesome class name for a given mime type. + * + * @param mimetype mime type of a file + * @returns the font awesome class + */ + faClassForMimeType(mimetype: string): string { + if (mimetype == null) { + return 'fa-file-o'; + } + switch (true) { + case mimetype.startsWith('image/'): + return 'fa-file-image-o'; + case mimetype.startsWith('audio/'): + return 'fa-file-audio-o'; + case mimetype.startsWith('text/'): + return 'fa-file-text-o'; + case mimetype.startsWith('video/'): + return 'fa-file-video-o'; + case mimetype.startsWith('application/vnd.openxmlformats-officedocument.presentationml'): + return 'fa-file-powerpoint-o'; + case mimetype.startsWith('application/vnd.openxmlformats-officedocument.wordprocessingml'): + return 'fa-file-word-o'; + case mimetype.startsWith('application/vnd.openxmlformats-officedocument.spreadsheetml'): + return 'fa-file-excel-o'; + case mimetype.startsWith('application/pdf'): + return 'fa-file-pdf-o'; + } + return 'fa-file-o'; + } + + /** + * Get the font awesome class given a type and a value. + * + * @param value the value corresponding to the icon + * @param type type of the icon i.e. file + * @returns the font awesome classes. + */ + transform(value: string, type: string): string | null { + switch(type) { + case 'file': + return this.faClassForMimeType(value); + } + return null; + } +} diff --git a/projects/sonar/src/app/record/document/detail/detail.component.html b/projects/sonar/src/app/record/document/detail/detail.component.html index a537d457..74e03692 100644 --- a/projects/sonar/src/app/record/document/detail/detail.component.html +++ b/projects/sonar/src/app/record/document/detail/detail.component.html @@ -19,53 +19,83 @@
- +
- {{ ('document_type_' + record.documentType) | translate }} + {{ 'document_type_' + record.documentType | translate }}
- -

- + {{ filteredFiles.length-1 }} - {{ 'other files' | translate }} -

- - + [tooltip]=" + record.masked === 'masked_for_all' + ? ('Masked for all' | translate) + : ('Masked for external IP addresses' | translate) + " + > + -  : {{ record.title[0].subtitle | languageValue | async }} +  : {{ + record.title[0].subtitle | languageValue | async + }}

- + {{ organisation.name }} - + {{ subdivision.name | languageValue | async }}

- + -
    +
      - +
    • {{ item.value }}
    • @@ -79,8 +109,12 @@

      {{ record.extent }} - ; - {{ record.formats|join:', ' }} + + ; + + {{ + record.formats | join : ', ' + }}

      @@ -92,12 +126,26 @@

      -

      {{ record.dissertation.text }}

      +

      + {{ record.dissertation.text }} +

      -
      +
      -
      {{ (record.documentType !== 'coar:c_816b' ? 'Published in' : 'Submitted to') | translate }}:
      +
      + {{ + (record.documentType !== 'coar:c_816b' + ? 'Published in' + : 'Submitted to' + ) | translate + }}: +
      • @@ -111,8 +159,13 @@

        - - + + {{ value }} @@ -122,15 +175,27 @@
        - + {{ abstract.language | translateLanguage }} - - {{ abstract.value | slice:0:400 }} + + {{ abstract.value | slice : 0 : 400 }} {{ 'Show more' | translate }}… @@ -150,7 +215,15 @@
        - -
        Other files
        -
        -
        - -
        - -
        -
        -
        -
        - - -
        Statistics
        -
        -
        -
        - - {{ record.statistics['record-view'] }} -
        -
        - -
          -
        • - - {{ file.key }}: {{ record.statistics[file.key] }} - - {{ file.key }}: 0 -
        • -
        -
        -
        -
        - + + + + @if (filteredFiles.length > 1) { + Other files + + @if (tabOther.active) { +
        + +
        + } } +
        + + + Statistics + + @if (tabStats.active) { +
        + +
        + } +
        + + Edit Files + + @if (tabEditFiles.active) { + + } + +
        diff --git a/projects/sonar/src/app/record/document/detail/detail.component.ts b/projects/sonar/src/app/record/document/detail/detail.component.ts index 41ddaf8b..e746adc8 100644 --- a/projects/sonar/src/app/record/document/detail/detail.component.ts +++ b/projects/sonar/src/app/record/document/detail/detail.component.ts @@ -20,13 +20,14 @@ import { OnInit, TemplateRef, ViewChild, + inject, } from '@angular/core'; -import { ApiService } from '@rero/ng-core'; +import { ApiService, RecordService } from '@rero/ng-core'; import { HttpClient } from '@angular/common/http'; import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; import { TranslateService } from '@ngx-translate/core'; import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal'; -import { Observable, Subscription } from 'rxjs'; +import { Observable, Subscription, map } from 'rxjs'; import { AppConfigService } from '../../../app-config.service'; import { DocumentFile } from '../document.interface'; @@ -49,6 +50,8 @@ export class DetailComponent implements OnDestroy, OnInit { // Record retrieved from observable. record: any = null; + recordService = inject(RecordService); + // Form modal reference. previewModalRef: BsModalRef; @@ -97,7 +100,6 @@ export class DetailComponent implements OnDestroy, OnInit { element.full = false; return element; }); - this.getStats(); }); // When language change, abstracts are sorted and first one is displayed. @@ -110,7 +112,15 @@ export class DetailComponent implements OnDestroy, OnInit { }) ); } + updateFiles(files) { + this.recordService.getRecord('documents', this.record.pid, 1).pipe( + map(doc => this.record._files = doc.metadata._files) + ).subscribe(); + } + get filteredKeys() { + return this.filteredFiles.map(file => file.key); + } /** * Component destruction. * @@ -159,38 +169,6 @@ export class DetailComponent implements OnDestroy, OnInit { }); } - /** - * Get the stats corresponding to given record. - */ - private getStats() { - const data = { - 'record-view': { - stat: 'record-view', - params: { - pid_value: this.record.pid, - pid_type: 'doc' - } - }, - 'file-download': { - stat: 'file-download', - params: { - bucket_id: this.record._bucket - } - } - }; - - this._httpClient.post(`${this._apiService.getEndpointByType('stats', true)}`, data) - .subscribe(results => { - const statistics = {}; - if (results['file-download'] != null) { - results['file-download'].buckets.map( - res => statistics[res.key] = res.unique_count - ); - } - statistics['record-view'] = results['record-view'].unique_count; - this.record.statistics = statistics; - }); - } /** * Show abstract's full text when clicking on the show more link. diff --git a/projects/sonar/src/app/record/files/other-files/other-files.component.html b/projects/sonar/src/app/record/files/other-files/other-files.component.html new file mode 100644 index 00000000..bda206fa --- /dev/null +++ b/projects/sonar/src/app/record/files/other-files/other-files.component.html @@ -0,0 +1,126 @@ + +@if (loading) { + +} @else { +@if (files()?.length > 0) { + @if(files().length > numVisible) { +
        +
        + + + + +
        + + {{ getResultsText() | async }} + +
        + } + + @if (filteredFiles.length > 0) { +
        + + +
        + @if (!file.thumbnail.startsWith('static')){ + + } @else { +
        + +
        + } +
        + @if (file.preview) { + + + + } + @if (file.download) { + + + + } +
        + {{ file.label }} +
        +
        +
        +
        + @if(files().length > numVisible) { + + } + } +} +} + + + @if (previewFile) { + + + } + diff --git a/projects/sonar/src/app/record/files/other-files/other-files.component.scss b/projects/sonar/src/app/record/files/other-files/other-files.component.scss new file mode 100644 index 00000000..5053867e --- /dev/null +++ b/projects/sonar/src/app/record/files/other-files/other-files.component.scss @@ -0,0 +1,45 @@ +/* + * SONAR User Interface + * Copyright (C) 2021-2024 RERO + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + + .file-preview { + cursor: zoom-in; + } + +.item-icon { + width: 80px; + height: 100px; + display: inline-flex; + align-items: self-end; +} +:host ::ng-deep { + img { + max-width: 80px; + } + a { + color: #495057; + } + .p-carousel-item { + align-self: flex-end; + } +} + +.files-icon { + width: 80px; + height: 100px; + display: inline-flex; + align-items: self-end; +} diff --git a/projects/sonar/src/app/record/files/other-files/other-files.component.spec.ts b/projects/sonar/src/app/record/files/other-files/other-files.component.spec.ts new file mode 100644 index 00000000..0ad4e2b2 --- /dev/null +++ b/projects/sonar/src/app/record/files/other-files/other-files.component.spec.ts @@ -0,0 +1,40 @@ +/* + * SONAR User Interface + * Copyright (C) 2019-2024 RERO + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { OtherFilesComponent } from './other-files.component'; + +describe('OtherFilesComponent', () => { + let component: OtherFilesComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [OtherFilesComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(OtherFilesComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/projects/sonar/src/app/record/files/other-files/other-files.component.ts b/projects/sonar/src/app/record/files/other-files/other-files.component.ts new file mode 100644 index 00000000..1e7180aa --- /dev/null +++ b/projects/sonar/src/app/record/files/other-files/other-files.component.ts @@ -0,0 +1,245 @@ +/* + * SONAR User Interface + * Copyright (C) 2019-2024 RERO + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { HttpClient } from '@angular/common/http'; +import { Component, Input, OnDestroy, OnInit, TemplateRef, ViewChild, inject, input } from '@angular/core'; +import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; +import { TranslateService } from '@ngx-translate/core'; +import { ApiService, Record, RecordService } from '@rero/ng-core'; +import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal'; +import { PrimeNGConfig } from 'primeng/api'; +import { Observable, Subscription, forkJoin, map, of, switchMap, tap } from 'rxjs'; + +import { BreakpointObserver, BreakpointState, Breakpoints } from '@angular/cdk/layout'; +import { toObservable, toSignal } from '@angular/core/rxjs-interop'; + +// file interface +export interface File { + // thumbnail URL + thumbnail?: string; + // download URL + download: string; + // thumbnail legend + label: string; + // preview URL + preview?: string; +} + +// Component itself +@Component({ + selector: 'sonar-other-files', + templateUrl: './other-files.component.html', + styleUrl: './other-files.component.scss' +}) +export class OtherFilesComponent implements OnInit, OnDestroy { + // input document pid + documentPid = input.required(); + + // list of files + files = toSignal( + toObservable(this.documentPid).pipe( + switchMap((documentPid) => + this.getFiles() + ) + ) + ); + // filtered array of files + filteredFiles = []; + // input text filter + filterText = ''; + // number of visible items in the carousel + numVisible = 5; + // current page for the carousel + page = 0; + loading = false; + + // file to preview + previewFile: { + label: string; + url: SafeUrl; + }; + // modal for the invenio previewer + previewModalRef: BsModalRef; + + // for modal + @ViewChild('previewModal') + previewModalTemplate: TemplateRef; + + // -------- Services ------------- + // primeng configuration service + private ngConfigService = inject(PrimeNGConfig); + // http service + private httpService = inject(HttpClient); + // translation service + private translateService = inject(TranslateService); + // ng-core record service + private recordService = inject(RecordService); + // ng-core api service + private apiService = inject(ApiService); + // url sanitizer service + private sanitizer = inject(DomSanitizer); + // modal service + private modalService = inject(BsModalService); + // service to detect responsive breakpoints + private breakpointObserver = inject(BreakpointObserver); + + /** all component subscription */ + private subscriptions = new Subscription(); + + // contructor + constructor() { + // to avoid primeng error + // TODO: remove this when primeng will be fixed + this.ngConfigService.translation.aria.slideNumber = '{slideNumber}'; + + } + + /** OnInit hook */ + ngOnInit(): void { + this.getFiles(); + this.changeNItemsOnCarousel(); + } + + /** OnDestroy hook */ + ngOnDestroy(): void { + this.subscriptions.unsubscribe(); + } + + /** + * Changes the number of items in the carousel. + * + * To be responsive. + */ + changeNItemsOnCarousel(): void { + this.subscriptions.add( + this.breakpointObserver + .observe([Breakpoints.XSmall, Breakpoints.Small, Breakpoints.Medium, Breakpoints.Large]) + .subscribe((state: BreakpointState) => { + switch (true) { + case this.breakpointObserver.isMatched(Breakpoints.XSmall): + this.numVisible = 1; + break; + case this.breakpointObserver.isMatched(Breakpoints.Small): + this.numVisible = 2; + break; + case this.breakpointObserver.isMatched(Breakpoints.Medium): + this.numVisible = 4; + break; + case this.breakpointObserver.isMatched(Breakpoints.Large): + this.numVisible = 5; + break; + } + }) + ); + } + + /** + * Retrieves the files information from the backend. + */ + getFiles(): Observable { + const baseUrl = this.apiService.getEndpointByType('records'); + // retrieve all records files linked to a given document pid + const query = `metadata.document.pid:${this.documentPid()}`; + this.loading = true; + return this.recordService + .getRecord('documents', this.documentPid(), 1) + .pipe( + tap(() => (this.loading = false)), + map(res => res?.metadata?._files? res.metadata._files : []), + map((res: any[]) => { + const files = []; + const data = {}; + // retrieve main files + res.map((entry) => { + // main file (such as pdf) + if (entry.type == 'file') { + const dataFile: any = { + label: entry?.label ? entry.label : entry.key, + mimetype: entry.mimetype, + download: entry.links.download, + }; + if (entry?.links?.preview) { + dataFile.preview = entry.links.preview; + } + if (entry?.thumbnail) { + dataFile.thumbnail = entry.thumbnail; + } + data[entry.key] = dataFile; + } + }); + Object.values(data).map((d: File) => files.push(d)); + files.sort((a, b) => a.label.localeCompare(b.label, 'en', { numeric: true })); + this.filteredFiles = files; + this.loading = false; + return files; + }) + ); + } + /** + * Fired when the text to filter the items changes. + * + * @param $event - standard event + */ + onTextChange($event): void { + if (this.filterText.length > 0) { + this.filteredFiles = this.files().filter((value) => value.label.toLowerCase().includes(this.filterText.toLowerCase())); + } else { + this.filteredFiles = this.files(); + } + } + + /** + * Get the string used to display the search result number. + * @param hits - list of hit results. + * @returns observable of the string representation of the number of results. + */ + getResultsText(): Observable { + const total = this.filteredFiles.length; + if (total == this.files.length) { + return this.translateService.stream('{{ total }} results', { total }); + } + return total === 0 + ? this.translateService.stream('no result') + : this.translateService.stream('{{ total }} results of {{ remoteTotal }}', { + total, + remoteTotal: this.files.length, + }); + } + + /** + * Fired when the page change in the paginator. + * @param $event - standard event. + */ + onPageChange($event): void { + this.page = $event.page; + } + + /** Open the file preview in a modal container. + * + * @param file - the file to preview. + */ + + preview(file: File): void { + this.previewModalRef = this.modalService.show(this.previewModalTemplate, { + class: 'modal-lg', + }); + this.previewFile = { + label: file.label, + url: this.sanitizer.bypassSecurityTrustResourceUrl(file.preview), + }; + } +} diff --git a/projects/sonar/src/app/record/files/stats-files/stats-files.component.html b/projects/sonar/src/app/record/files/stats-files/stats-files.component.html new file mode 100644 index 00000000..d488c01f --- /dev/null +++ b/projects/sonar/src/app/record/files/stats-files/stats-files.component.html @@ -0,0 +1,33 @@ + +
        +
        + + {{ statistics['record-view'] }} +
        + +
          + @for (key of filteredKeys; track key) { +
        • + @if(statistics[key]) { + {{ key }}: {{ statistics[key] }} + } @else { + {{ key }}: 0 } +
        • + } +
        +
        diff --git a/projects/sonar/src/app/record/files/stats-files/stats-files.component.spec.ts b/projects/sonar/src/app/record/files/stats-files/stats-files.component.spec.ts new file mode 100644 index 00000000..f72dbd80 --- /dev/null +++ b/projects/sonar/src/app/record/files/stats-files/stats-files.component.spec.ts @@ -0,0 +1,40 @@ +/* + * SONAR User Interface + * Copyright (C) 2019-2024 RERO + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { StatsFilesComponent } from './stats-files.component'; + +describe('StatsFilesComponent', () => { + let component: StatsFilesComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [StatsFilesComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(StatsFilesComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/projects/sonar/src/app/record/files/stats-files/stats-files.component.ts b/projects/sonar/src/app/record/files/stats-files/stats-files.component.ts new file mode 100644 index 00000000..9c707d60 --- /dev/null +++ b/projects/sonar/src/app/record/files/stats-files/stats-files.component.ts @@ -0,0 +1,71 @@ +/* + * SONAR User Interface + * Copyright (C) 2019-2024 RERO + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { HttpClient } from '@angular/common/http'; +import { Component, Input, OnInit, inject } from '@angular/core'; +import { ApiService } from '@rero/ng-core'; + +@Component({ + selector: 'sonar-stats-files', + templateUrl: './stats-files.component.html' +}) +export class StatsFilesComponent implements OnInit{ + statistics = {}; + + @Input() record; + @Input() filteredKeys: string[]; + + private httpClient = inject(HttpClient); + private apiService = inject(ApiService); + + ngOnInit(): void { + this.getStats(); + } + /** + * Get the stats corresponding to given record. + */ + private getStats() { + const data = { + 'record-view': { + stat: 'record-view', + params: { + pid_value: this.record.pid, + pid_type: 'doc' + } + }, + 'file-download': { + stat: 'file-download', + params: { + bucket_id: this.record._bucket + } + } + }; + + this.httpClient.post(`${this.apiService.getEndpointByType('stats', true)}`, data) + .subscribe(results => { + const statistics = {}; + if (results['file-download'] != null) { + results['file-download'].buckets.map( + res => statistics[res.key] = res.unique_count + ); + } + statistics['record-view'] = results['record-view'].unique_count; + this.statistics = statistics; + }); + } + +} diff --git a/projects/sonar/src/app/record/files/upload-files/upload-files.component.ts b/projects/sonar/src/app/record/files/upload-files/upload-files.component.ts index 3567d799..f3cc2edd 100644 --- a/projects/sonar/src/app/record/files/upload-files/upload-files.component.ts +++ b/projects/sonar/src/app/record/files/upload-files/upload-files.component.ts @@ -16,7 +16,7 @@ */ import { HttpClient } from '@angular/common/http'; -import { Component, ViewChild, effect, inject, input } from '@angular/core'; +import { Component, ViewChild, effect, inject, input, output } from '@angular/core'; import { toObservable, toSignal } from '@angular/core/rxjs-interop'; import { TranslateService } from '@ngx-translate/core'; import { DialogService, RecordService } from '@rero/ng-core'; @@ -49,6 +49,8 @@ export class UploadFilesComponent { // record type such as documents recordType = input.required(); + filesChanged = output(); + // initial record from pid and recordType initialRecord = toSignal( combineLatest(toObservable(this.pid), toObservable(this.recordType)).pipe( @@ -156,6 +158,7 @@ export class UploadFilesComponent { 'Metadata have been saved successfully.' ) ); + this.filesChanged.emit(this.files); }); } } @@ -193,6 +196,7 @@ export class UploadFilesComponent { this.translateService.instant('File uploaded successfully.') ); this.nUploadedFiles = 0; + this.filesChanged.emit(this.files); }) ) .subscribe(() => this.spinner.hide('file-upload')); @@ -223,6 +227,7 @@ export class UploadFilesComponent { this.getRecord(); }), tap(() => { + this.filesChanged.emit(this.files); this.resetFilter(); this.toastrService.success( this.translateService.instant('File uploaded successfully.') @@ -338,6 +343,7 @@ export class UploadFilesComponent { this.toastrService.success( this.translateService.instant('File removed successfully.') ); + this.filesChanged.emit(this.files); return true; }) ); @@ -436,6 +442,7 @@ export class UploadFilesComponent { this.files.map((file) => { file.metadata = this._getFileInRecord(file.key); }); + this.filesChanged.emit(this.files); }); }