Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[MNT-24575] Added dialog to display folder information #4282

Open
wants to merge 9 commits into
base: develop
Choose a base branch
from
30 changes: 30 additions & 0 deletions projects/aca-content/assets/app.extensions.json
Original file line number Diff line number Diff line change
Expand Up @@ -632,6 +632,21 @@
"visible": "app.selection.canDelete"
}
},
{
"id": "app.context.menu.folder-info",
"title": "APP.ACTIONS.FOLDER_INFO",
"order": 800,
"icon": "info",
"actions": {
"click": "FOLDER_INFORMATION"
},
"rules": {
"visible": [
"app.selection.folder",
"!app.navigation.isTrashcan"
]
}
},
{
"id": "app.create.separator.3",
"type": "separator",
Expand Down Expand Up @@ -877,6 +892,21 @@
"visible": "app.selection.canDelete"
}
},
{
"id": "app.context.menu.folder-info",
"title": "APP.ACTIONS.FOLDER_INFO",
"order": 1200,
"icon": "info",
"actions": {
"click": "FOLDER_INFORMATION"
},
"rules": {
"visible": [
"app.selection.folder",
"!app.navigation.isTrashcan"
]
}
},
{
"id": "app.create.separator.3",
"type": "separator",
Expand Down
17 changes: 15 additions & 2 deletions projects/aca-content/assets/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,8 @@
"EDIT_OFFLINE": "Edit Offline",
"EDIT_OFFLINE_CANCEL": "Cancel Editing",
"CHANGE_ASPECT": "Edit Aspects",
"ADD_ASPECTS": "Add Aspects"
"ADD_ASPECTS": "Add Aspects",
"FOLDER_INFO": "Folder Information"
},
"DIALOGS": {
"CONFIRM_PURGE": {
Expand Down Expand Up @@ -451,7 +452,19 @@
"OPTIONS_SETTINGS": "Options and settings",
"MY_PROFILE": "My profile",
"EXPAND_NAVIGATION": "Expand navigation menu"
}
},
"FOLDER_INFO": {
"ICON": "Folder Icon",
"TITLE": "Folder Information",
"SIZE" : "Size",
"CALCULATING": "Calculating...",
"CALCULATED_SIZE_LARGE": "{{sizeInBytes}} bytes ({{sizeInLargeUnit}} {{unit}} on disk) for {{count}} files",
"CALCULATED_SIZE_NORMAL": "{{sizeInBytes}} bytes for {{count}} files",
"LOCATION": "Location",
"CREATED": "Created",
"MODIFIED": "Modified",
"DONE": "Done"
}
},
"NODE_SELECTOR": {
"COPY_ITEM": "Copy '{{ name }}' to...",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<div class="aca-folder-info-container">
AleksanderSklorz marked this conversation as resolved.
Show resolved Hide resolved
<div class="aca-folder-info-header">
<div class="aca-folder-icon">
AleksanderSklorz marked this conversation as resolved.
Show resolved Hide resolved
<img alt="{{ 'APP.FOLDER_INFO.ICON' | translate }}" src="{{ icon }}">
</div>
<div class="aca-folder-title" data-automation-id="folder-info-name">{{ name }}</div>
</div>
<mat-divider></mat-divider>
<div class="aca-folder-info-body">
<div class="aca-folder-info-item">
<div class="aca-folder-info-item-label">{{ 'APP.FOLDER_INFO.SIZE' | translate }}</div>
<div class="aca-folder-info-item-value"
data-automation-id="folder-info-size">{{ size }}</div>
</div>
<mat-divider></mat-divider>
<div class="aca-folder-info-item">
<div class="aca-folder-info-item-label">{{ 'APP.FOLDER_INFO.LOCATION' | translate }}</div>
<div class="aca-folder-info-item-value"
data-automation-id="folder-info-location">{{ location }}</div>
</div>
<mat-divider></mat-divider>
<div class="aca-folder-info-item">
<div class="aca-folder-info-item-label">{{ 'APP.FOLDER_INFO.CREATED' | translate }}</div>
<div class="aca-folder-info-item-value"
data-automation-id="folder-info-creation-date"
[title]="created | adfLocalizedDate">{{ created | adfTimeAgo }}</div>
</div>
<mat-divider></mat-divider>
<div class="aca-folder-info-item">
<div class="aca-folder-info-item-label">{{ 'APP.FOLDER_INFO.MODIFIED' | translate }}</div>
<div class="aca-folder-info-item-value"
data-automation-id="folder-info-modify-date"
[title]="modified | adfLocalizedDate">{{ modified | adfTimeAgo }}</div>
</div>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
.app-folder-info {
.aca-folder-info {
&-container {
display: flex;
flex-direction: column;
border: 1px solid var(--theme-border-color);
border-radius: 12px;
}

&-header {
display: flex;
flex-direction: row;
align-items: center;
column-gap: 10px;
padding: 20px;

.aca-folder-title {
flex: 1;
font-weight: bold;
}
}

&-body {
display: flex;
flex-direction: column;
flex: 1;
padding: 10px 20px;

.aca-folder-info-item {
display: flex;
flex-direction: row;
padding: 20px 0;

&-label {
width: 30%;
color: var(--theme-text-color);
}

&-value {
width: 70%;
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*!
* Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Alfresco Example Content Application
*
* This file is part of the Alfresco Example Content Application.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The Alfresco Example Content Application 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
*/

import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { FolderInformationComponent } from './folder-information.component';
import { DIALOG_COMPONENT_DATA, RedirectAuthService } from '@alfresco/adf-core';
import { ContentService, NodesApiService } from '@alfresco/adf-content-services';
import { By } from '@angular/platform-browser';
import { EMPTY, of, Subject } from 'rxjs';
import { LibTestingModule } from '@alfresco/aca-shared';

describe('FolderInformationComponent', () => {
let fixture: ComponentFixture<FolderInformationComponent>;
let nodeService: NodesApiService;
let initiateFolderSizeCalculationSpy: jasmine.Spy;
AleksanderSklorz marked this conversation as resolved.
Show resolved Hide resolved
let getFolderSizeInfoSpy: jasmine.Spy;
AleksanderSklorz marked this conversation as resolved.
Show resolved Hide resolved
const mockSub = new Subject<{ entry: { jobId: string } }>();
swapnil-verma-gl marked this conversation as resolved.
Show resolved Hide resolved
AleksanderSklorz marked this conversation as resolved.
Show resolved Hide resolved

const dialogData = {
name: 'mock-folder',
id: 'mock-folder-id',
path: {
name: 'mock-folder-path'
},
createdAt: new Date(2024, 1, 1, 11, 11),
modifiedAt: new Date(2024, 2, 2, 22, 22)
};
AleksanderSklorz marked this conversation as resolved.
Show resolved Hide resolved

const getValueFromElement = (id: string) => fixture.debugElement.query(By.css(`[data-automation-id="${id}"]`)).nativeElement.textContent;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const getValueFromElement = (id: string) => fixture.debugElement.query(By.css(`[data-automation-id="${id}"]`)).nativeElement.textContent;
const getValueFromElement = (id: string): string => fixture.debugElement.query(By.css(`[data-automation-id="${id}"]`)).nativeElement.textContent;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added return type

beforeEach(() => {
TestBed.configureTestingModule({
imports: [FolderInformationComponent, LibTestingModule],
providers: [
{ provide: DIALOG_COMPONENT_DATA, useValue: dialogData },
{ provide: RedirectAuthService, useValue: { onLogin: EMPTY, onTokenReceived: EMPTY } }
]
});
fixture = TestBed.createComponent(FolderInformationComponent);
nodeService = TestBed.inject(NodesApiService);
spyOn(TestBed.inject(ContentService), 'getNodeIcon').and.returnValue('./assets/images/ft_ic_folder.svg');
initiateFolderSizeCalculationSpy = spyOn(nodeService, 'initiateFolderSizeCalculation').and.returnValue(mockSub.asObservable());
getFolderSizeInfoSpy = spyOn(nodeService, 'getFolderSizeInfo').and.returnValue(EMPTY);
});

it('should render all information in init', () => {
fixture.detectChanges();
expect(getValueFromElement('folder-info-name')).toBe('mock-folder');
expect(getValueFromElement('folder-info-size')).toBe('APP.FOLDER_INFO.CALCULATING');
expect(getValueFromElement('folder-info-location')).toBe('mock-folder-path');
expect(getValueFromElement('folder-info-creation-date')).toBe('01/02/2024 11:11');
expect(getValueFromElement('folder-info-modify-date')).toBe('02/03/2024 22:22');
});

it('should make API call on init to start folder size calculation', () => {
fixture.detectChanges();
expect(initiateFolderSizeCalculationSpy).toHaveBeenCalledWith('mock-folder-id');
});

it('should fetch folder size only when the initial folder size calculation request is completed', () => {
fixture.detectChanges();
expect(initiateFolderSizeCalculationSpy).toHaveBeenCalledWith('mock-folder-id');
expect(getFolderSizeInfoSpy).not.toHaveBeenCalled();
mockSub.next({ entry: { jobId: 'mock-job-id' } });
expect(getFolderSizeInfoSpy).toHaveBeenCalled();
});

it('should make repeated calls to get folder size info, if the response returned from the API is IN_PROGRESS', fakeAsync(() => {
getFolderSizeInfoSpy.and.returnValue(of({ entry: { status: 'IN_PROGRESS' } }));
fixture.detectChanges();
expect(getFolderSizeInfoSpy).not.toHaveBeenCalled();
mockSub.next({ entry: { jobId: 'mock-job-id' } });
expect(getFolderSizeInfoSpy).toHaveBeenCalledTimes(1);
tick(5000);
expect(getFolderSizeInfoSpy).toHaveBeenCalledTimes(2);
tick(5000);
expect(getFolderSizeInfoSpy).toHaveBeenCalledTimes(3);
getFolderSizeInfoSpy.and.returnValue(of({ entry: { status: 'COMPLETE' } }));
tick(5000);
expect(getFolderSizeInfoSpy).toHaveBeenCalledTimes(4);
tick(5000);
expect(getFolderSizeInfoSpy).not.toHaveBeenCalledTimes(5);
}));
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*!
* Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Alfresco Example Content Application
*
* This file is part of the Alfresco Example Content Application.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The Alfresco Example Content Application 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
*/

import { Component, DestroyRef, inject, OnInit, ViewEncapsulation } from '@angular/core';
import { CommonModule, NgIf } from '@angular/common';
import { DIALOG_COMPONENT_DATA, LocalizedDatePipe, TimeAgoPipe } from '@alfresco/adf-core';
import { JobIdBodyEntry, Node, SizeDetailsEntry } from '@alfresco/js-api';
import { MatDividerModule } from '@angular/material/divider';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { ContentService, NodesApiService } from '@alfresco/adf-content-services';
import { concatMap, expand, first } from 'rxjs/operators';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { EMPTY, timer } from 'rxjs';

const MEMORY_UNIT_LIST = ['bytes', 'KB', 'MB', 'GB', 'TB'];

@Component({
selector: 'app-folder-info',
standalone: true,
imports: [CommonModule, MatDividerModule, TimeAgoPipe, NgIf, TranslateModule, LocalizedDatePipe, LocalizedDatePipe],
swapnil-verma-gl marked this conversation as resolved.
Show resolved Hide resolved
AleksanderSklorz marked this conversation as resolved.
Show resolved Hide resolved
templateUrl: './folder-information.component.html',
styleUrls: ['./folder-information.component.scss'],
encapsulation: ViewEncapsulation.None,
host: { class: 'app-folder-info' }
})
export class FolderInformationComponent implements OnInit {
readonly contentService = inject(ContentService);
readonly nodesService = inject(NodesApiService);
readonly translateService = inject(TranslateService);

private readonly destroyRef = inject(DestroyRef);

data: Node = inject(DIALOG_COMPONENT_DATA);
name: string;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need separate variables for name,size, location, created and modified?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consolidated all properties into a single FolderInformation type, defined in folder-information.component.ts

size: string;
location: string;
created: Date;
modified: Date;
icon: string;

ngOnInit() {
this.name = this.data.name;
this.location = this.data.path.name;
this.created = this.data.createdAt;
this.modified = this.data.modifiedAt;
this.icon = this.contentService.getNodeIcon(this.data);
this.size = this.translateService.instant('APP.FOLDER_INFO.CALCULATING');

this.nodesService
AleksanderSklorz marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please add some error handling (for example you can display some error content inside dialog when loading is failed)?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@swapnil-verma-gl I guess this one is not addressed because I can't see changes for that one?

.initiateFolderSizeCalculation(this.data.id)
.pipe(first())
.subscribe((jobIdEntry: JobIdBodyEntry) => {
AleksanderSklorz marked this conversation as resolved.
Show resolved Hide resolved
this.nodesService
.getFolderSizeInfo(this.data.id, jobIdEntry.entry.jobId)
.pipe(
expand((result: any) =>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use proper type instead of any

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed instance of any to SizeDetailsEntry

result.entry.status === 'IN_PROGRESS'
AleksanderSklorz marked this conversation as resolved.
Show resolved Hide resolved
? timer(5000).pipe(concatMap(() => this.nodesService.getFolderSizeInfo(this.data.id, jobIdEntry.entry.jobId)))
: EMPTY
),
takeUntilDestroyed(this.destroyRef)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

takeUntilDestroyed(this.destroyRef) is added for pipe for result of getFolderSizeInfo function. I guess we should add takeUntilDestroyed for pipe for result of initiateFolderSizeCalculation instead as we call subscribe for result of that function?

)
.subscribe((folderInfo: SizeDetailsEntry) => {
AleksanderSklorz marked this conversation as resolved.
Show resolved Hide resolved
AleksanderSklorz marked this conversation as resolved.
Show resolved Hide resolved
let size = parseFloat(folderInfo.entry.sizeInBytes);
let unitIndex = 0;
let isMoreThanBytes = false;
while (size > 1000) {
isMoreThanBytes = true;
size = size / 1000;
unitIndex++;
}
const params = {
sizeInBytes: parseFloat(folderInfo.entry.sizeInBytes).toLocaleString('en'),
sizeInLargeUnit: size.toFixed(2),
unit: MEMORY_UNIT_LIST[unitIndex],
count: folderInfo.entry.numberOfFiles
};
this.size = this.translateService.instant(
isMoreThanBytes ? 'APP.FOLDER_INFO.CALCULATED_SIZE_LARGE' : 'APP.FOLDER_INFO.CALCULATED_SIZE_NORMAL',
params
);
});
});
}
}
Loading
Loading