Skip to content

Commit

Permalink
feat(core/file-input): add upload to storage options in file field
Browse files Browse the repository at this point in the history
  • Loading branch information
sara-gnucoop committed Jul 1, 2022
1 parent 2955009 commit e48bd88
Show file tree
Hide file tree
Showing 9 changed files with 138 additions and 25 deletions.
2 changes: 1 addition & 1 deletion projects/core/file-input/src/file-input.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<div ajfDnd (file)="onFileDrop($event)" class="ajf-drop-zone">
<div *ngIf="value === undefined; else fileInfo" class="ajf-drop-message" (click)="triggerNativeInput()">
<div *ngIf="emptyFile; else fileInfo" class="ajf-drop-message" (click)="triggerNativeInput()">
<ng-container *ngIf="_dropMessageChildren?.length; else defaultDropMessage">
<ng-content select="[ajfDropMessage]"></ng-content>
</ng-container>
Expand Down
25 changes: 25 additions & 0 deletions projects/core/file-input/src/file-input.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ const emptyPngValue = {
type: 'image/png',
};

const emptyPngValueWithUrl = {
content: '',
name: 'empty.png',
size: n,
type: 'image/png',
url: 'http://empty.png',
};

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

Expand Down Expand Up @@ -89,6 +97,23 @@ describe('AjfFileInput', () => {
const filePreview = filePreviews[0];
expect(filePreview.innerHTML).toMatch(/empty\.png/);
});

it('should set to deleted value with url', async () => {
const fileInput = fixture.componentInstance;
fileInput.value = emptyPngFile;
const lastValue = fileInput.valueChange.pipe(shareReplay(1));
await firstValueFrom(lastValue.pipe(take(1)));
const {name, size, type} = fileInput.value as AjfFile;
fileInput.value.url = emptyPngValueWithUrl.url;
expect(name).toEqual(emptyPngFile.name);
expect(size).toEqual(emptyPngFile.size);
expect(type).toEqual(emptyPngFile.type);
fileInput.resetValue();
await firstValueFrom(lastValue.pipe(take(1)));
expect(fileInput.value.deleteUrl).toBeTrue();
expect(fileInput.value.url).toEqual(emptyPngValueWithUrl.url);
expect(fileInput.value.content).toBeNull();
});
});

