Skip to content

Commit

Permalink
feat: file upload component (#1617)
Browse files Browse the repository at this point in the history
  • Loading branch information
itssharmasandeep authored Jun 1, 2022
1 parent 2f790a6 commit 771ad6c
Show file tree
Hide file tree
Showing 25 changed files with 832 additions and 2 deletions.
5 changes: 5 additions & 0 deletions projects/assets-library/assets/icons/cloud-upload.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions projects/assets-library/assets/icons/trash.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions projects/assets-library/src/icons/icon-library.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const iconsRootPath = 'assets/icons';
{ key: IconType.Close, url: `${iconsRootPath}/close.svg` },
{ key: IconType.CloseCircle, url: `${iconsRootPath}/close-circle.svg` },
{ key: IconType.CloseCircleFilled, url: `${iconsRootPath}/close-circle-filled.svg` },
{ key: IconType.CloudUpload, url: `${iconsRootPath}/cloud-upload.svg` },
{ key: IconType.Code, url: `${iconsRootPath}/code.svg` },
{ key: IconType.CollapseAll, url: `${iconsRootPath}/collapse-all.svg` },
{ key: IconType.Collapsed, url: `${iconsRootPath}/plus-square.svg` },
Expand Down Expand Up @@ -93,6 +94,7 @@ const iconsRootPath = 'assets/icons';
{ key: IconType.StatusCode, url: `${iconsRootPath}/status-code.svg` },
{ key: IconType.StringAttribute, url: `${iconsRootPath}/string-attribute.svg` },
{ key: IconType.TraceId, url: `${iconsRootPath}/trace-id.svg` },
{ key: IconType.Trash, url: `${iconsRootPath}/trash.svg` },
{ key: IconType.TriangleDown, url: `${iconsRootPath}/triangle-down.svg` },
{ key: IconType.TriangleLeft, url: `${iconsRootPath}/triangle-left.svg` },
{ key: IconType.TriangleRight, url: `${iconsRootPath}/triangle-right.svg` },
Expand Down
2 changes: 2 additions & 0 deletions projects/assets-library/src/icons/icon-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export const enum IconType {
Close = 'svg:close',
CloseCircle = 'svg:close-circle',
CloseCircleFilled = 'svg:close-circle-filled',
CloudUpload = 'svg:cloud-upload',
CollapseAll = 'svg:collapse-all',
Collapsed = 'svg:plus-square',
Compare = 'svg:compare',
Expand Down Expand Up @@ -118,6 +119,7 @@ export const enum IconType {
StringAttribute = 'svg:string-attribute',
Time = 'schedule',
TraceId = 'svg:trace-id',
Trash = 'svg:trash',
TriangleDown = 'svg:triangle-down',
TriangleLeft = 'svg:triangle-left',
TriangleRight = 'svg:triangle-right',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { createPipeFactory } from '@ngneat/spectator/jest';
import { DisplayFileSizePipe } from './display-file-size.pipe';

describe('Display File Size Pipe', () => {
const createPipe = createPipeFactory(DisplayFileSizePipe);

test('should return correct size in Bytes', () => {
const spectator = createPipe(`{{ 1 | htDisplayFileSize}}`);
expect(spectator.element).toHaveText('1 B');
});

test('should return correct size in KiloBytes', () => {
const spectator = createPipe(`{{ 102.4 | htDisplayFileSize}}`);
expect(spectator.element).toHaveText('0.1 KB');
});

test('should return correct size in MegaBytes', () => {
const spectator = createPipe(`{{ 1024 * 102.4 | htDisplayFileSize}}`);
expect(spectator.element).toHaveText('0.1 MB');
});

test('should return correct size in GigaBytes', () => {
const spectator = createPipe(`{{ 1024 * 1024 * 102.4 | htDisplayFileSize}}`);
expect(spectator.element).toHaveText('0.1 GB');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
name: 'htDisplayFileSize'
})
export class DisplayFileSizePipe implements PipeTransform {
public transform(sizeInBytes: number): string {
let power = Math.round(Math.log(sizeInBytes) / Math.log(1024));
power = Math.min(power, FILE_SIZE_UNITS.length - 1);

const size = sizeInBytes / Math.pow(1024, power);
const formattedSize = Math.round(size * 100) / 100; // Formatting the size to 2 decimals
const unit = FILE_SIZE_UNITS[power];

return `${formattedSize} ${unit}`;
}
}

const FILE_SIZE_UNITS = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
7 changes: 5 additions & 2 deletions projects/common/src/utilities/formatters/formatting.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { NgModule } from '@angular/core';
import { DisplayDatePipe } from './date/display-date.pipe';
import { DisplayDurationPipe } from './duration/display-duration.pipe';
import { DisplayStringEnumPipe } from './enum/display-string-enum.pipe';
import { DisplayFileSizePipe } from './file-size/display-file-size.pipe';
import { DisplayNumberPipe } from './numeric/display-number.pipe';
import { OrdinalPipe } from './ordinal/ordinal.pipe';
import { DisplayStringPipe } from './string/display-string.pipe';
Expand All @@ -19,7 +20,8 @@ import { DisplayTimeAgo } from './time/display-time-ago.pipe';
HighlightPipe,
DisplayTitlePipe,
OrdinalPipe,
DisplayStringEnumPipe
DisplayStringEnumPipe,
DisplayFileSizePipe
],
exports: [
DisplayNumberPipe,
Expand All @@ -30,7 +32,8 @@ import { DisplayTimeAgo } from './time/display-time-ago.pipe';
HighlightPipe,
DisplayTitlePipe,
OrdinalPipe,
DisplayStringEnumPipe
DisplayStringEnumPipe,
DisplayFileSizePipe
]
})
export class FormattingModule {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { createDirectiveFactory } from '@ngneat/spectator/jest';
import { DropZoneDirective } from './drop-zone.directive';

describe('Drop Zone Directive', () => {
const createDirective = createDirectiveFactory({ directive: DropZoneDirective });

test('should emit events correctly', () => {
const spectator = createDirective(`<div class="content" htDropZone></div>`);

spyOn(spectator.directive.dragHover, 'emit');
spyOn(spectator.directive.dropped, 'emit');

spectator.dispatchMouseEvent(spectator.element, 'dragover');
expect(spectator.directive.dropped.emit).toHaveBeenCalledTimes(0);
expect(spectator.directive.dragHover.emit).toHaveBeenCalledWith(true);

spectator.dispatchMouseEvent(spectator.element, 'dragleave');
expect(spectator.directive.dropped.emit).toHaveBeenCalledTimes(0);
expect(spectator.directive.dragHover.emit).toHaveBeenCalledWith(false);

spectator.dispatchMouseEvent(spectator.element, 'drop');
expect(spectator.directive.dropped.emit).toHaveBeenCalledWith(undefined);
expect(spectator.directive.dragHover.emit).toHaveBeenCalledWith(false);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Directive, EventEmitter, HostListener, Output } from '@angular/core';

@Directive({
selector: '[htDropZone]'
})
export class DropZoneDirective {
@Output()
public readonly dropped: EventEmitter<FileList> = new EventEmitter();

@Output()
public readonly dragHover: EventEmitter<boolean> = new EventEmitter();

@HostListener('drop', ['$event'])
public onDrop(event: DragEvent): void {
event.preventDefault();
this.dropped.emit(event.dataTransfer?.files);
this.dragHover.emit(false);
}

@HostListener('dragover', ['$event'])
public onDragover(event: DragEvent): void {
event.preventDefault();
this.dragHover.emit(true);
}

@HostListener('dragleave', ['$event'])
public onDragleave(event: DragEvent): void {
event.preventDefault();
this.dragHover.emit(false);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
@import 'mixins';

.file-display {
display: flex;
align-items: center;
gap: 20px;
padding: 12px;
border: 1px solid $gray-2;
border-radius: 6px;
position: relative;
z-index: 0;

.file-info {
min-width: 0;
flex: 1 1 auto;
display: flex;
flex-direction: column;
gap: 6px;

.basic-detail {
width: 100%;
display: flex;
align-items: center;

.file-name {
@include body-1-regular($gray-7);
@include ellipsis-overflow;
padding-right: 8px;
border-right: 1px solid $gray-4;
}

.file-size {
@include body-small($gray-3);
padding-left: 8px;
}
}
}

.delete-icon {
cursor: pointer;
position: absolute;
right: -4px;
top: -4px;
z-index: 1;
}

&.error {
border-color: $red-4;
background-color: $red-1;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { FormattingModule } from '@hypertrace/common';
import { createComponentFactory } from '@ngneat/spectator/jest';
import { MockComponent } from 'ng-mocks';
import { IconComponent } from '../../icon/icon.component';
import { FileUploadState } from './file-display';
import { FileDisplayComponent } from './file-display.component';

describe('File Display Component', () => {
const createComponent = createComponentFactory({
component: FileDisplayComponent,
shallow: true,
imports: [FormattingModule],
declarations: [MockComponent(IconComponent)]
});

test('should render everything correctly', () => {
const file = new File([new Blob(['text'])], 'file.txt');
const spectator = createComponent();
expect(spectator.query('.file-display')).not.toExist();

// File with no progress bar
spectator.setInput({ file: file });
expect(spectator.query('.file-display')).toExist();
expect(spectator.query('.file-name')).toHaveText('file.txt');
expect(spectator.query('.file-size')).toHaveText('4');

// Delete file action
spyOn(spectator.component.deleteClick, 'emit');
spectator.click(spectator.query('.delete-icon') as Element);
expect(spectator.component.deleteClick.emit).toHaveBeenCalled();

// Success state
spectator.setInput({ file: file, state: FileUploadState.Success, showState: true });
expect(spectator.query('.success-icon')).toExist();
expect(spectator.query('.delete-icon')).not.toExist();

// Failure state
spectator.setInput({ file: file, state: FileUploadState.Failure, showState: true });
expect(spectator.query('.file-display.error')).toExist();
expect(spectator.query('.failure-icon')).toExist();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { IconType } from '@hypertrace/assets-library';
import { Color } from '@hypertrace/common';
import { IconSize } from '../../icon/icon-size';
import { FileUploadState } from './file-display';

@Component({
selector: 'ht-file-display',
styleUrls: ['./file-display.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div *ngIf="this.file" class="file-display" [ngClass]="{ error: this.state === '${FileUploadState.Failure}' }">
<ht-icon class="note-icon" icon="${IconType.Note}" size="${IconSize.Medium}" color="${Color.Blue4}"></ht-icon>
<div class="file-info">
<div class="basic-detail">
<div class="file-name" [htTooltip]="this.file.name">{{ this.file.name }}</div>
<div class="file-size">{{ this.file.size | htDisplayFileSize }}</div>
</div>
</div>
<ng-container *ngIf="this.showState">
<ng-container [ngSwitch]="this.state"
><ng-container *ngSwitchCase="'${FileUploadState.Success}'"
><ht-icon
class="success-icon"
icon="${IconType.CheckCircle}"
color="${Color.Green5}"
></ht-icon></ng-container
><ng-container *ngSwitchCase="'${FileUploadState.Failure}'"
><ht-icon
class="failure-icon"
icon="${IconType.Alert}"
color="${Color.Red6}"
[htTooltip]="this.errorStateTooltipText"
></ht-icon></ng-container
></ng-container>
</ng-container>
<ht-icon
*ngIf="this.showDelete"
class="delete-icon"
icon="${IconType.CloseCircleFilled}"
size="${IconSize.Small}"
color="${Color.Gray9}"
(click)="this.onDeleteClick()"
></ht-icon>
</div>
`
})
export class FileDisplayComponent {
@Input()
public file?: File;

@Input()
public state: FileUploadState = FileUploadState.NotStarted;

@Input()
public showState: boolean = false;

@Input()
public errorStateTooltipText: string = '';

@Output()
public readonly deleteClick: EventEmitter<void> = new EventEmitter();

// Only show file delete option if file upload has not started or it has resulted in failure
public get showDelete(): boolean {
return this.state === FileUploadState.NotStarted || this.state === FileUploadState.Failure;
}

public onDeleteClick(): void {
this.deleteClick.emit();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormattingModule } from '@hypertrace/common';
import { IconModule } from '../../icon/icon.module';
import { TooltipModule } from '../../tooltip/tooltip.module';
import { FileDisplayComponent } from './file-display.component';

@NgModule({
imports: [CommonModule, IconModule, FormattingModule, TooltipModule],
declarations: [FileDisplayComponent],
exports: [FileDisplayComponent]
})
export class FileDisplayModule {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const enum FileUploadState {
Success = 'success',
Failure = 'failure',
InProgress = 'in-progress',
NotStarted = 'not-started'
}
Loading

0 comments on commit 771ad6c

Please sign in to comment.