README
ngx-lib
ngx-lib is an Angular 6+ library providing foundational functionality to Cleaveland/Price applications. Use of the library requires some simple configuration of the module(s) that you plan to consume in your application.
Please note that this library is not intended to be used for non-Cleaveland/Price applications.
The Angular services in this library make use of back-end web services that are proprietary and not part of this library. While the code in this library is publicly available, it's unlikely to be highly useful to developers who are not part of Cleaveland/Price. However, you can certainly use this as a reference to indicate how you might want to develop the public APIs of your back-end services. You're welcome to use this library as you see fit, just know that a lot of the "heavy lifting" (interfacing with Active Directory, SharePoint, Exchange, etc.) is done by web services that are not available via this library.
Installation
To install this library, run:
$ npm install @cleavelandprice/ngx-lib
Please note that some modules may have additional requirements. Those requirements are documented in the module-specific sections of this document.
Prerequisites
Ngx-lib requires the following packages that are not installed by default:
- @angular/cdk
- @angular/material
- hammerjs (third-party, required by Angular Material)
- @auth0/angular-jwt (third-party, required for Authentication token management)
These packages must be installed prior to using ngx-lib.
npm install @angular/cdk @angular/material
npm install hammerjs
npm install @auth0/angular-jwt
Consuming ngx-lib
After installing ngx-lib, import the module(s) that you need in your AppModule
file:
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http'; // Required by ngx-lib modules that call web services
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
// Cleaveland/Price desired module(s)
import {
AuthenticationModule,
AuthenticationServiceConfig,
DialogModule,
EmailModule,
EmailServiceConfig,
EmployeeModule,
SharePointModule,
UploadModule,
UrlModule
} from '@cleavelandprice/ngx-lib';
// Define the base URL for C/P API endpoints
// Definining this prefix here makes it easier to define the module-specific Urls
// i.e. The Email module will utilize a different endpoint than the SharePoint module
const webServices = 'http://SomeServer/api';
// Define a configuration for the Email module
const emailConfig = {
emailApiUrl: `${webServices}/email`,
server: 'EmailServerAddress',
smtpPort: 25,
sender: {
displayName: 'IT Application Process',
emailAddress: 'address@domain.com',
userName: 'emailuser',
password: 'emailpassword'
}
};
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
// Import the desired module(s)
// For the Email module, pass in the configuration that we defined above
// At this point, all of the functionality exported by each module will be available throughout this app
// This includes services, components, pipes, interfaces, classes, etc.
DialogModule,
UrlModule,
SharePointModule.forRoot({
sharePointApiUrl: `${webServices}/sharepoint`,
userName: MySharePointUser,
password: MySharePointPassword
}),
AuthenticationModule.forRoot({ authenticationApiUrl: `${webServices}/authenticate` }),
EmailModule.forRoot(emailConfig),
EmployeeModule.forRoot({ employeeApiUrl: `${webServices}/users` }),
// Note: UploadModule has more configuration options available than the minimum that is shown here.
// See the Using UploadModule section for more details
// Configure UploadModule to upload to the filesystem
UploadModule.forRoot({
uploadApiUrl: `${webServices}/upload/filesystem`,
// Note: uploadPath is optional for UploadModule
// If not specified, files will be uploaded to the "Uploads" folder of the web API
uploadPath: '\\\\server\\share'
})
// OR... Configure UploadModule to upload to the database
/* UploadModule.forRoot({
uploadApiUrl: `${webServices}/upload/database`,
// Note: dbAppId is required for database uploads
// It is used to uniquely identify an upload as belonging to a particular application
dbAppId: 'myAppId'
})*/
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Once modules are imported, you can use their components, directives, services, and pipes in your Angular application:
<!-- app.component.html example -->
<nav>
<cp-login></cp-login>
</nav>
Using AuthenticationModule
The Authentication module provides a component and a service that allows you to perform Active/Directory authentication. You can tap into this functionality "manually" - meaning that you construct the interface to acquire user credentials and pass it into the service call, and you have complete control over the user interface before, during, and after authentication.
Or, you may choose to simply use the cp-login
component, which performs all of the heavy lifting for you. By simply including the cp-login
tag in your HTML markup, the Authentication module provides its own login interface, built with Angular Material components, and displays the user's photo after authentication. This method requires no manual coding or user interface construction. It's well suited as a drop-in component for a toolbar.
Regardless of which of the two approaches you decide to take, the Authentication module will handle management of the authentication token, saving it in the browser's Local Storage and automatically retrieving it the next time the user visits the page - preventing them from having to re-authenticate each time.
The Authentication module also allows the developer to (optionally) specify an Active Directory group that a user must belong to in order to be considered an administrator of the application. When the web service returns the authentication token, an Admin flag will be set to true or false depending on whether or not the user belongs to the group specified. It is then up to the developer to customize the user's experience within the app based on this value. In some cases, it may even be desireable for the app to restrict login to only members of the specified group. In such a case, you would perform the authentication and implement some additional logic after authentication completes via the observable subscribe()
method. If the user isn't an Admin (because they don't belong to the group), you would call the logout()
method of the AuthenticationService and display a message informing them that they can't login without the required group membership.
LoginComponent requirements
If you plan to use the cp-login
tag in your HTML markup, please note that it requires font-awesome. Your application will need to install the font-awesome package and reference the font-awesome style sheet.
Since the LoginComponent also displays a login form that utiliizes Angular Material components, you will need to install @angular/material and reference an Angular Material theme stylesheet. Some Material components also require HammerJS. So you'll need to include that package as well and add an import
statement for it to your main.ts file. For more thorough up-to-date documentation on Angular Material, please visit the Angular Material site.
Installing font-awesome
npm install font-awesome
Installing Angular Material (four packages required)
npm install @angular/material @angular/cdk @angular/animations hammerjs
styles.css
/* font-awesome stylesheet */
@import "~font-awesome/css/font-awesome.min.css";
/* Angular Material 'Indigo-Pink' theme (or choose one that you like) */
@import "~@angular/material/prebuilt-themes/indigo-pink.css";
main.ts
import 'hammerjs';
Using the Authentication service (manual login/logout)
import { AuthenticationService } from '@cleavelandprice/ngx-lib';
@Component({
selector: 'app-authentication',
template: `
<input type="text" [(ngModel)]="username">
<input type="text" [(ngModel)]="password">
<img *ngIf="authenticationService.authenticated && authenticationService.authenticatedUser.photoUrl"
[src]="authenticationService.authenticatedUser.photoUrl" />
<button (click)="login()">Login</button>
<button (click)="logout()">Logout</button>
`
})
export class AuthenticationComponent implements OnInit {
username: string;
password: string;
adminGroup = 'App Admins'; // Optional AD group specifying who is an admin of this app
requiredGroup: boolean; // Optional login restriction
// Inject the AuthenticationService into this component
constructor(public authenticationService: AuthenticationService) {}
ngOnInit(): void {
// If we're requiring the user to be in a certain group to login, then check it after login
// If not a valid member, then log them out
this.authenticationService.authenticatedChanged
.subscribe(() => {
if (this.authenticationService.authenticated) {
if (this.adminGroup && this.requiredGroup) {
if (!this.authenticationService.authenticatedUser.admin) {
this.logout();
alert(`Sorry bro, you're not a member of '${this.adminGroup}'`);
return;
}
}
alert('Logged in');
} else {
alert('Logged out');
}
});
}
login() {
if (this.username && this.password) {
this.authenticationService.login(this.username, this.password, this.adminGroup);
}
}
logout() {
this.authenticationService.logout();
}
}
Interfaces provided by AuthenticationModule
- AuthenticationToken
- AuthenticationServiceConfig
Using DialogModule
The Dialog module provides a simple way to display 3 types of dialog boxes built with Angular Material components:
- Message Box displays a message to the user.
The only button displayed is an OK button. - Confirmation Box displays a simple question for the user and returns a true/false value indicating which button they selected.
The buttons can be of the following types:Ok/Cancel
(default option),Yes/No
,True/False
, 'Custom'.
Regardless of which option is selected by the developer, it will always return a true/false value, withOk
,Yes
,True
= true andCancel
,No
,False
= false. If ConfirmationDialogMode is set to Custom, then you need to pass in a string array property called customTrueFalseText, with the first element being the text for the True button, and the second being the text for the False button. - Input Box displays a dialog box with an input field that the user can type into.
The user can then click Ok, or Cancel.
The developer may provide a placeholder for the input field.
All three types of dialogs allow the developer to specify the Title and the Message to be displayed. Additionally, the developer may optionally provide a message to be displayed via a checkbox. If the checkboxMessage
is provided, then the dialog box will include a checkbox at the bottom and the value (true/false) will be returned to the calling code. This also applies to dialogs of type Message Box - which otherwise doesn't return a value. Once the value is returned, it's up to the developer to decide what to do with the checkbox value. Typically, this is used for options like Don't show me this message in the future, etc.
To prevent the user from closing the dialog without responding (i.e. clicking outside of the dialog), simply pass in the optional disableClose
parameter provided by Angular Material (this is native functionality provided by Angular).
import { MatDialog } from '@angular/material/dialog';
import {
ConfirmationDialogComponent,
ConfirmationDialogMode,
InputDialogComponent,
MessageDialogComponent
} from '@cleavelandprice/ngx-lib';
@Component({
selector: 'app-dialogs',
template: `
<input type="text" [(ngModel)]="title">
<input type="text" [(ngModel)]="message">
<input type="text" [(ngModel)]="checkboxMessage">
<input type="text" [(ngModel)]="placeholder">
`
})
export class DialogsComponent {
confirmationMode = ConfirmationDialogMode.OkCancel;
title: string;
message: string;
checkboxMessage: string; // Optional
placeholder: string; // For input box
disableClose = false; // Optional Angular Material option to specify modal
// MatDialog provided by Angular Material
constructor(public dialog: MatDialog) { }
messageBox(): void {
this.dialog
.open(MessageDialogComponent, {
data: {
title: this.title,
message: this.message,
checkboxMessage: this.checkboxMessage
},
disableClose: this.disableClose
})
.afterClosed()
.subscribe(response => {
if (this.checkboxMessage) {
console.log(response);
}
});
}
confirmationBox(): void {
this.dialog
.open(ConfirmationDialogComponent, {
data: {
title: this.title,
message: this.message,
checkboxMessage: this.checkboxMessage,
mode: this.confirmationMode
},
disableClose: this.disableClose
})
.afterClosed()
.subscribe(response => console.log(response));
}
inputBox(): void {
this.dialog
.open(InputDialogComponent, {
data: {
title: this.title,
message: this.message,
checkboxMessage: this.checkboxMessage,
placeholder: this.placeholder
},
disableClose: this.disableClose
})
.afterClosed()
.subscribe(response => console.log(response));
}
}
Enumerations provided by DialogModule
- ConfirmationDialogMode
Using EmailModule
The Email module provides a simple mechanism for sending emails via the Cleaveland/Price web services.
import { EmailService, Email } from '@cleavelandprice/ngx-lib';
@Component({
selector: 'app-email',
template: '<button (click)="sendMail()">Send Email</button>'
})
export class EmailComponent {
// Inject the EmailService into this component
constructor(private emailService: EmailService) { }
sendEmail(): void {
// Define an email object
const email: Email = {
subject: 'Test Email',
recipients: {
to: ['recipient@domain.com']
},
content: { html: '<html><body>hooray!</body></html>' }
};
// Send the email
this.emailService.send(email).subscribe();
}
}
Interfaces provided by EmailModule
- EmailServiceConfig
Using EmployeesModule
The Employees module provides a service for retrieving a list of employees from Active Directory (or a single employee), and a wealth of information about employees. Additionally, the module also provides some pre-built pipes for things like filtering, sorting, and determining group membership.
import { EmployeeService, Employee } from '@cleavelandprice/ngx-lib';
@Component({
selector: 'app-employees',
template: `
<div *ngFor="let emp of employees">
<div>{{ emp.displayName }}</div>
<div>{{ emp.department }}</div>
</div>`
})
export class EmployeesComponent implements OnInit {
employees: Employee[];
// Inject the EmployeeService into this component
constructor(private employeeService: EmployeeService) { }
ngOnInit(): void {
this.employeeService.getEmployees(true)
.subscribe(employees => this.employees = employees);
}
}
Employee pipes
departments returns a list of unique departments.
<div *ngFor="let dept of employees | departments">{{ dept}}</div>
employeeByDn returns an employee matching an Active Directory distinguishedName value.
{{ (emp.manager | employeeByDn).displayName }}
employeePhotoUrl returns the Url of an employee's photo (from SharePoint), provided an array of SharePointEmployeePhoto (see SharePointModule section for information about retrieving employee photos).
<div *ngFor="let emp of employees"><img [src]="emp | employeePhotoUrl:photos"></div>
employeesWithPropertyValue returns a list of employees with a given Active Directory attribute value (exact match).
<div *ngFor="let emp of employees | employeesWithPropertyValue:'department':'Information Technology'">{{ emp.displayName }}</div>
filterEmployees returns a filtered list of employees based on a given search string (partial or exact). The pipe will check for matches based on displayName, sAMAccountName, department, and title.
<div *ngFor="let emp of employees | filterEmployees:'daniel">{{ emp.displayName }}</div>
isMemberOf returns a true/false value indicating whether or not an employee is a member of the specified Active Directory group.
<div *ngFor="let emp of employees" [hidden]="!(emp | isMemberOf:'Domain Admins')"></div>
sortEmployees returns a list of employees sorted by the specified Active Directory attribute. By default, employees are sorted by displayName. If the value of the attribue is a number or a date, you should also provide a
dataType
parameter. The default dataType is "string". Additionally, you can provide a true/false value as a final parameter to specify whether or not you want the array to be sorted in reverse order.
<div *ngFor="let emp of employees | sortEmployees:'lastName'"></div>
<div *ngFor="let emp of employees | sortEmployees:'lastName':'string':true"></div>
<div *ngFor="let emp of employees | sortEmployees:'employeeNumber':'number'"></div>
<div *ngFor="let emp of employees | sortEmployees:'hireDate':'date'"></div>
Interfaces provided by EmployeeModule
- Employee
- EmployeeServiceConfig
Using SharePointModule
The SharePointModule provides a service that can be used to retrieve data from SharePoint. Built-in methods allow for quick retrieval of employee photos, people (employees with SharePoint accounts), and customer visits. Photos and customer visits come directly from SharePoint lists. Methods are provided to retrieve them because they are commonly needed in many applications. However, you can retrieve data from any list on any SharePoint site by constructing a SharePointList
request value and passing it to the getListItems()
method.
import { SharePointService,
SharePointList,
SharePointPerson,
SharePointPicture,
SharePointEmployeePicture,
SharePointCustomerVisit,
SharePointListItem
} from '@cleavelandprice/ngx-lib';
@Component({
selector: 'app-sharepoint',
template: `
<div *ngIf="documents">
<h1>Documents</h1>
<div *ngFor="let doc of documents">
<div>{{ doc.ows_Title }}</div>
</div>
</div>
<div *ngIf="visits">
<h1>Customer Visits</h1>
<div *ngFor="let visit of visits">
<div>{{ visit.ows_Title }}</div>
{{ visit.ows_EventDate | date:'shortDate' }}
</div>
</div>
<div *ngIf="people">
<h1>People on SharePoint</h1>
<div *ngFor="let person of people">
<div>{{ person.displayName }}</div>
<div>{{ person.department }} </div>
<div>{{ person.accountName }}</div>
</div>
</div>
<div *ngIf="photos">
<h1>Employee Photos from SharePoint - with hover tooltips</h1>
<img *ngFor="let photo of photos" [src]="photo | sharePointPhotoUrl" alt="" [title]="photo.ows_Title">
</div>
`
})
export class SharePointComponent implements OnInit {
photos: SharePointEmployeePicture[];
people: SharePointPerson[];
visits: SharePointCustomerVisit[];
documents: SharePointListItem[]; // retrieved via a custom request
// Inject the SharePointService into this component
constructor(private sharepointService: SharePointService) { }
ngOnInit() {
this.getCustomerVisits();
this.getPeople();
this.getPhotos();
this.getDocuments();
}
private getCustomerVisits(): void {
this.sharepointService.getCustomerVisits()
.subscribe(data => this.visits = data);
}
private getPeople(): void {
this.sharepointService.getPeople(true)
.subscribe(data => this.people = data);
}
private getPhotos(): void {
this.sharepointService.getPhotos(true)
.subscribe(data => this.photos = data);
}
private getDocuments(): void {
// Define a SharePointList to be used in the request
// NOTE: the 'site' property is only needed if the library exists in a subsite (different Url than the one used to configure the service)
const list: SharePointList = {
list: 'Documents',
view: 'All Documents',
site: 'http://MySharePointSiteUrl'
};
this.sharepointService.getListItems(list)
.subscribe(data => this.documents = data);
}
}
Interfaces provided by SharePointModule
- SharePointCalendarEvent
- SharePointCustomerVisit
- SharePointEmployeePicture
- SharePointListItem
- SharePointList
- SharePointPerson
- SharePointPicture
- SharePointServiceConfig
Using UploadModule
The UploadModule provides a component that can be used to upload one or more files simultaneously. Upload status is displayed via an Angular Material progress bar. By default, the UploadService (automatically called by the component) will attempt to upload files to the Uploads folder of the API (if configured in filesystem mode). Alternatively, an uploadPath configuration parameter can be provided to specify a more appropriate application-specific upload location. If upload failures occur, the most likely cause is a permissions problem (the API may not have write permissions to the destination).
If UploadModule is configured to store files in the database instead of the filesystem, the API will store them in the ITDev database in the FileUpload.Uploads table. In database mode, the dbAppId and the file name are combined to form a composite key as a unique identifier.
dbAppId is typically only configured once - during initial configuration of UploadModule in your app.module.ts file. However, because a developer may wish to use multiple application identifiers within a single application, the UploadService
allows dbAppId to be changed during runtime. See example code below.
When the user attempts to add a file to the upload component (prior to sending it to the server), the component will perform a call to the server to check for the existence of a file of the same name. If it already exists, the file will be rejected by the component and a message will be displayed. Files that do not already exist will be added to the component, ready to be sent to the server. In database mode, the dbAppId is combined with the file name to check for existence. Multiple files of the same name are only valid if the dbAppId is unique.
import { Component } from '@angular/core';
@Component({
selector: 'app-upload',
template: '<cp-upload></cp-upload>'
})
export class MyUploadComponent {
}
If you'd like to launch the file upload dialog component manually (choosing to not use the Upload button that cp-upload
gives you), you can bypass cp-upload
and implement your own mechanism (such as a custom button, etc.). To do that, you will need to import MatDialogModule
from @angular/material/dialog
in you app.module.ts file as a pre-requisite. The following is an example component that manually launches the file upload dialog through a custom button. In this example, the width of the dialog is configured by the developer - just to show that you have more control over the user interface.
import { Component } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { UploadDialogComponent } from '@cleavelandprice/ngx-lib';
@Component({
selector: 'app-upload',
template: '<button (click)="openDialog()">Open Upload Dialog</button>'
})
export class MyUploadComponent {
constructor(private dialog: MatDialog) { }
openDialog(): void {
this.dialog.open(UploadDialogComponent, { width: '25%' });
}
}
To change the dbAppId value during runtime, import UploadService
and set the value of dbAppId
prior to uploading files that should be stored with that identifier.
import { Component } from '@angular/core';
import { UploadService } from '@cleavelandprice/ngx-lib';
@Component({
selector: 'app-upload',
template: '<button (click)="changeAppId()">Change Database App Id</button>'
})
export class MyUploadComponent {
constructor(private uploadService: UploadService) { }
changeAppId(): void {
this.uploadService.dbAppId = 'newAppId';
}
}
In addition to the minimum configuration settings, the user interface of UploadModule can be further customized by providing the following values in the call to .forRoot
in app.module.ts.
- uploadButtonText
- uploadDialogTitle
- uploadDialogAddFilesButtonText
- uploadDialogCancelButtonText
- uploadDialogUploadButtonText
- uploadDialogFinishButtonText
Interfaces provided by UploadModule
- UploadServiceConfig
Using UrlModule
The UrlModule provides pipes that allow manipulation of urls used as href values in links within your application.
import { Component } from '@angular/core';
@Component({
selector: 'app-url',
template: '<a [href]="url | uriScheme | safeUrl">Open File</a>'
})
export class MyUploadComponent {
url = 'http://sharepoint-server/document1.docx';
}
Url pipes
uriScheme prefixes urls for Microsoft Office files (determined by extension) with a URI scheme string that instructs the browser to open the document in the associated app, rather that download it.
<a [href]="'http://server/file.docx' | uriScheme">Open in Microsoft Word</a>
safeUrl whitelists your url so that angular doesn't render it with an 'unsafe:' prefix in the DOM.
<a [href]="'http://server/file.docx' | uriScheme | safeUrl">Open in Microsoft Word</a>
License
MIT © Dan Rullo
This project was built using the ng-packagr project.