describe('AjfFileInput with custom drop message', () => {
Expand Down
71 changes: 63 additions & 8 deletions projects/core/file-input/src/file-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@ export class AjfFilePreview implements OnDestroy {
}
}

/**
* It allows the upload of a file inside an AjfForm.
*
* @export
* @class AjfFileInput
*/
@Component({
selector: 'ajf-file-input',
templateUrl: './file-input.html',
Expand All @@ -96,13 +102,27 @@ export class AjfFilePreview implements OnDestroy {
export class AjfFileInput implements ControlValueAccessor {
@ContentChildren(AjfDropMessage, {descendants: false})
_dropMessageChildren!: QueryList<AjfDropMessage>;

@ContentChildren(AjfFilePreview, {descendants: false})
_filePreviewChildren!: QueryList<AjfFilePreview>;

@ViewChild('nativeInput') _nativeInput!: ElementRef<HTMLInputElement>;

readonly fileIcon: SafeResourceUrl;
readonly removeIcon: SafeResourceUrl;

/**
* Enable drop for a new file to upload
*/
private _emptyFile: boolean;
get emptyFile(): boolean {
return this._emptyFile;
}

/**
* Accepted MimeType
* Es. "image/*" or "application/pdf"
*/
@Input() accept: string | undefined;

private _value: any;
Expand All @@ -119,6 +139,9 @@ export class AjfFileInput implements ControlValueAccessor {
}
} else if (value == null || (isAjfFile(value) && isValidMimeType(value.type, this.accept))) {
this._value = value;
if (isAjfFile(value)) {
this._emptyFile = false;
}
this._valueChange.emit(this._value);
if (this._controlValueAccessorChangeFn != null) {
this._controlValueAccessorChangeFn(this.value);
Expand All @@ -133,13 +156,25 @@ export class AjfFileInput implements ControlValueAccessor {
AjfFile | undefined
>;

/** The method to be called in order to update ngModel. */
/**
* Event emitter for the delete file action
*/
private _deleteFile = new EventEmitter<string>();
@Output()
readonly deleteFile: Observable<string> = this._deleteFile as Observable<string>;

/**
* The method to be called in order to update ngModel.
*/
_controlValueAccessorChangeFn: (value: any) => void = () => {};

/** onTouch function registered via registerOnTouch (ControlValueAccessor). */
/**
* onTouch function registered via registerOnTouch (ControlValueAccessor).
*/
_onTouched: () => any = () => {};

constructor(domSanitizer: DomSanitizer, private _cdr: ChangeDetectorRef) {
this._emptyFile = true;
this.fileIcon = domSanitizer.bypassSecurityTrustResourceUrl(fileIcon);
this.removeIcon = domSanitizer.bypassSecurityTrustResourceUrl(trashIcon);
}
Expand Down Expand Up @@ -173,8 +208,20 @@ export class AjfFileInput implements ControlValueAccessor {
}

resetValue(): void {
this.value = null;
if (this.value !== null) {
if (this.value.url && this.value.url.length) {
this._deleteFile.emit(this.value.url);
this.value.deleteUrl = true;
this.value.content = null;
this.value.name = null;
this.value.size = 0;
} else {
this.value = null;
}
}
this._nativeInput.nativeElement.value = '';
this._emptyFile = true;
this._cdr.markForCheck();
}

triggerNativeInput(): void {
Expand All @@ -186,6 +233,11 @@ export class AjfFileInput implements ControlValueAccessor {

writeValue(value: any) {
this.value = value;
if (value == null || value == undefined || (value !== null && value.deleteUrl)) {
this._emptyFile = true;
} else {
this._emptyFile = false;
}
this._cdr.markForCheck();
}

Expand All @@ -201,22 +253,25 @@ export class AjfFileInput implements ControlValueAccessor {
return;
}
this.value = {name, size, type, content};
this._emptyFile = false;
};
reader.readAsDataURL(file);
}
}

const ajfFileKeys = JSON.stringify(['content', 'name', 'size', 'type']);

/**
* Test if a value is an AjfFile interface.
* The AjfFile is valid if it contains the name and
* the content or the url of the file
*/
function isAjfFile(value: any): value is AjfFile {
if (typeof value !== 'object') {
if (value == null || typeof value !== 'object') {
return false;
}
const keys = Object.keys(value).sort((a, b) => a.localeCompare(b));
return JSON.stringify(keys) === ajfFileKeys;
if ('name' in value && ('content' in value || 'url' in value)) {
return true;
}
return false;
}

function isValidMimeType(mimeType: string, accept: string | undefined): boolean {
Expand Down
6 changes: 4 additions & 2 deletions projects/core/file-input/src/file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@

export interface AjfFile {
name: string;
size: number;
size?: number;
type: string;
content: string;
content?: string;
url?: string;
deleteUrl: boolean;
}
24 changes: 17 additions & 7 deletions projects/core/forms/src/form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,17 +58,23 @@ export interface AjfFormActionEvent {

@Directive()
export abstract class AjfFormRenderer implements AfterViewChecked, AfterViewInit, OnDestroy {
// formGroup is an Observable FormGroup type
/**
* formGroup is an Observable FormGroup type
*/
readonly formGroup: Observable<FormGroup | null>;

// slides is an observable AjfSlide array type
/**
* slides is an observable AjfSlide array type
*/
readonly slides: Observable<AjfSlideInstance[]>;

readonly slidesNum: Observable<number>;
readonly errors: Observable<number>;
readonly formIsInit: Observable<boolean>;

// ajfFieldTypes [ Text, Number, Boolean, SingleChoice, MultipleChoice,
// Formula, Empty, Composed, LENGTH ]
/**
* The available ajf field types.
*/
readonly ajfFieldTypes = AjfFieldType;

@Input() title: string = '';
Expand Down Expand Up @@ -180,12 +186,16 @@ export abstract class AjfFormRenderer implements AfterViewChecked, AfterViewInit

private _errorMoveEvent: EventEmitter<boolean> = new EventEmitter<boolean>();

// _errorPositions is a private subject structure that contains next and prev
/**
* Is a private subject structure that contains next and prev
*/
private _errorPositions: Observable<number[]>;

// _form is a private ajFForm
/**
* is a private AjfForm
*/
private _form: AjfForm | undefined;
// _init is a private boolean

private _init = false;

private _nextSlideSubscription: Subscription = Subscription.EMPTY;
Expand Down
11 changes: 9 additions & 2 deletions projects/core/forms/src/image-field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ import {AJF_WARNING_ALERT_SERVICE, AjfWarningAlertService} from './warning-alert
encapsulation: ViewEncapsulation.None,
})
export class AjfImageFieldComponent extends AjfBaseFieldComponent {
readonly imageUrl: Observable<SafeResourceUrl>;
readonly imageUrl: Observable<SafeResourceUrl | null>;

constructor(
cdr: ChangeDetectorRef,
Expand All @@ -70,7 +70,14 @@ export class AjfImageFieldComponent extends AjfBaseFieldComponent {
shareReplay(1),
);
this.imageUrl = fileStream.pipe(
map(file => domSanitizer.bypassSecurityTrustResourceUrl(file.content)),
map(file => {
if (file.content && file.content.length) {
return domSanitizer.bypassSecurityTrustResourceUrl(file.content);
} else if (file.url && file.url.length) {
return domSanitizer.bypassSecurityTrustResourceUrl(file.url);
}
return null;
}),
);
}
}
2 changes: 1 addition & 1 deletion projects/core/forms/src/read-only-file-field.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<a *ngIf="fileUrl|async as fu ; else noFile" [href]="fu" [download]="fileName|async">
<a *ngIf="fileUrl|async as fu ; else noFile" [href]="fu" [download]="fileName|async" target="_blank">
<img [src]="fileIcon"> {{ fileName|async }}
</a>
<ng-template #noFile>
Expand Down
11 changes: 9 additions & 2 deletions projects/core/forms/src/read-only-file-field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ import {AJF_WARNING_ALERT_SERVICE, AjfWarningAlertService} from './warning-alert
})
export class AjfReadOnlyFileFieldComponent extends AjfBaseFieldComponent {
readonly fileIcon: SafeResourceUrl;
readonly fileUrl: Observable<SafeResourceUrl>;
readonly fileUrl: Observable<SafeResourceUrl | null>;
readonly fileName: Observable<string>;

constructor(
Expand All @@ -74,7 +74,14 @@ export class AjfReadOnlyFileFieldComponent extends AjfBaseFieldComponent {
shareReplay(1),
);
this.fileUrl = fileStream.pipe(
map(file => domSanitizer.bypassSecurityTrustResourceUrl(file.content)),
map(file => {
if (file.content && file.content.length) {
return domSanitizer.bypassSecurityTrustResourceUrl(file.content);
} else if (file.url && file.url.length && !file.deleteUrl) {
return domSanitizer.bypassSecurityTrustResourceUrl(file.url);
}
return null;
}),
);
this.fileName = fileStream.pipe(map(file => file.name));
}
Expand Down
11 changes: 9 additions & 2 deletions projects/core/forms/src/read-only-image-field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ import {AJF_WARNING_ALERT_SERVICE, AjfWarningAlertService} from './warning-alert
encapsulation: ViewEncapsulation.None,
})
export class AjfReadOnlyImageFieldComponent extends AjfBaseFieldComponent {
readonly imageUrl: Observable<SafeResourceUrl>;
readonly imageUrl: Observable<SafeResourceUrl | null>;

constructor(
cdr: ChangeDetectorRef,
Expand All @@ -71,7 +71,14 @@ export class AjfReadOnlyImageFieldComponent extends AjfBaseFieldComponent {
shareReplay(1),
);
this.imageUrl = fileStream.pipe(
map(file => domSanitizer.bypassSecurityTrustResourceUrl(file.content)),
map(file => {
if (file.content && file.content.length) {
return domSanitizer.bypassSecurityTrustResourceUrl(file.content);
} else if (file.url && file.url.length) {
return domSanitizer.bypassSecurityTrustResourceUrl(file.url);
}
return null;
}),
);
}
}

0 comments on commit e48bd88

Please sign in to comment.