[ADF - 1681] Added username and created initials pipe for user info (#2479)

* [ADF-1681] appling design spec to userinfo component

* [ADF-1681] added default pipe to userinfo

* [ADF-1681] fixed pipe name and added pipe test

* [ADF-168] fixed test for new userinfo feature

* [ADF-1681] fixed test

* [ADF-1681] fixed style for initials image

* [ADF-1681] fixed button for new material design spec

* [ADF - 1681] Added documentation for the new values

* [ADF - 1681] added some test to check new input attributes
This commit is contained in:
Vito
2017-10-16 22:55:40 +01:00
committed by Eugenio Romano
parent bf05b5df05
commit 3023d30d38
15 changed files with 325 additions and 90 deletions

View File

@@ -26,7 +26,8 @@
| bpmBackgroundImage | string | (alfresco image) | Custom path for the background banner image for BPM users |
| menuPositionX | string | | Custom choice for opening the menu bottom : `before` or `after` |
| menuPositionY | string | | Custom choice for opening the menu bottom : `above` or `below` |
| fallBackThumbnailImage | string | (alfresco image) | Fallback image for profile when thumbnail is missing|
| namePosition | string | `right` | When the username is showed this define his position relatively the user info button. It can be two values : `right` or `left`|
| showName | boolean | true | Show/Hide the username next the user info button|
## Details

View File

@@ -19,9 +19,7 @@
<mat-autocomplete #auto="matAutocomplete" (optionSelected)="onItemSelect($event.option.value)" [displayWith]="getDisplayName">
<mat-option *ngFor="let user of users; let i = index" [value]="user">
<div class="adf-people-widget-row" id="adf-people-widget-user-{{i}}">
<div class="adf-people-widget-pic">
{{getInitialUserName(user.firstName, user.lastName)}}
</div>
<div [outerHTML]="user | usernameInitials:'adf-people-widget-pic'"></div>
<div *ngIf="user.pictureId" class="adf-people-widget-image-row">
<img id="adf-people-widget-pic-{{i}}" class="adf-people-widget-image"
[src]="peopleProcessService.getUserImage(user)"/>

View File

@@ -28,7 +28,7 @@
}
&-people-widget-image {
margin-left: -45px;
margin-left: -44px;
left: 21px;
background: mat-color($background, dialog);
border-radius: 100px;

View File

@@ -116,16 +116,4 @@ export class PeopleWidgetComponent extends WidgetComponent implements OnInit {
this.field.value = item;
}
}
getInitialUserName(firstName: string, lastName: string) {
firstName = (firstName !== null && firstName !== '' ? firstName[0] : '');
lastName = (lastName !== null && lastName !== '' ? lastName[0] : '');
return this.getDisplayUser(firstName, lastName, '');
}
getDisplayUser(firstName: string, lastName: string, delimiter: string = '-'): string {
firstName = (firstName !== null ? firstName : '');
lastName = (lastName !== null ? lastName : '');
return firstName + delimiter + lastName;
}
}

View File

@@ -29,7 +29,7 @@
<data-columns>
<data-column key="firstName">
<ng-template let-entry="$implicit">
<div *ngIf="!entry.row.obj.pictureId" class="people-pic">
<div *ngIf="!entry.row.obj.pictureId" class="adf-people-search-people-pic">
{{getInitialUserName(entry.row.obj.firstName, entry.row.obj.lastName)}}</div>
<div>
<img *ngIf="entry.row.obj.pictureId" class="people-img"

View File

@@ -58,7 +58,16 @@
.people-email {
opacity: 0.54;
}
.people-pic {
}
.people-img {
border-radius: 90%;
width: 40px;
height: 40px;
vertical-align: middle;
}
.adf-people-search-people-pic {
background: mat-color($primary);
width: 30px;
padding: 10px 5px;
@@ -70,12 +79,4 @@
text-transform: uppercase;
vertical-align: text-bottom;
}
}
.people-img {
border-radius: 90%;
width: 40px;
height: 40px;
vertical-align: middle;
}
}

View File

