Skip to content

Commit

Permalink
fix(*): search results problems improved
Browse files Browse the repository at this point in the history
Merge pull request #424 from Aam-Digital/search-panel-fix
see issue #366
  • Loading branch information
sleidig authored Mar 30, 2020
2 parents 5ef38cb + b86237d commit 9d0bf54
Show file tree
Hide file tree
Showing 21 changed files with 378 additions and 133 deletions.
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
<span *ngIf="entity" (mouseenter)="showTooltip()" (mouseleave)="hideTooltip()">
<img [src]="entity?.photo" class="child-pic">
<img [src]="entity?.photo?.value" class="child-pic">
{{entity?.name}} <span style="font-size: x-small">({{entity?.projectNumber}})</span>
</span>

<div style="position:absolute;" *ngIf="tooltip">
<div class="mat-elevation-z5 child-tooltip" (mouseenter)="showTooltip()" (mouseleave)="hideTooltip()" fxLayout='row'>
<div fxFlex='30'>
<img [src]="entity?.photo" class="child-pic-large">
<img [src]="entity?.photo?.value" class="child-pic-large">
</div>
<div fxFlex>
<h3>{{entity?.name}}</h3>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@

.child-pic-photofile {
position: absolute;
left: 0;
top: 150px;
left: 1px;
top: 115px;
width: 120px;
background: white;
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,17 +58,19 @@ <h1 class="page-header section-child">
<div fxLayout='row' fxLayout.xs='column wrap' fxLayout.md='column wrap' fxLayout.sm='column wrap'>

<div fxFlex='160px' class='child-pic-container'>
<img [src]="child?.photo" class="child-pic" alt="child's photo">
<img [src]="child?.photo?.value" class="child-pic" alt="child's photo">

<input style="display: none" type="file" accept=".jpg, .jpeg, .png" (change)=uploadChildPhoto($event) #fileUpload>
<button *ngIf="enablePhotoUpload && (creatingNew || editing)"
class="child-pic-upload" (click)="fileUpload.click()">
<mat-icon class="upload-icon" fontIcon="fa-upload"></mat-icon>
</button>

<input *ngIf="(creatingNew || editing) && isAdminUser"
class="child-pic-photofile" matTooltip='filename for child photo uploaded by server administrator'
matInput formControlName="photoFile" placeholder="Photo filename" title="photoFile" type="text">
<mat-form-field *ngIf="(creatingNew || editing) && isAdminUser" class="child-pic-photofile">
<input #photoFileInput matTooltip='filename for child photo uploaded by server administrator'
matInput formControlName="photoFile" placeholder="Photo filename" title="photoFile" type="text">
<span matSuffix (click)="photoFileInput.value = ''" class='fa fa-times'></span>
</mat-form-field>
</div>


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ export class ChildDetailsComponent implements OnInit {
dropoutType: [{value: this.child.dropoutType, disabled: !this.editing}],
dropoutRemarks: [{value: this.child.dropoutRemarks, disabled: !this.editing}],

photoFile: [this.child.photoFile],
photoFile: [{value: this.child.photoFile, disabled: !this.editing}],
});