@@ -130,6 +130,7 @@ import { FileSizePipe } from './src/pipes/file-size.pipe';
import { MimeTypeIconPipe } from './src/pipes/mime-type-icon.pipe';
import { HighlightPipe } from './src/pipes/text-highlight.pipe';
import { TimeAgoPipe } from './src/pipes/time-ago.pipe';
import { InitialUsernamePipe } from './src/pipes/user-initial.pipe';
export { InfinitePaginationComponent } from './src/components/pagination/infinite-pagination.component';
export { PaginationComponent } from './src/components/pagination/pagination.component';
@@ -139,6 +140,7 @@ export { CollapsableModule } from './src/components/collapsable/collapsable.modu
export { CardViewItem } from './src/interface/card-view-item.interface';
export { TimeAgoPipe } from './src/pipes/time-ago.pipe';
export { EXTENDIBLE_COMPONENT } from './src/interface/injection.tokens';
export { InitialUsernamePipe } from './src/pipes/user-initial.pipe';
export * from './src/components/data-column/data-column.component';
export * from './src/components/data-column/data-column-list.component';
@@ -219,7 +221,8 @@ export function pipes() {
FileSizePipe,
HighlightPipe,
TimeAgoPipe,
MimeTypeIconPipe
MimeTypeIconPipe,
InitialUsernamePipe
];
}

View File

@@ -0,0 +1,95 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { DomSanitizer } from '@angular/platform-browser';
import { LightUserRepresentation } from '../models/user-process.model';
import { InitialUsernamePipe } from './user-initial.pipe';
class FakeSanitazer extends DomSanitizer {
constructor() {
super();
}
sanitize(html) {
return html;
}
bypassSecurityTrustHtml(value: string): any {
return value;
}
bypassSecurityTrustStyle(value: string): any {
return null;
}
bypassSecurityTrustScript(value: string): any {
return null;
}
bypassSecurityTrustUrl(value: string): any {
return null;
}
bypassSecurityTrustResourceUrl(value: string): any {
return null;
}
}
describe('UserInitialPipe', () => {
let pipe: InitialUsernamePipe;
let fakeUser: LightUserRepresentation;
beforeEach(() => {
pipe = new InitialUsernamePipe(new FakeSanitazer());
fakeUser = new LightUserRepresentation();
});
it('should return a div with the user initials', () => {
fakeUser.firstName = 'FAKE-NAME';
fakeUser.lastName = 'FAKE-SURNAME';
let result = pipe.transform(fakeUser);
expect(result).toBe('<div id="user-initials-image" class="">FF</div>');
});
it('should apply the style class passed in input', () => {
fakeUser.firstName = 'FAKE-NAME';
fakeUser.lastName = 'FAKE-SURNAME';
let result = pipe.transform(fakeUser, 'fake-class-to-check');
expect(result).toBe('<div id="user-initials-image" class="fake-class-to-check">FF</div>');
});
it('should return a single letter into div when lastname is undefined', () => {
fakeUser.firstName = 'FAKE-NAME';
fakeUser.lastName = undefined;
let result = pipe.transform(fakeUser);
expect(result).toBe('<div id="user-initials-image" class="">F</div>');
});
it('should return a single letter into div when firstname is null', () => {
fakeUser.firstName = undefined;
fakeUser.lastName = 'FAKE-SURNAME';
let result = pipe.transform(fakeUser);
expect(result).toBe('<div id="user-initials-image" class="">F</div>');
});
it('should return an empty string when user is null', () => {
let result = pipe.transform(null);
expect(result).toBe('');
});
});

View File

@@ -0,0 +1,43 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Pipe, PipeTransform } from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { LightUserRepresentation } from '../models/user-process.model';
@Pipe({
name: 'usernameInitials'
})
export class InitialUsernamePipe implements PipeTransform {
constructor(private sanitized: DomSanitizer) {}
transform(user: LightUserRepresentation, className: string = '', delimiter: string = ''): SafeHtml {
let result: SafeHtml = '';
if (user) {
let initialResult = this.getInitialUserName(user.firstName, user.lastName, delimiter);
result = this.sanitized.bypassSecurityTrustHtml(`<div id="user-initials-image" class="${className}">${initialResult}</div>`);
}
return result;
}
getInitialUserName(firstName: string, lastName: string, delimiter: string) {
firstName = (firstName ? firstName[0] : '');
lastName = (lastName ? lastName[0] : '');
return firstName + delimiter + lastName;
}
}

View File