Expand Down Expand Up @@ -223,6 +223,6 @@ export class ChildDetailsComponent implements OnInit {
*/
async uploadChildPhoto(event) {
await this.childPhotoService.setImage(event.target.files[0], this.child.entityId);
this.child.photo = await this.childPhotoService.getImage(this.child);
this.child.photo.next(await this.childPhotoService.getImage(this.child));
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { TestBed } from '@angular/core/testing';
import { fakeAsync, TestBed, tick } from '@angular/core/testing';

import { ChildPhotoService } from './child-photo.service';
import { CloudFileService } from '../../../core/webdav/cloud-file-service.service';
Expand Down Expand Up @@ -69,6 +69,21 @@ describe('ChildPhotoService', () => {
});


it('should getImageAsyncObservable with multiple next() images', fakeAsync(() => {
const testChild = new Child('1');
const testImg = 'url-encoded-img';
mockCloudFileService.isConnected.and.returnValue(true);
mockCloudFileService.doesFileExist.and.returnValue(Promise.resolve(true));
mockCloudFileService.getFile.and.returnValue(Promise.resolve(testImg));

const resultSubject = service.getImageAsyncObservable(testChild);
expect(resultSubject.value).toBe(DEFAULT_IMG);

tick();
expect(resultSubject.value).toBe(testImg);
}));


it('should return false for canSetImage if no webdav connection', async () => {
mockCloudFileService.isConnected.and.returnValue(false);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Injectable } from '@angular/core';
import { SafeUrl } from '@angular/platform-browser';
import { CloudFileService } from '../../../core/webdav/cloud-file-service.service';
import { Child } from '../model/child';
import { BehaviorSubject } from 'rxjs';

@Injectable({
providedIn: 'root',
Expand All @@ -17,13 +18,20 @@ export class ChildPhotoService {
* Creates an ArrayBuffer of the photo for that Child or the default image url.
* @param child
*/
public async getImage(child: Child): Promise<SafeUrl> {
let image: SafeUrl;
public async getImage(child: { entityId: string, photoFile?: string }): Promise<SafeUrl> {
let image = await this.getImageFromCloudService(child);
if (!image) {
image = this.getImageFromAssets(child);
}
return image;
}

private async getImageFromCloudService(child: { entityId: string }): Promise<SafeUrl> {
let image;
if (this.cloudFileService.isConnected()) {
const imageType = [ '.png' , '.jpg', '.jpeg', '' ];
const imageType = ['.png', '.jpg', '.jpeg', ''];
for (const ext of imageType) {
const filepath = this.basePath + child.getId() + ext;
const filepath = this.basePath + child.entityId + ext;
try {
image = await this.cloudFileService.getFile(filepath);
break;
Expand All @@ -36,16 +44,11 @@ export class ChildPhotoService {
}
}
}

if (!image) {
image = this.getImageFromAssets(child);
}

return image;
}

private getImageFromAssets(child: Child): SafeUrl {
if (!child.photoFile) {
private getImageFromAssets(child: { photoFile?: string }): SafeUrl {
if (!child.photoFile || child.photoFile.trim() === '') {
return this.getDefaultImage();
}
return Child.generatePhotoPath(child.photoFile);
Expand All @@ -55,6 +58,24 @@ export class ChildPhotoService {
return 'assets/child.png';
}

/**
* Load the image for the given child asynchronously, immediately returning an Observable
* that initially emits the static image and later resolves to the image from the cloud service if one exists.
* This allows to immediately display a proper placeholder while the loading may take some time.
* @param child The Child instance for which the photo should be loaded.
*/
public getImageAsyncObservable(child: { entityId: string, photoFile?: string }): BehaviorSubject<SafeUrl> {
const resultSubject = new BehaviorSubject(this.getImageFromAssets(child));
this.getImageFromCloudService(child)
.then(photo => {
if (photo && photo !== resultSubject.value) {
resultSubject.next(photo);
}
resultSubject.complete();
});
return resultSubject;
}


/**
* Check if saving/uploading images is supported in the current state.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* This file is part of ndb-core.
*
* ndb-core is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ndb-core 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ndb-core. If not, see <http://www.gnu.org/licenses/>.
*/

import { fakeAsync, TestBed, tick } from '@angular/core/testing';
import { SafeUrl } from '@angular/platform-browser';
import { EntitySchemaService } from '../../../core/entity/schema/entity-schema.service';
import { DatabaseField } from '../../../core/entity/database-field.decorator';
import { Entity } from '../../../core/entity/entity';
import { ChildPhotoService } from './child-photo.service';
import { LoadChildPhotoEntitySchemaDatatype } from './datatype-load-child-photo';
import { BehaviorSubject } from 'rxjs';

describe('dataType load-child-photo', () => {
let entitySchemaService: EntitySchemaService;
let mockChildPhotoService: jasmine.SpyObj<ChildPhotoService>;

beforeEach(() => {
mockChildPhotoService = jasmine.createSpyObj('mockChildPhotoService', ['getImageAsyncObservable']);

TestBed.configureTestingModule({
providers: [
EntitySchemaService,
{ provide: ChildPhotoService, useValue: mockChildPhotoService },
],
},
);

entitySchemaService = TestBed.get(EntitySchemaService);
entitySchemaService.registerSchemaDatatype(new LoadChildPhotoEntitySchemaDatatype(mockChildPhotoService));
});


it('schema:load-child-photo is removed from rawData to be saved', function () {
class TestEntity extends Entity {
@DatabaseField({dataType: 'load-child-photo'}) photo: SafeUrl;
}
const id = 'test1';
const entity = new TestEntity(id);
entity.photo = '12345';

const rawData = entitySchemaService.transformEntityToDatabaseFormat(entity);
expect(rawData.photo).toBeUndefined();
});

it('schema:load-child-photo is provided through ChildPhotoService on load', fakeAsync(() => {
class TestEntity extends Entity {
@DatabaseField({dataType: 'load-child-photo'}) photo: BehaviorSubject<SafeUrl>;
}
const id = 'test1';
const entity = new TestEntity(id);

const defaultImg = 'default-img';
const mockCloudImg = 'test-img-data';

const mockImgObs = new BehaviorSubject(defaultImg);
mockChildPhotoService.getImageAsyncObservable.and.returnValue(mockImgObs);

const data = {
_id: id,
};
entitySchemaService.loadDataIntoEntity(entity, data);

expect(entity.photo.value).toEqual(defaultImg);

mockImgObs.next(mockCloudImg);
mockImgObs.complete();
tick();
expect(entity.photo.value).toEqual(mockCloudImg);
}));
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* This file is part of ndb-core.
*
* ndb-core is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ndb-core 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ndb-core. If not, see <http://www.gnu.org/licenses/>.
*/


import { EntitySchemaDatatype } from '../../../core/entity/schema/entity-schema-datatype';
import { ChildPhotoService } from './child-photo.service';
import { EntitySchemaField } from '../../../core/entity/schema/entity-schema-field';
import { EntitySchemaService } from '../../../core/entity/schema/entity-schema.service';
import { Entity } from '../../../core/entity/entity';

/**
* Dynamically load the child's photo through the ChildPhotoService during Entity loading process.
*/
export class LoadChildPhotoEntitySchemaDatatype implements EntitySchemaDatatype {
public readonly name = 'load-child-photo';

constructor(
private childPhotoService: ChildPhotoService,
) { }


public transformToDatabaseFormat(value) {
return undefined;
}

public transformToObjectFormat(value, schemaField: EntitySchemaField, schemaService: EntitySchemaService, parent: Entity) {
const childDummy: any = Object.assign({}, parent);
if (!childDummy.entityId) {
childDummy.entityId = Entity.extractEntityIdFromId(childDummy._id);
}

return this.childPhotoService.getImageAsyncObservable(childDummy);
}
}
30 changes: 6 additions & 24 deletions src/app/child-dev-project/children/children.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { MockDatabase } from '../../core/database/mock-database';
import { TestBed } from '@angular/core/testing';
import { Database } from 'app/core/database/database';
import { ChildPhotoService } from './child-photo-service/child-photo.service';
import { CloudFileService } from '../../core/webdav/cloud-file-service.service';
import { MockCloudFileService } from '../../core/webdav/mock-cloud-file-service';

function generateChildEntities(): Child[] {
const data = [];
Expand Down Expand Up @@ -103,10 +105,12 @@ describe('ChildrenService', () => {
beforeEach(() => {
mockChildPhotoService = jasmine.createSpyObj('mockChildPhotoService', ['getImage']);
TestBed.configureTestingModule({
providers: [EntityMapperService,
providers: [
EntityMapperService,
EntitySchemaService,
{ provide: Database, useClass: MockDatabase },
{ provide: ChildPhotoService, useValue: mockChildPhotoService },
{ provide: CloudFileService, useClass: MockCloudFileService },
ChildPhotoService,
ChildrenService,
],
},
Expand Down Expand Up @@ -140,28 +144,6 @@ describe('ChildrenService', () => {
expect(childrenBefore.length).toBe(childrenAfter.length - 1);
});

it('should load image for a single child', async() => {
let child = new Child('10');
await entityMapper.save<Child>(child);
expect(child.photo).not.toBeDefined();
mockChildPhotoService.getImage.and.returnValue(Promise.resolve('test-img'));
child = await service.getChild('10').toPromise();
expect(mockChildPhotoService.getImage).toHaveBeenCalledWith(child);
expect(child.photo).toEqual('test-img');
});

it('should load images for children', async() => {
let child = new Child('10');
await entityMapper.save<Child>(child);
expect(child.photo).not.toBeDefined();
mockChildPhotoService.getImage.and.returnValue(Promise.resolve('test-img'));
const childrenList = await service.getChildren().toPromise();
child = childrenList[0];
expect(mockChildPhotoService.getImage).toHaveBeenCalledWith(child);
expect(child.photo).toEqual('test-img');
});


it('should find a newly saved child', async () => {
const child = new Child('10');
let error;
Expand Down
Loading

0 comments on commit 9d0bf54

Please sign in to comment.