@@ -20,7 +20,7 @@ Contains the Alfresco User Info component.
## Documentation
See the [ADF Analytics](../../docs/README.md#adf-analytics) section of the [docs index](../../docs/README.md)
See the [ADF Userinfo](../../docs/README.md#adf-userinfo) section of the [docs index](../../docs/README.md)
for all available documentation on this library.
## Prerequisites

View File

@@ -57,7 +57,7 @@ export let fakeBpmUser = {
lastUpdate: 'fake-update-date',
latestSyncTimeStamp: 'fake-timestamp',
password: 'fake-password',
pictureId: 'src/assets/bpmImg.gif',
pictureId: 12,
status: 'fake-status',
tenantId: 'fake-tenant-id',
tenantName: 'fake-tenant-name',

View File

@@ -1,9 +1,26 @@
<div id="userinfo_container" *ngIf="isLoggedIn()">
<div id="userinfo_container"
[class.adf-userinfo-name-right]="showOnRight()"
class="adf-userinfo-container" *ngIf="isLoggedIn()">
<span *ngIf="ecmUser && showName" id="adf-userinfo-ecm-name-display"
class="adf-userinfo-name">{{ecmUser.fullNameDisplay}}</span>
<span *ngIf="bpmUser && !ecmUser && showName" id="adf-userinfo-bpm-name-display"
class="adf-userinfo-name">{{bpmUser.fullNameDisplay}}</span>
<button mat-button [matMenuTriggerFor]="menu" class="adf-userinfo-menu_button">
<div class="adf-userinfo-button-profile" id="user-profile" data-automation-id="user-profile">
<img id="logged-user-img" [src]="getUserAvatar()"
alt="user-info-profile-button"
(error)="onImageLoadingError($event)" class="adf-userinfo-profile-image"/>
<div *ngIf="bpmUser && !ecmUser" id="bpm-user-image">
<div [outerHTML]="bpmUser | usernameInitials:'adf-userinfo-pic'"></div>
<div *ngIf="bpmUser.pictureId" class="adf-userinfo-profile-container">
<img id="logged-user-img" [src]="bpmUserImage" alt="user-info-profile-button"
class="adf-userinfo-profile-image"/>
</div>
</div>
<div *ngIf="ecmUser" id="ecm-user-image">
<div [outerHTML]="ecmUser | usernameInitials:'adf-userinfo-pic'"></div>
<div *ngIf="ecmUser.avatarId" class="adf-userinfo-profile-container">
<img id="logged-user-img" [src]="ecmUserImage" alt="user-info-profile-button"
class="adf-userinfo-profile-image"/>
</div>
</div>
</div>
</button>
<mat-menu #menu="matMenu" id="user-profile-lists" [xPosition]="menuPositionX" [yPosition]="menuPositionY" [overlapTrigger]="false" class="adf-userinfo-menu">
@@ -12,11 +29,11 @@
<mat-tab id="ecm-panel" label="Content Services" *ngIf="ecmUser">
<mat-card class="adf-userinfo-card">
<mat-card-header class="adf-userinfo-card-header" [style.background-image]="'url(' + ecmBackgroundImage + ')'">
<img class="adf-userinfo-profile-picture"
id="ecm-user-detail-image"
alt="ecm-profile-image"
(error)="onImageLoadingError($event)"
[src]="getEcmUserAvatar()" />
<div [outerHTML]="ecmUser | usernameInitials:'adf-userinfo-profile-initials'"></div>
<div *ngIf="ecmUser.avatarId" class="adf-userinfo-profile-container">
<img class="adf-userinfo-profile-picture" id="ecm-user-detail-image"
alt="ecm-profile-image" [src]="ecmUserImage" />
</div>
<div class="adf-userinfo-title" id="ecm-username">{{ecmUser.fullNameDisplay}}</div>
</mat-card-header>
<mat-card-content>
@@ -38,11 +55,9 @@
<mat-tab label="Process Services" id="bpm-panel" *ngIf="bpmUser">
<mat-card class="adf-userinfo-card">
<mat-card-header class="adf-userinfo-card-header" [style.background-image]="'url(' + bpmBackgroundImage + ')'">
<img class="adf-userinfo-profile-picture"
id="bpm-user-detail-image"
alt="bpm-profile-image"
(error)="onImageLoadingError($event)"
[src]="getBpmUserAvatar()"/>
<div [outerHTML]="bpmUser | usernameInitials:'adf-userinfo-profile-initials'"></div>
<img *ngIf="bpmUser.pictureId" class="adf-userinfo-profile-picture" id="bpm-user-detail-image"
alt="bpm-profile-image" [src]="bpmUserImage"/>
<div class="adf-userinfo-title" id="bpm-username">{{bpmUser.fullNameDisplay}}</div>
</mat-card-header>
<mat-card-content>

View File

@@ -2,9 +2,38 @@
$primary: map-get($theme, primary);
$accent: map-get($theme, accent);
$warn: map-get($theme, warn);
$foreground: map-get($theme, foreground);
.adf {
&-userinfo-container {
display: flex;
align-items: center;
padding: 0 5px 0 5px;
}
&-userinfo-name-right {
flex-direction: row-reverse;
}
&-userinfo-name {
padding: 0 5px 0 5px;
}
&-userinfo-pic {
background: mat-color($primary, 300);
display: inline-block;
width: 40px;
height: 40px;
border-radius: 100px;
text-align: center;
font-weight: bolder;
font-size: 18px;
text-transform: uppercase;
vertical-align: middle;
line-height: 40px
}
&-userinfo-profile-image {
text-align: center;
border-radius: 90%;
@@ -13,6 +42,11 @@
margin-right: 0;
cursor: pointer;
vertical-align: middle;
margin-left: -45px;
}
&-userinfo-profile-container{
display: inline-block;
}
&-userinfo-menu_button.mat-button {
@@ -101,9 +135,25 @@
border-radius: 50%;
height: 80px;
width: 80px;
z-index: 3;
margin-left: -88px;
margin-right: 8px;
}
&-userinfo-profile-initials {
background-size: cover;
background: mat-color($primary, 300);
border-radius: 50%;
height: 80px;
width: 80px;
margin-left: 0;
margin-right: 8px;
font-size: 35px;
font-weight: 400;
letter-spacing: 0;
line-height: 78px;
overflow: hidden;
display: flex;
justify-content: space-around;
}
&-userinfo-button-profile {

View File

@@ -81,17 +81,6 @@ describe('User info component', () => {
expect(element.querySelector('#user-profile-lists')).toBeNull();
});
it('should return the anonymous avatar when users do not have images', () => {
let event = <any> {
target: {
src: ''
}
};
userInfoComp.onImageLoadingError(event);
expect(event.target.src).toContain('assets/images/anonymous');
expect(event.target.src).toContain('.gif');
});
describe('when user is logged on ecm', () => {
beforeEach(() => {
@@ -113,6 +102,7 @@ describe('User info component', () => {
});
fixture.whenStable().then(() => {
fixture.detectChanges();
let imageButton: HTMLButtonElement = <HTMLButtonElement> element.querySelector('#logged-user-img');
imageButton.click();
fixture.detectChanges();
@@ -125,6 +115,65 @@ describe('User info component', () => {
});
}));
it('should show the username when showName attribute is true', async(() => {
fixture.detectChanges();
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'application/json',
responseText: JSON.stringify({entry: fakeEcmEditedUser})
});
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(userInfoComp.showName).toBeTruthy();
expect(element.querySelector('#adf-userinfo-ecm-name-display')).not.toBeNull();
});
}));
it('should hide the username when showName attribute is false', async(() => {
userInfoComp.showName = false;
fixture.detectChanges();
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'application/json',
responseText: JSON.stringify({entry: fakeEcmEditedUser})
});
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(element.querySelector('#adf-userinfo-ecm-name-display')).toBeNull();
});
}));
it('should have the defined class to show the name on the right side', async(() => {
fixture.detectChanges();
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'application/json',
responseText: JSON.stringify({entry: fakeEcmEditedUser})
});
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(element.querySelector('#userinfo_container').classList).toContain('adf-userinfo-name-right');
});
}));
it('should not have the defined class to show the name on the left side', async(() => {
userInfoComp.namePosition = 'left';
fixture.detectChanges();
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'application/json',
responseText: JSON.stringify({entry: fakeEcmEditedUser})
});
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(element.querySelector('#userinfo_container').classList).not.toContain('adf-userinfo-name-right');
});
}));
describe('and has image', () => {
beforeEach(async(() => {
@@ -139,6 +188,7 @@ describe('User info component', () => {
it('should get the ecm current user image from the service', async(() => {
fixture.whenStable().then(() => {
fixture.detectChanges();
let imageButton: HTMLButtonElement = <HTMLButtonElement> element.querySelector('#logged-user-img');
imageButton.click();
fixture.detectChanges();
@@ -152,6 +202,7 @@ describe('User info component', () => {
it('should get the ecm user informations from the service', () => {
fixture.whenStable().then(() => {
fixture.detectChanges();
let imageButton: HTMLButtonElement = <HTMLButtonElement> element.querySelector('#logged-user-img');
imageButton.click();
fixture.detectChanges();
@@ -172,17 +223,17 @@ describe('User info component', () => {
describe('and has no image', () => {
beforeEach(async(() => {
userInfoComp.anonymousImageUrl = userInfoComp.anonymousImageUrl.replace('/base/dist', '');
fixture.detectChanges();
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'application/json',
responseText: JSON.stringify({entry: fakeEcmUserNoImage})
});
fixture.whenStable().then(() => fixture.detectChanges());
}));
it('should show N/A when the job title is null', async(() => {
let imageButton: HTMLButtonElement = <HTMLButtonElement> element.querySelector('#logged-user-img');
let imageButton: HTMLButtonElement = <HTMLButtonElement> element.querySelector('#user-initials-image');
imageButton.click();
fixture.detectChanges();
expect(element.querySelector('#userinfo_container')).not.toBeNull();
@@ -193,7 +244,7 @@ describe('User info component', () => {
}));
it('should not show the tabs', () => {
let imageButton: HTMLButtonElement = <HTMLButtonElement> element.querySelector('#logged-user-img');
let imageButton: HTMLButtonElement = <HTMLButtonElement> element.querySelector('#user-initials-image');
imageButton.click();
fixture.detectChanges();
let tabHeader = fixture.debugElement.query(By.css('#tab-group-env'));
@@ -209,8 +260,6 @@ describe('User info component', () => {
spyOn(stubAuthService, 'isBpmLoggedIn').and.returnValue(true);
spyOn(stubAuthService, 'isLoggedIn').and.returnValue(true);
jasmine.Ajax.install();
userInfoComp.anonymousImageUrl = userInfoComp.anonymousImageUrl.replace('/base/dist', '');
}));
afterEach(() => {
@@ -226,6 +275,7 @@ describe('User info component', () => {
});
fixture.whenStable().then(() => {
fixture.detectChanges();
let imageButton: HTMLButtonElement = <HTMLButtonElement> element.querySelector('#logged-user-img');
imageButton.click();
fixture.detectChanges();
@@ -323,7 +373,6 @@ describe('User info component', () => {
spyOn(stubAuthService, 'isBpmLoggedIn').and.returnValue(true);
spyOn(stubAuthService, 'isLoggedIn').and.returnValue(true);
spyOn(stubContent, 'getContentUrl').and.returnValue('src/assets/images/ecmImg.gif');
userInfoComp.anonymousImageUrl = userInfoComp.anonymousImageUrl.replace('/base/dist', '');
jasmine.Ajax.install();
}));
@@ -342,6 +391,7 @@ describe('User info component', () => {
}));
beforeEach(() => {
fixture.detectChanges();
let imageButton: HTMLButtonElement = <HTMLButtonElement> element.querySelector('#logged-user-img');
imageButton.click();
fixture.detectChanges();
@@ -386,11 +436,14 @@ describe('User info component', () => {
it('should show the bpm image if ecm does not have it', () => {
userInfoComp.ecmUserImage = null;
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(element.querySelector('#userinfo_container')).toBeDefined();
expect(element.querySelector('#logged-user-img')).toBeDefined();
expect(element.querySelector('#logged-user-img').getAttribute('src')).toContain('rest/admin/profile-picture');
});
});
it('should show the tabs for the env', () => {
let tabGroup = fixture.debugElement.query(By.css('#tab-group-env'));

View File

@@ -45,7 +45,10 @@ export class UserInfoComponent implements OnInit {
menuPositionY: string = 'below';
@Input()
fallBackThumbnailImage: string;
showName: boolean = true;
@Input()
namePosition: string = 'right';
ecmUser: EcmUserModel;
bpmUser: BpmUserModel;
@@ -101,13 +104,6 @@ export class UserInfoComponent implements OnInit {
}
}
onImageLoadingError(event) {
if (event) {
let element = <any> event.target;
element.src = this.fallBackThumbnailImage || this.anonymousImageUrl;
}
}
stopClosing(event) {
event.stopPropagation();
}
@@ -116,15 +112,7 @@ export class UserInfoComponent implements OnInit {
this.ecmUserImage = this.ecmUserService.getUserProfileImage(this.ecmUser.avatarId);
}
getUserAvatar() {
return this.ecmUserImage || this.bpmUserImage;
}
getBpmUserAvatar() {
return this.bpmUserImage;
}
getEcmUserAvatar() {
return this.ecmUserImage;
showOnRight() {
return this.namePosition === 'right';
}
}