├── src
├── app
│ ├── components
│ │ ├── app
│ │ │ ├── app.component.scss
│ │ │ ├── index.ts
│ │ │ ├── app.component.ts
│ │ │ └── app.component.html
│ │ ├── home
│ │ │ ├── index.ts
│ │ │ ├── home.component.html
│ │ │ └── home.component.ts
│ │ ├── contact
│ │ │ ├── index.ts
│ │ │ ├── contact.component.scss
│ │ │ ├── create-contact.component.ts
│ │ │ ├── create-contact.component.html
│ │ │ ├── contact.component.html
│ │ │ └── contact.component.ts
│ │ └── index.ts
│ ├── resolves
│ │ ├── index.ts
│ │ └── salesforce.resolver.ts
│ ├── directives
│ │ ├── index.ts
│ │ ├── gravatar.directive.ts
│ │ └── contentEditableModel.directive.ts
│ ├── pipes
│ │ ├── index.ts
│ │ ├── keys.pipe.ts
│ │ └── newlineToBreak.pipe.ts
│ ├── services
│ │ ├── index.ts
│ │ ├── logger.service.ts
│ │ └── salesforce.service.ts
│ ├── main.ts
│ ├── app.routing.ts
│ ├── app.module.ts
│ └── shared
│ │ └── sobjects.ts
├── salesforce
│ └── classes
│ │ ├── AngularAppController.cls-meta.xml
│ │ └── AngularAppController.cls
├── index.page.html
├── systemjs.config.js
└── styles
│ └── material.min.css
├── .gitignore
├── typings.json
├── tsconfig.json
├── config.sample.js
├── gulpfile.js
├── app.js
├── LICENSE
├── gulp
├── styles.js
├── scripts.js
├── html.js
└── deploy.js
├── package.json
└── README.md
/src/app/components/app/app.component.scss:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/components/app/index.ts:
--------------------------------------------------------------------------------
1 | export { AppComponent } from './app.component';
--------------------------------------------------------------------------------
/src/app/components/home/index.ts:
--------------------------------------------------------------------------------
1 | export { HomeComponent } from './home.component';
--------------------------------------------------------------------------------
/src/app/resolves/index.ts:
--------------------------------------------------------------------------------
1 | export { SalesforceResolver } from './salesforce.resolver';
--------------------------------------------------------------------------------
/src/app/directives/index.ts:
--------------------------------------------------------------------------------
1 | export { ContentEditableModelDirective } from './contentEditableModel.directive';
--------------------------------------------------------------------------------
/src/app/pipes/index.ts:
--------------------------------------------------------------------------------
1 | export { NewlineToBreakPipe } from './newlineToBreak.pipe';
2 | export { KeysPipe } from './keys.pipe';
--------------------------------------------------------------------------------
/src/app/services/index.ts:
--------------------------------------------------------------------------------
1 | export { LoggerService, LOG_LEVEL } from './logger.service';
2 | export { SalesforceService, SOQL, API } from './salesforce.service';
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .ssh/
2 | .tmp/
3 | build/
4 | config.js
5 | node_modules/
6 | vendor/
7 | typings/
8 | *.sublime-workspace
9 | *.sublime-project
10 | .vscode
--------------------------------------------------------------------------------
/src/app/components/contact/index.ts:
--------------------------------------------------------------------------------
1 | export { ContactComponent } from './contact.component';
2 | export { CreateContactComponent } from './create-contact.component';
--------------------------------------------------------------------------------
/src/app/main.ts:
--------------------------------------------------------------------------------
1 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
2 | import { AppModule } from './app.module';
3 | platformBrowserDynamic().bootstrapModule(AppModule);
4 |
--------------------------------------------------------------------------------
/src/app/components/index.ts:
--------------------------------------------------------------------------------
1 | export { AppComponent } from './app/index';
2 | export { HomeComponent } from './home/index';
3 | export { ContactComponent, CreateContactComponent } from './contact/index';
--------------------------------------------------------------------------------
/src/salesforce/classes/AngularAppController.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 36.0
4 | Active
5 |
6 |
--------------------------------------------------------------------------------
/typings.json:
--------------------------------------------------------------------------------
1 | {
2 | "globalDependencies": {
3 | "core-js": "registry:dt/core-js#0.0.0+20160725163759",
4 | "node": "registry:dt/node#6.0.0+20161110151007"
5 | },
6 | "dependencies": {
7 | "es6-promise": "registry:npm/es6-promise#3.0.0+20160723033700"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/app/pipes/keys.pipe.ts:
--------------------------------------------------------------------------------
1 | import { Pipe, PipeTransform } from '@angular/core';
2 |
3 | @Pipe({
4 | name: 'keys'
5 | })
6 |
7 | export class KeysPipe implements PipeTransform {
8 | transform(value: any, args: any[]): any {
9 | if (typeof(value) !== 'object') return value;
10 | return Object.keys(value);
11 | }
12 | }
--------------------------------------------------------------------------------
/src/app/components/app/app.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import { SalesforceService, LoggerService } from '../../services/index';
3 |
4 | @Component({
5 | moduleId: module.id,
6 | selector: 'app',
7 | templateUrl: 'app.component.html'
8 | })
9 | export class AppComponent {
10 |
11 | constructor(public sfdc: SalesforceService, public log: LoggerService) {
12 |
13 | }
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/src/app/pipes/newlineToBreak.pipe.ts:
--------------------------------------------------------------------------------
1 | import { Pipe, PipeTransform } from '@angular/core';
2 |
3 | @Pipe({
4 | name: 'newlineToBreak'
5 | })
6 | export class NewlineToBreakPipe implements PipeTransform {
7 | transform(value: any, args: any[]): any {
8 | if(typeof(value) === 'string') {
9 | return value.replace(/(?:\r\n|\r|\n)/g, '
');
10 | } else {
11 | return value;
12 | }
13 | }
14 | }
--------------------------------------------------------------------------------
/src/app/components/app/app.component.html:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 3,
3 | "compilerOptions": {
4 | "target": "es5",
5 | "module": "commonjs",
6 | "moduleResolution": "node",
7 | "sourceMap": true,
8 | "emitDecoratorMetadata": true,
9 | "experimentalDecorators": true,
10 | "removeComments": false,
11 | "noImplicitAny": false,
12 | "rootDir": "src"
13 | },
14 | "exclude": [
15 | "node_modules"
16 | ],
17 | "buildOnSave": false,
18 | "compileOnSave": false
19 | }
--------------------------------------------------------------------------------
/src/app/components/contact/contact.component.scss:
--------------------------------------------------------------------------------
1 | .detail.line {
2 | font-size: 1.2rem;
3 | display: block;
4 | margin-bottom: .5rem;
5 |
6 | .detail.label {
7 | width: 200px;
8 | display: inline-block;
9 | vertical-align: middle;
10 | font-weight: 700;
11 | float: left;
12 | }
13 |
14 | .detail.content {
15 | float: left;
16 | display: inline-block;
17 |
18 | &[contenteditable="true"] {
19 | border-bottom: 1px solid #DDD;
20 | min-width: 200px;
21 | }
22 | }
23 |
24 | &:after {
25 | content: "";
26 | display: table;
27 | clear: both;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/config.sample.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | deploy: {
3 | username: 'user.name@yourcompany.com',
4 | password: 'YourPasswordAndPossiblySecurityToken',
5 | login_url: 'https://login.salesforce.com',
6 | api_version: 36.0,
7 | timeout: 120000,
8 | poll_interval: 5000,
9 | },
10 |
11 | visualforce: {
12 | template: 'index.page.html',
13 | page: 'AngularApp',
14 | controller: 'AngularAppController'
15 | },
16 |
17 | resources: {
18 | app_resource_name: 'AngularApp',
19 | node_module_resource_name: 'NodeModules',
20 | },
21 |
22 | options: {
23 |
24 | }
25 | }
--------------------------------------------------------------------------------
/src/app/components/home/home.component.html:
--------------------------------------------------------------------------------
1 | Contact Management
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const gulp = require('gulp'),
4 | fs = require('fs'),
5 | gls = require('gulp-live-server');
6 |
7 | let config = require('./config');
8 | let server = gls.new('app.js');
9 |
10 | gulp.task('serve', () => {
11 | return server.start();
12 | });
13 |
14 | // All tasks are located in other files within the gulp folder
15 | require('./gulp/scripts')(gulp, config, server);
16 | require('./gulp/styles')(gulp, config, server);
17 | require('./gulp/html')(gulp, config, server);
18 | require('./gulp/deploy')(gulp, config);
19 |
20 | gulp.task('watch:all', gulp.parallel('watch:scripts', 'watch:styles', 'watch:html'))
21 | gulp.task('default', gulp.series('scripts:dev', 'styles:dev', 'html:dev', 'visualforce:dev', gulp.parallel('watch:all', 'serve')));
22 |
--------------------------------------------------------------------------------
/src/app/directives/gravatar.directive.ts:
--------------------------------------------------------------------------------
1 | import { Directive, Input, ElementRef, OnInit, OnChanges, SimpleChanges } from '@angular/core';
2 | import { Http, Response } from '@angular/http';
3 | import { MD5 } from 'crypto-js';
4 |
5 | @Directive({
6 | selector: '[gravatar]'
7 | })
8 | export class GravatarDirective implements OnInit, OnChanges {
9 |
10 | @Input('gravatar') email: string;
11 | @Input() size: number = 300;
12 | public gravatarUrl: string;
13 |
14 | constructor(private el: ElementRef) {}
15 |
16 | private getAvatarUrl() {
17 | let hash: string = MD5(this.email);
18 | let url: string = `http://gravatar.com/avatar/${hash}.json?s=${this.size}`;
19 |
20 | let el: HTMLImageElement = this.el.nativeElement;
21 | el.src = url;
22 | }
23 |
24 | ngOnChanges(changes: SimpleChanges) {
25 | if (changes['email'].previousValue !== changes['email'].currentValue) {
26 | this.getAvatarUrl();
27 | }
28 | }
29 |
30 | ngOnInit(): void {
31 | this.getAvatarUrl();
32 | }
33 |
34 | }
--------------------------------------------------------------------------------
/src/app/app.routing.ts:
--------------------------------------------------------------------------------
1 | import { Routes, RouterModule } from '@angular/router';
2 |
3 | import { HomeComponent } from './components/index';
4 | import { ContactComponent, CreateContactComponent } from './components/index';
5 |
6 | import { SalesforceResolver } from './resolves/index';
7 |
8 | const appRoutes: Routes = [
9 | {
10 | path: '',
11 | redirectTo: 'home',
12 | pathMatch: 'full'
13 | },
14 | {
15 | path: 'home',
16 | component: HomeComponent,
17 | resolve: {
18 | sfdc: SalesforceResolver
19 | }
20 | },
21 | {
22 | path: 'contact/view/:id',
23 | component: ContactComponent,
24 | resolve: {
25 | sfdc: SalesforceResolver
26 | }
27 | },
28 | {
29 | path: 'contact/new',
30 | component: CreateContactComponent,
31 | resolve: {
32 | sfdc: SalesforceResolver
33 | }
34 | }
35 | // { path: '**', component: PageNotFoundComponent }
36 | ];
37 |
38 | export const appRoutingProviders: any[] = [
39 |
40 | ];
41 |
42 | export const routing = RouterModule.forRoot(appRoutes, { useHash: true });
43 |
--------------------------------------------------------------------------------
/app.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const express = require('express'),
4 | serveStatic = require('serve-static'),
5 | jsforceAjaxProxy = require('jsforce-ajax-proxy'),
6 | http = require('http'),
7 | // https = require('https'),
8 | fs = require('fs');
9 |
10 | let app = express();
11 |
12 | //you won't need 'connect-livereload' if you have livereload plugin for your browser
13 | app.use(require('connect-livereload')());
14 | app.use('/', serveStatic('build'));
15 | app.use('/node_modules', serveStatic('node_modules'));
16 | app.all('/proxy/?*', jsforceAjaxProxy({ enableCORS: true }));
17 |
18 | // let privateKey = fs.readFileSync('.ssh/domain.key', 'utf8');
19 | // let certificate = fs.readFileSync('.ssh/domain.crt', 'utf8');
20 | // let credentials = {key: privateKey, cert: certificate};
21 |
22 | let server = http.createServer(app);
23 | server = server.listen(8085, function() {
24 | console.log('http listening on port 8085')
25 | });
26 |
27 | // let httpsServer = https.createServer(credentials, app);
28 | // httpsServer = httpsServer.listen(8080, function() {
29 | // console.log('https listening on port 8080')
30 | // });
31 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License
2 |
3 | Copyright (c) 2010-2016 Chris Watson
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/app/directives/contentEditableModel.directive.ts:
--------------------------------------------------------------------------------
1 | import { Directive, Input, Output, ElementRef, OnChanges, EventEmitter, SimpleChanges } from '@angular/core';
2 |
3 | @Directive({
4 | selector: '[contentEditableModel]',
5 | host: {
6 | '(keyup)': 'onKeyUp()'
7 | }
8 | })
9 | export class ContentEditableModelDirective implements OnChanges {
10 |
11 | @Input('contentEditableModel') model: any;
12 | @Output('contentEditableModelChange') update = new EventEmitter();
13 |
14 | private lastViewModel: any;
15 |
16 | constructor(private el: ElementRef) {}
17 |
18 | public onKeyUp() {
19 | let value = this.el.nativeElement.innerText;
20 | this.lastViewModel = value;
21 | this.update.emit(value);
22 | }
23 |
24 | ngOnChanges(changes: SimpleChanges) {
25 | if (changes['model'].currentValue !== changes['model'].previousValue) {
26 | if (!changes['model'].currentValue) { this.model = null; }
27 | this.lastViewModel = this.model;
28 | this.refreshView();
29 | }
30 | }
31 |
32 | private refreshView() {
33 | this.el.nativeElement.innerText = this.model;
34 | }
35 |
36 | }
--------------------------------------------------------------------------------
/gulp/styles.js:
--------------------------------------------------------------------------------
1 | module.exports = function(gulp, config) {
2 | 'use strict';
3 |
4 | const sourcemaps = require('gulp-sourcemaps'),
5 | sass = require('gulp-sass');
6 |
7 | gulp.task('sass:dev', () => {
8 | return gulp.src(['src/**/*.scss'])
9 | .pipe(sourcemaps.init())
10 | .pipe(sass().on('error', sass.logError))
11 | .pipe(sourcemaps.write())
12 | .pipe(gulp.dest('build'));
13 | });
14 |
15 | gulp.task('sass:prod', () => {
16 | return gulp.src(['src/**/*.scss'])
17 | .pipe(sass({
18 | outputStyle: 'compressed'
19 | }).on('error', sass.logError))
20 | .pipe(gulp.dest('build'));
21 | });
22 |
23 | gulp.task('css:dev', () => {
24 | return gulp.src(['src/**/*.css'])
25 | .pipe(gulp.dest('build'));
26 | });
27 |
28 | gulp.task('css:prod', () => {
29 | return gulp.src(['src/**/*.css'])
30 | .pipe(gulp.dest('build'));
31 | });
32 |
33 | gulp.task('styles:dev', gulp.parallel('sass:dev', 'css:dev'));
34 | gulp.task('styles:prod', gulp.parallel('sass:prod', 'css:prod'));
35 |
36 | gulp.task('watch:styles', () => {
37 | gulp.watch('src/**/*.scss', gulp.series('sass:dev'));
38 | gulp.watch('src/**/*.css', gulp.series('css:dev'));
39 | });
40 | }
41 |
--------------------------------------------------------------------------------
/src/salesforce/classes/AngularAppController.cls:
--------------------------------------------------------------------------------
1 | global class AngularAppController {
2 |
3 | @RemoteAction
4 | WebService static Contact upsertContact(Contact contact) {
5 | upsert contact;
6 | return contact;
7 | }
8 |
9 | @RemoteAction
10 | WebService static String getContactSalutationsPicklist() {
11 | return AngularAppController.getValueToLabelMapOfPicklistJson(Schema.Contact.Salutation);
12 | }
13 |
14 | @RemoteAction
15 | WebService static List executeQuery(String query) {
16 | return Database.query(query);
17 | }
18 |
19 | private static String getValueToLabelMapOfPicklistJson(Schema.SObjectField picklistField) {
20 | Map options = new Map();
21 |
22 | // Get the picklist values for the field.
23 | for (Schema.PicklistEntry entry : picklistField.getDescribe().getPicklistValues()) {
24 | // Only include ones that are active
25 | if (entry.isActive() == true) {
26 | options.put(entry.getLabel(), entry.getValue());
27 | }
28 | }
29 |
30 | // Create a list of labels from the key set.
31 | List labels = new List(options.keySet());
32 |
33 | Map reversedOptions = new Map();
34 |
35 | // We need to reverse the map since the page would normally get it in the reverse order.
36 | for (Integer i = labels.size() - 1; i >= 0; i--) {
37 | reversedOptions.put(options.get(labels[i]), labels[i]);
38 | }
39 |
40 | return JSON.serialize(reversedOptions);
41 | }
42 |
43 | }
--------------------------------------------------------------------------------
/src/app/services/logger.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 |
3 | export enum LOG_LEVEL {
4 | ALL = 0,
5 | DEBUG = 1,
6 | INFO = 2,
7 | WARN = 3,
8 | ERROR = 4,
9 | NONE = 5
10 | }
11 |
12 | @Injectable()
13 | export class LoggerService {
14 |
15 | public logLevel: LOG_LEVEL = LOG_LEVEL.ERROR;
16 |
17 | public group(message?: string, logLevel: LOG_LEVEL = LOG_LEVEL.ERROR) {
18 | if (this.logLevel <= logLevel) {
19 | window.console.group.apply(window.console, [message]);
20 | }
21 | }
22 |
23 | public groupEnd(logLevel: LOG_LEVEL = LOG_LEVEL.ERROR) {
24 | if (this.logLevel <= logLevel) {
25 | window.console.groupEnd.apply(window.console, arguments);
26 | }
27 | }
28 |
29 | public debug(...args: any[]) {
30 | if (this.logLevel <= 1) {
31 | window.console.debug.apply(window.console, args);
32 | }
33 | }
34 |
35 | public info(...args: any[]) {
36 | if (this.logLevel <= 2) {
37 | window.console.info.apply(window.console, args);
38 | }
39 | }
40 |
41 | public warn(...args: any[]) {
42 | if (this.logLevel <= 3) {
43 | window.console.warn.apply(window.console, args);
44 | }
45 | }
46 |
47 | public error(...args: any[]) {
48 | if (this.logLevel <= 4) {
49 | window.console.error.apply(window.console, args);
50 | }
51 | }
52 |
53 | public log(...args: any[]) {
54 | window.console.info.apply(window.console, args);
55 | }
56 |
57 | }
--------------------------------------------------------------------------------
/gulp/scripts.js:
--------------------------------------------------------------------------------
1 | module.exports = function(gulp, config, server) {
2 | 'use strict';
3 |
4 | const sourcemaps = require('gulp-sourcemaps'),
5 | uglify = require('gulp-uglify'),
6 | ts = require('gulp-typescript');
7 |
8 | let tsProject = ts.createProject('tsconfig.json', {
9 | rootDir: 'src',
10 | typescript: require('typescript')
11 | });
12 |
13 | gulp.task('typescript:dev', () => {
14 | let tsResult = tsProject.src('src/**/*.ts')
15 | .pipe(sourcemaps.init())
16 | .pipe(ts(tsProject));
17 |
18 | return tsResult.js
19 | .pipe(sourcemaps.write())
20 | .pipe(gulp.dest('build'));
21 | });
22 |
23 | gulp.task('typescript:prod', () => {
24 | let tsResult = tsProject.src('src/**/*.ts')
25 | .pipe(sourcemaps.init())
26 | .pipe(ts(tsProject));
27 |
28 | return tsResult.js
29 | // .pipe(uglify({
30 | // mangle: false
31 | // }))
32 | .pipe(sourcemaps.write())
33 | .pipe(gulp.dest('build'));
34 | });
35 |
36 | gulp.task('javascript:dev', () => {
37 | return gulp.src(['src/**/*.js'])
38 | .pipe(gulp.dest('build'));
39 | });
40 |
41 | gulp.task('javascript:prod', () => {
42 | return gulp.src(['src/**/*.js'])
43 | .pipe(uglify({
44 | mangle: false
45 | }))
46 | .pipe(gulp.dest('build'));
47 | });
48 |
49 | gulp.task('scripts:dev', gulp.parallel('typescript:dev', 'javascript:dev'));
50 | gulp.task('scripts:prod', gulp.parallel('typescript:prod', 'javascript:prod'));
51 |
52 | gulp.task('watch:scripts', () => {
53 | gulp.watch('src/**/*.ts', gulp.series('typescript:dev'));
54 | gulp.watch(['src/**/*.js', 'src/systemjs.config.js'], gulp.series('javascript:dev'));
55 | });
56 | }
57 |
--------------------------------------------------------------------------------
/src/app/app.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { BrowserModule } from '@angular/platform-browser';
3 | import { FormsModule } from '@angular/forms';
4 | import { HttpModule } from '@angular/http';
5 |
6 | import { routing, appRoutingProviders } from './app.routing';
7 |
8 | import { AppComponent, HomeComponent, ContactComponent, CreateContactComponent } from './components/index';
9 | import { SalesforceService, LoggerService, LOG_LEVEL } from './services/index';
10 | import { SalesforceResolver } from './resolves/index';
11 |
12 | import { ContentEditableModelDirective } from './directives/contentEditableModel.directive';
13 | import { GravatarDirective } from './directives/gravatar.directive';
14 |
15 | import { NewlineToBreakPipe, KeysPipe } from './pipes/index'
16 |
17 | @NgModule({
18 | imports: [
19 | BrowserModule,
20 | FormsModule,
21 | HttpModule,
22 | routing
23 | ],
24 | declarations: [
25 | AppComponent,
26 | HomeComponent,
27 | ContactComponent,
28 | CreateContactComponent,
29 |
30 | ContentEditableModelDirective,
31 | GravatarDirective,
32 | NewlineToBreakPipe,
33 | KeysPipe
34 | ],
35 | providers: [
36 | SalesforceService,
37 | LoggerService,
38 | SalesforceResolver,
39 | appRoutingProviders
40 | ],
41 | bootstrap: [AppComponent]
42 | })
43 | export class AppModule {
44 | constructor(private sfdc: SalesforceService, private log: LoggerService) {
45 | this.sfdc.controller = 'AngularAppController';
46 | this.log.logLevel = LOG_LEVEL.ALL;
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/app/resolves/salesforce.resolver.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import { Resolve, ActivatedRouteSnapshot } from '@angular/router';
3 | import { Observable, Observer } from 'rxjs/Rx';
4 | import { SalesforceService } from '../services/salesforce.service';
5 | let jsforce = require('jsforce');
6 |
7 | @Injectable()
8 | export class SalesforceResolver implements Resolve {
9 |
10 | constructor(private salesforceService: SalesforceService) {}
11 |
12 | resolve(route: ActivatedRouteSnapshot): Observable {
13 | let sf = (window)._sf
14 | return Observable.create((observer: Observer) => {
15 | if (this.salesforceService.conn) {
16 | observer.next(this.salesforceService);
17 | observer.complete();
18 | } else if (sf.api) {
19 | this.salesforceService.conn = new jsforce.Connection({
20 | sessionId: sf.api,
21 | serverUrl: `${window.location.protocol}//${window.location.hostname}`
22 | });
23 | observer.next(this.salesforceService);
24 | observer.complete();
25 | } else if (sf.auth) {
26 | this.salesforceService.authenticate(sf.auth.login_url, sf.auth.username, sf.auth.password, sf.auth.oauth2)
27 | .then((res) => {
28 | observer.next(this.salesforceService);
29 | observer.complete();
30 | }, (reason) => {
31 | observer.error(reason);
32 | observer.complete();
33 | });
34 | }
35 | });
36 | }
37 | }
--------------------------------------------------------------------------------
/src/index.page.html:
--------------------------------------------------------------------------------
1 | <% if (!local) { %>
2 |
3 | <% } %>
4 |
5 |
6 |
7 |
8 |
9 | Angular 2 Salesforce Test
10 |
11 | <% if (local) { %>
12 |
16 | <% } else { %>
17 |
22 | <% } %>
23 |
24 |
25 |
26 | Loading...
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
37 |
38 |
39 | <% if (!local) { %>
40 |
41 | <% } %>
42 |
--------------------------------------------------------------------------------
/src/app/components/home/home.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit,Input, trigger, state, style, transition, animate } from '@angular/core';
2 | import { SalesforceService, LoggerService, SOQL } from '../../services/index';
3 | import { IContact } from '../../shared/sobjects';
4 |
5 | interface ContactCard extends IContact {
6 | state: string
7 | }
8 |
9 | @Component({
10 | moduleId: module.id,
11 | selector: 'home',
12 | templateUrl: 'home.component.html',
13 | animations: [
14 | trigger('cardState', [
15 | state('hovering', style({
16 | transform: 'scale(1.05)'
17 | })),
18 | state('normal', style({
19 | transform: 'scale(1)'
20 | })),
21 | transition('normal => hovering', animate('200ms ease-in')),
22 | transition('hovering => normal', animate('200ms ease-out'))
23 | ])
24 | ]
25 | })
26 | export class HomeComponent implements OnInit {
27 |
28 | private contacts: ContactCard[] = [];
29 |
30 | constructor(private sfdc: SalesforceService, private log: LoggerService) {}
31 |
32 | ngOnInit() {
33 | let query = 'SELECT Id, Salutation, FirstName, LastName, Email FROM Contact';
34 | let s = new SOQL()
35 | .select('Id', 'Salutation', 'FirstName', 'LastName', 'PhotoUrl')
36 | .from('Contact');
37 | this.sfdc.execute('executeQuery', { query: s.soql })
38 | .then((res) => {
39 | this.contacts = res;
40 | this.contacts.map((c) => {
41 | c.state = 'normal';
42 | c.PhotoUrl = this.sfdc.instanceUrl + c.PhotoUrl;
43 | });
44 | }, (err) => {
45 | this.log.error(err);
46 | });
47 | }
48 |
49 | }
--------------------------------------------------------------------------------
/src/app/components/contact/create-contact.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit } from '@angular/core';
2 | import { NgControl } from '@angular/forms';
3 | import { Subscription } from 'rxjs/Rx'
4 | import { Router } from '@angular/router';
5 |
6 | import { IContact } from '../../shared/sobjects';
7 | import { SalesforceService, LoggerService } from '../../services/index';
8 |
9 | @Component({
10 | moduleId: module.id,
11 | selector: 'create-contact',
12 | templateUrl: 'create-contact.component.html'
13 | })
14 | export class CreateContactComponent implements OnInit {
15 |
16 | private contact: IContact = {};
17 | private salutations: { [s: string]: string };
18 | private saving: boolean = false;
19 | private loading: boolean = true;
20 | private error: string;
21 |
22 | constructor(private sfdc: SalesforceService, private log: LoggerService, private router: Router) {}
23 |
24 | private getContactSalutations() {
25 | let id = this.contact.Id;
26 | this.sfdc.execute('getContactSalutationsPicklist')
27 | .then((res) => {
28 | this.salutations = res[0];
29 | this.loading = false;
30 | });
31 | }
32 |
33 | private save() {
34 | if (!this.saving) {
35 | this.saving = true;
36 | let contact: IContact = JSON.parse(JSON.stringify(this.contact));
37 | contact.Birthdate = this.sfdc.convertDate(contact.Birthdate);
38 | this.sfdc.execute('upsertContact', { contact: contact })
39 | .then((res) => {
40 | this.saving = false;
41 | this.router.navigate(['/contact/view', res[0].Id]);
42 | }, (reason) => {
43 | this.saving = false;
44 | this.error = reason;
45 | this.log.error(reason);
46 | });
47 | }
48 | }
49 |
50 | ngOnInit(): void {
51 | this.getContactSalutations();
52 | }
53 | }
--------------------------------------------------------------------------------
/gulp/html.js:
--------------------------------------------------------------------------------
1 | module.exports = function(gulp, config, server) {
2 | 'use strict';
3 |
4 | const template = require('gulp-template'),
5 | rename = require('gulp-rename');
6 |
7 | let vfDevTemplate = {
8 | node_modules_directory: "/node_modules/",
9 | app_directory: "/",
10 | baseUrl: '/',
11 | local: true,
12 | controller: '',
13 | auth: JSON.stringify({
14 | username: config.deploy.username,
15 | password: config.deploy.password,
16 | login_url: config.deploy.login_url,
17 | api_version: config.deploy.api_version
18 | })
19 | };
20 | let vfProdTemplate = {
21 | node_modules_directory: `{!URLFOR($Resource.${config.resources.node_module_resource_name})}/`,
22 | app_directory: `{!URLFOR($Resource.${config.resources.app_resource_name})}/`,
23 | baseUrl: '/apex/' + config.visualforce.page,
24 | local: false,
25 | controller: config.visualforce.controller
26 | };
27 |
28 | gulp.task('html:dev', () => {
29 | return gulp.src(['src/**/*.html', 'src/' + config.visualforce.template])
30 | .pipe(gulp.dest('build'));
31 | });
32 |
33 | gulp.task('html:prod', () => {
34 | return gulp.src(['src/**/*.html', 'src/' + config.visualforce.template])
35 | .pipe(gulp.dest('build'));
36 | });
37 |
38 | gulp.task('visualforce:dev', () => {
39 | return gulp.src('src/' + config.visualforce.template)
40 | .pipe(rename((path) => {
41 | path.basename = 'index';
42 | path.extname = '.html'
43 | }))
44 | .pipe(template(vfDevTemplate))
45 | .pipe(gulp.dest('build'));
46 | });
47 |
48 | gulp.task('visualforce:prod', () => {
49 | return gulp.src(`src/${config.visualforce.template}`)
50 | .pipe(rename((path) => {
51 | path.basename = config.visualforce.page;
52 | path.extname = '.page';
53 | }))
54 | .pipe(template(vfProdTemplate))
55 | .pipe(gulp.dest('build'));
56 | });
57 |
58 | gulp.task('watch:html', () => {
59 | gulp.watch('src/**/!(*.vf).html', gulp.series('html:dev'));
60 | gulp.watch('src/' + config.visualforce.template, gulp.series('visualforce:dev'));
61 | });
62 | }
63 |
--------------------------------------------------------------------------------
/src/app/components/contact/create-contact.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
New Contact
4 |
5 |
41 |
--------------------------------------------------------------------------------
/src/systemjs.config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * System configuration for Angular 2 samples
3 | * Adjust as necessary for your application needs.
4 | */
5 | (function (global) {
6 |
7 | var node_modules_directory = window._sf.node_modules_dir || 'node_modules/';
8 | var app_directory = window._sf.app_dir || '';
9 |
10 | System.config({
11 | paths: {
12 | // paths serve as alias
13 | 'npm:': node_modules_directory
14 | },
15 | // map tells the System loader where to look for things
16 | map: {
17 | // our app is within the app folder
18 | app: app_directory,
19 |
20 | // angular bundles
21 | '@angular/core': 'npm:@angular/core/bundles/core.umd.js',
22 | '@angular/common': 'npm:@angular/common/bundles/common.umd.js',
23 | '@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js',
24 | '@angular/platform-browser': 'npm:@angular/platform-browser/bundles/platform-browser.umd.js',
25 | '@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js',
26 | '@angular/http': 'npm:@angular/http/bundles/http.umd.js',
27 | '@angular/router': 'npm:@angular/router/bundles/router.umd.js',
28 | '@angular/forms': 'npm:@angular/forms/bundles/forms.umd.js',
29 |
30 | // angular testing umd bundles
31 | '@angular/core/testing': 'npm:@angular/core/bundles/core-testing.umd.js',
32 | '@angular/common/testing': 'npm:@angular/common/bundles/common-testing.umd.js',
33 | '@angular/compiler/testing': 'npm:@angular/compiler/bundles/compiler-testing.umd.js',
34 | '@angular/platform-browser/testing': 'npm:@angular/platform-browser/bundles/platform-browser-testing.umd.js',
35 | '@angular/platform-browser-dynamic/testing': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic-testing.umd.js',
36 | '@angular/http/testing': 'npm:@angular/http/bundles/http-testing.umd.js',
37 | '@angular/router/testing': 'npm:@angular/router/bundles/router-testing.umd.js',
38 | '@angular/forms/testing': 'npm:@angular/forms/bundles/forms-testing.umd.js',
39 |
40 | // other libraries
41 | 'rxjs': 'npm:rxjs',
42 | 'jsforce': 'npm:jsforce/build/jsforce.min.js',
43 | 'moment': 'npm:moment/min/moment.min.js',
44 | 'crypto-js': 'npm:crypto-js/crypto-js.js',
45 | 'lodash': 'npm:lodash/lodash.min.js'
46 | },
47 | // packages tells the System loader how to load when no filename and/or no extension
48 | packages: {
49 | app: {
50 | main: './app/main.js',
51 | defaultExtension: 'js'
52 | },
53 | rxjs: {
54 | defaultExtension: 'js'
55 | }
56 | }
57 | });
58 | })(this);
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular2-salesforce-boilerplate",
3 | "version": "0.7.0",
4 | "description": "Boilerplate for developing Angular 2 applications on salesforce that work locally",
5 | "main": "index.js",
6 | "scripts": {
7 | "serve": "gulp",
8 | "watch": "gulp watch:all",
9 | "test": "protractor",
10 | "postinstall": "typings install"
11 | },
12 | "author": "Chris Watson",
13 | "license": "MIT",
14 | "dependencies": {
15 | "@angular/common": "^2.2.3",
16 | "@angular/compiler": "^2.2.3",
17 | "@angular/core": "^2.2.3",
18 | "@angular/forms": "^2.2.3",
19 | "@angular/http": "^2.2.3",
20 | "@angular/platform-browser": "^2.2.3",
21 | "@angular/platform-browser-dynamic": "^2.2.3",
22 | "@angular/router": "^3.2.3",
23 | "@types/node": "^6.0.51",
24 | "angular-in-memory-web-api": "~0.1.15",
25 | "core-js": "^2.4.1",
26 | "crypto-js": "^3.1.8",
27 | "jade": "^1.11.0",
28 | "jade-loader": "^0.8.0",
29 | "jsforce": "github:idev0urer/jsforce",
30 | "jsforce-ajax-proxy": "^1.0.0",
31 | "moment": "^2.17.0",
32 | "reflect-metadata": "^0.1.8",
33 | "rxjs": "^5.0.0-beta.12",
34 | "systemjs": "0.19.27",
35 | "zone.js": "^0.6.26"
36 | },
37 | "devDependencies": {
38 | "@types/core-js": "^0.9.32",
39 | "@types/crypto-js": "^3.1.31",
40 | "@types/es6-promise": "0.0.31",
41 | "@types/jasmine": "^2.5.36",
42 | "@types/lodash": "^4.14.34",
43 | "@types/moment": "^2.13.0",
44 | "@types/node": "^6.0.51",
45 | "@types/selenium-webdriver": "^2.53.33",
46 | "canonical-path": "0.0.2",
47 | "concurrently": "^3.1.0",
48 | "del": "^2.2.2",
49 | "express": "^4.14.0",
50 | "gulp": "github:gulpjs/gulp#4.0",
51 | "gulp-archiver": "^1.0.0",
52 | "gulp-file": "^0.3.0",
53 | "gulp-jsforce-deploy": "^1.1.2",
54 | "gulp-live-server": "0.0.30",
55 | "gulp-rename": "^1.2.2",
56 | "gulp-sass": "^2.3.2",
57 | "gulp-sourcemaps": "^1.6.0",
58 | "gulp-template": "^4.0.0",
59 | "gulp-typescript": "^2.14.1",
60 | "gulp-uglify": "^2.0.0",
61 | "http-server": "^0.9.0",
62 | "jasmine-core": "~2.4.1",
63 | "js-yaml": "^3.6.1",
64 | "karma": "^1.3.0",
65 | "karma-chrome-launcher": "^2.0.0",
66 | "karma-cli": "^1.0.1",
67 | "karma-htmlfile-reporter": "^0.3.4",
68 | "karma-jasmine": "^1.0.2",
69 | "karma-jasmine-html-reporter": "^0.2.2",
70 | "lite-server": "^2.2.2",
71 | "lodash": "^4.17.2",
72 | "merge-stream": "^1.0.0",
73 | "module": "^1.2.5",
74 | "protractor": "4.0.9",
75 | "pxml": "^0.2.3",
76 | "require": "^2.4.20",
77 | "rimraf": "^2.5.4",
78 | "serve-static": "^1.11.1",
79 | "tslint": "^3.15.1",
80 | "typescript": "^2.0.10",
81 | "typings": "^1.3.2",
82 | "webdriver-manager": "10.2.5"
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/app/shared/sobjects.ts:
--------------------------------------------------------------------------------
1 | export type Id = string;
2 |
3 | export interface ISObject {
4 |
5 | Id?: Id;
6 | CreatedBy?: Id;
7 | LastModifiedBy?: Id;
8 | Name?: string;
9 | fieldsToNull?: string[];
10 |
11 | }
12 |
13 | export interface IRetrieveResult {
14 |
15 | }
16 |
17 | export interface IContact extends ISObject {
18 |
19 | Account?: Id;
20 | AssistantName?: string;
21 | AssistantPhone?: number;
22 | Birthdate?: number|string;
23 | CleanStatus?: string;
24 | Owner?: Id;
25 | Jigsaw?: string;
26 | Department?: string;
27 | Description?: string;
28 | DoNotCall?: boolean;
29 | Email?: string;
30 | HasOptedOutOfEmail?: boolean;
31 | Fax?: number;
32 | HasOptedOutOfFax?: boolean;
33 | HomePhone?: number;
34 | LastCURequestDate?: number|string;
35 | LastCUUpdateDate?: number|string;
36 | LeadSource?: string;
37 | MailingAddress?: string;
38 | MobilePhone?: number;
39 | Name?: string;
40 | Salutation?: string;
41 | FirstName?: string;
42 | LastName?: string;
43 | OtherAddress?: string;
44 | OtherPhone?: number;
45 | Phone?: number;
46 | PhotoUrl?: string;
47 | ReportsTo?: Id;
48 | Title?: string;
49 |
50 | }
51 |
52 | export interface IUser extends ISObject {
53 |
54 | AboutMe?: string;
55 | IsActive?: boolean;
56 | Address?: string;
57 | ReceivesAdminInfoEmails?: boolean;
58 | Alias?: string;
59 | ForecastEnabled?: boolean;
60 | CallCenter?: any; // CallCenter
61 | MobilePhone?: number;
62 | DigestFrequency?: string;
63 | CompanyName?: string;
64 | Contact?: IContact;
65 | JigsawImportLimitOverride?: number;
66 | DefaultGroupNotificationFrequency?: string;
67 | DelegatedApprover?: Id; // User | Group
68 | Department?: string;
69 | Division?: string;
70 | Email?: string;
71 | EmailEncodingKey?: string;
72 | SenderEmail?: string;
73 | SenderName?: string;
74 | Signature?: string;
75 | EmployeeNumber?: string;
76 | EndDay?: string;
77 | Extension?: number;
78 | Fax?: number;
79 | LoginLimit?: number;
80 | Workspace?: any; // Workspace
81 | ReceivesInfoEmails?: boolean;
82 | UserSubtype?: string;
83 | IsSystemControlled?: boolean;
84 | LanguageLocaleKey?: string;
85 | LocaleSidKey?: string;
86 | Manager?: any; // Hierchy
87 | CommunityNickname?: string;
88 | Phone?: number;
89 | Profile?: any; // Profilr
90 | UserRole?: any; // Role
91 | FederationIdentifier?: string;
92 | StartDay?: string;
93 | StayInTouchNote?: string;
94 | StayInTouchSignature?: string;
95 | StayInTouchSubject?: string;
96 | TimeZoneSidKey?: string;
97 | Title?: string;
98 | Username?: string;
99 |
100 | }
101 |
102 | export interface IAccount extends ISObject {
103 |
104 | }
--------------------------------------------------------------------------------
/src/app/components/contact/contact.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Contact Detail
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
![]()
17 |
18 |
19 |
20 |
21 |
22 |
Salutation:
23 |
{{contact.Salutation}}
24 |
27 |
28 |
29 |
30 |
First Name:
31 |
{{contact.FirstName}}
32 |
33 |
34 |
35 |
Last Name:
36 |
{{contact.LastName}}
37 |
38 |
39 |
40 |
Email:
41 |
{{contact.Email}}
42 |
43 |
44 |
45 |
Title:
46 |
{{contact.Title}}
47 |
48 |
49 |
50 |
Birthdate:
51 |
{{contact.Birthdate | date:'MM/dd/yyyy'}}
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/src/app/components/contact/contact.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit } from '@angular/core';
2 | import { ActivatedRoute } from '@angular/router';
3 | import { Subscription } from 'rxjs/Rx'
4 |
5 | import { IContact } from '../../shared/sobjects';
6 | import { SalesforceService, SOQL, LoggerService } from '../../services/index';
7 |
8 | import * as _ from 'lodash';
9 |
10 | @Component({
11 | moduleId: module.id,
12 | selector: 'contact',
13 | templateUrl: 'contact.component.html',
14 | styleUrls: ['contact.component.css']
15 | })
16 | export class ContactComponent implements OnInit {
17 |
18 | private contact: IContact;
19 | private oldContact: IContact;
20 | private salutations: { [s: string]: string };
21 | private editLock: boolean = true;
22 | private editing: boolean = false;
23 | private saving: boolean = false;
24 |
25 | constructor(private sfdc: SalesforceService, private log: LoggerService, private route: ActivatedRoute) {}
26 |
27 | private startEdit() {
28 | this.editing = true;
29 | this.oldContact = JSON.parse(JSON.stringify(this.contact));
30 | }
31 |
32 | private cancelEdit() {
33 | if (this.oldContact) {
34 | this.contact = JSON.parse(JSON.stringify(this.oldContact));
35 | }
36 | this.editing = false;
37 | }
38 |
39 | private getContact() {
40 | this.route.params
41 | .map(params => params['id'])
42 | .subscribe((id) => {
43 | let s = new SOQL()
44 | .select('Id', 'Salutation', 'FirstName', 'LastName', 'Title', 'Birthdate', 'Email')
45 | .from('Contact')
46 | .where(`Id = '${id}'`);
47 | this.sfdc.execute('executeQuery', { query: s.soql })
48 | .then((res) => {
49 | this.contact = res[0];
50 | this.contact.PhotoUrl = this.sfdc.instanceUrl + this.contact.PhotoUrl;
51 | this.editLock = false;
52 | return this.getContactSalutations();
53 | });
54 | });
55 | }
56 |
57 | private getContactSalutations() {
58 | let id = this.contact.Id;
59 | this.sfdc.execute('getContactSalutationsPicklist', {})
60 | .then((res) => {
61 | this.salutations = res[0];
62 | });
63 | }
64 |
65 | private saveContact() {
66 | if (!this.saving) {
67 | this.saving = true;
68 | let contact: IContact = JSON.parse(JSON.stringify(this.contact));
69 | contact.Birthdate = this.sfdc.convertDate(contact.Birthdate);
70 | this.sfdc.execute('upsertContact', { contact: contact })
71 | .then((res) => {
72 | this.saving = false;
73 | this.editing = false;
74 | }, (reason) => {
75 | this.saving = false;
76 | this.log.error(reason);
77 | });
78 | }
79 | }
80 |
81 | ngOnInit() {
82 | this.getContact();
83 | }
84 | }
--------------------------------------------------------------------------------
/gulp/deploy.js:
--------------------------------------------------------------------------------
1 | module.exports = function(gulp, config) {
2 | 'use strict';
3 |
4 | const archiver = require('gulp-archiver'),
5 | rename = require('gulp-rename'),
6 | del = require('del'),
7 | pxml = require('pxml').PackageXML,
8 | file = require('gulp-file'),
9 | merge = require('merge-stream'),
10 | forceDeploy = require('gulp-jsforce-deploy'),
11 | uglify = require('gulp-uglify');
12 |
13 | let pageMetaXml = `
14 |
15 | 36.0
16 |
17 | `;
18 |
19 | let resourceMetaXml = `
20 |
21 | Public
22 | application/octet-stream
23 | `;
24 |
25 | gulp.task('clean-tmp', () => {
26 | return del(['.tmp']);
27 | });
28 |
29 | gulp.task('clean-build', () => {
30 | return del(['build']);
31 | });
32 |
33 | gulp.task('clean-resources', () => {
34 | return del(['.tmp/static_resources']);
35 | });
36 |
37 | gulp.task('init-deploy', gulp.series(
38 | 'clean-tmp',
39 | 'clean-build',
40 | gulp.parallel('html:prod', 'visualforce:prod', 'scripts:prod', 'styles:prod')
41 | ));
42 |
43 | gulp.task('tempgen:visualforce', () => {
44 | return gulp.src(`build/${config.visualforce.page}.page`)
45 | .pipe(gulp.dest('.tmp/pages'));
46 | });
47 |
48 | gulp.task('tempgen:node_modules', () => {
49 | return gulp.src([
50 | 'node_modules/@angular/**/bundles/*.umd.js',
51 | 'node_modules/rxjs/**/*.js',
52 | 'node_modules/jsforce/build/jsforce.min.js',
53 | 'node_modules/core-js/client/shim.min.js',
54 | 'node_modules/zone.js/dist/zone.js',
55 | 'node_modules/reflect-metadata/Reflect.js',
56 | 'node_modules/systemjs/dist/system.src.js',
57 | 'node_modules/moment/min/moment.min.js',
58 | 'node_modules/crypto-js/crypto-js.js',
59 | 'node_modules/lodash/lodash.min.js'
60 | ], { base: 'node_modules' })
61 | .pipe(gulp.dest(`.tmp/static_resources/${config.resources.node_module_resource_name}`));
62 | });
63 |
64 | gulp.task('tempgen:app', () => {
65 | return gulp.src(['build/**/*', `!build/${config.visualforce.template}`])
66 | .pipe(gulp.dest(`.tmp/static_resources/${config.resources.app_resource_name}`));
67 | });
68 |
69 | gulp.task('tempgen:salesforce', () => {
70 | return gulp.src(['src/salesforce/**/*'])
71 | .pipe(gulp.dest('.tmp/'));
72 | });
73 |
74 | gulp.task('package:node_modules', () => {
75 | return gulp.src(`.tmp/static_resources/${config.resources.node_module_resource_name}/**/*`, {
76 | base: `.tmp/static_resources/${config.resources.node_module_resource_name}`
77 | })
78 | .pipe(archiver(`${config.resources.node_module_resource_name}.zip`))
79 | .pipe(rename({
80 | extname: '.resource'
81 | }))
82 | .pipe(gulp.dest('.tmp/staticresources'));
83 | });
84 |
85 | gulp.task('package:app', () => {
86 | return gulp.src(`.tmp/static_resources/${config.resources.app_resource_name}/**/*`, {
87 | base: `.tmp/static_resources/${config.resources.app_resource_name}`
88 | })
89 | .pipe(archiver(`${config.resources.app_resource_name}.zip`))
90 | .pipe(rename({
91 | extname: '.resource'
92 | }))
93 | .pipe(gulp.dest('.tmp/staticresources'));
94 | });
95 |
96 | gulp.task('tempgen:pxml', () => {
97 | return file('package.xml', pxml.from_dir('.tmp').generate().to_string(), { src: true })
98 | .pipe(gulp.dest('.tmp'));
99 | });
100 |
101 | gulp.task('tempgen:meta-xml', () => {
102 | let node_modules = file(`${config.resources.node_module_resource_name}.resource-meta.xml`,
103 | resourceMetaXml, { src: true })
104 | .pipe(gulp.dest('.tmp/staticresources'));
105 |
106 | let app = file(`${config.resources.app_resource_name}.resource-meta.xml`,
107 | resourceMetaXml, { src: true })
108 | .pipe(gulp.dest('.tmp/staticresources'));
109 |
110 | let page = file(`${config.visualforce.page}.page-meta.xml`,
111 | pageMetaXml.replace("{0}", config.visualforce.page), { src: true })
112 | .pipe(gulp.dest('.tmp/pages'));
113 |
114 | return merge(node_modules, app, page);
115 | });
116 |
117 | gulp.task('package-resources', gulp.parallel('package:app', 'package:node_modules'));
118 |
119 | gulp.task('tempgen', gulp.series(
120 | 'init-deploy',
121 | gulp.parallel('tempgen:node_modules', 'tempgen:app', 'tempgen:visualforce'),
122 | 'package-resources',
123 | 'clean-resources',
124 | 'tempgen:salesforce',
125 | 'tempgen:pxml',
126 | 'tempgen:meta-xml',
127 | 'clean-build'
128 | ));
129 |
130 | gulp.task('deploy:jsforce', () => {
131 | return gulp.src('.tmp/**/*', { base: '.' })
132 | .pipe(archiver('pkg.zip'))
133 | .pipe(forceDeploy({
134 | username: config.deploy.username,
135 | password: config.deploy.password,
136 | loginUrl: config.deploy.login_url,
137 | version: config.deploy.api_version,
138 | checkOnly: process.env.CHECK_ONLY,
139 | pollTimeout: config.deploy.timeout,
140 | pollInterval: config.deploy.poll_interval
141 | }));
142 | });
143 |
144 |
145 | gulp.task('deploy', gulp.series('tempgen', 'deploy:jsforce'));
146 | gulp.task('deploy:classes', gulp.series('tempgen:salesforce', 'tempgen:pxml', 'deploy:jsforce'));
147 |
148 | }
149 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # The Easier Way to Combine Salesforce and Angular 2
2 |
3 | **Notice:** I am putting development for this on hold for the time being as I just started a new job and have projects that need my attention. If anyone is interested in being added as a collaborator please let me know.
4 |
5 | This boilerplate, which is still largely unifinished, combines the powers of [Gulp](http://gulpjs.com/), [JSForce](http://jsforce.github.io), [Angular 2](http://angular.io), and [Salesforce](https://salesforce.com) and allows you to develop and test your Salesforce applications completely locally. Deploying is as easy as running `gulp` or you can be more specific and just `gulp deploy:classes`.
6 |
7 | ### Getting set up
8 |
9 | Setting up is super easy. First clone or fork this repository to your local machine with `git clone https://github.com/iDev0urer/salesforce-angular2-boilerplate.git` or, if you forked the repo, do `git clone YOUR-REPO-URL`. Then go to the salesforce-angular2-boilerplate directory with `cd salesforce-angular2-boilerplate`.
10 |
11 | You will need [NodeJs](http://nodejs.org) in order to work with this so make sure you have that installed.
12 |
13 | Once you're in the salesforce-angular2-boilerplate directory run `npm install`. This project also uses **gulp 4**, so install that:
14 |
15 | ```
16 | npm rm -g gulp
17 | npm install -g gulp-cli
18 | ```
19 |
20 | If everything worked `gulp -v` should give you a version number over 4.
21 |
22 | You will also need to copy the `config.sample.js` file to `config.js` and fill out the pertenent information. It should look like this:
23 |
24 | ```javascript
25 | module.exports = {
26 | deploy: {
27 | username: 'user.name@yourcompany.com',
28 | password: 'YourPasswordAndPossiblySecurityToken',
29 | login_url: 'https://login.salesforce.com',
30 | api_version: 36.0,
31 | timeout: 120000,
32 | poll_interval: 5000,
33 | },
34 |
35 | visualforce: {
36 | template: 'index.page.html',
37 | page: 'AngularApp',
38 | controller: 'AngularAppController'
39 | },
40 |
41 | resources: {
42 | app_resource_name: 'AngularApp',
43 | node_module_resource_name: 'NodeModules',
44 | },
45 |
46 | options: {
47 |
48 | }
49 | }
50 | ```
51 |
52 | ### Running the example
53 |
54 | This boilerplate comes with a working example of a **contact management application**. To get it running just run the `gulp` command while in the salesforce-angular2-boilerplate directory. It will open a local server at [http://localhost:8080](http://localhost:8080) where you should be able to view the working application. When you're ready to deploy the application and test it in Salesforce just run `gulp deploy` and wait for the application to finish deploying.
55 |
56 | ### File Tree
57 |
58 | The way I have structured this project bears describing; whether you are new to Salesforce, Angular 2, both, or just need some explianation.
59 |
60 | #### gulp
61 |
62 | The gulp directory contains most of the gulp tasks separated into different files.
63 |
64 | + deploy.js - Contains tasks specific to deployment such as the task that creates the package.xml and the actual jsforce-deploy task.
65 | - **Tasks**
66 | - clean-tmp
67 | - clean-build
68 | - clean-resources
69 | - init-deploy
70 | - tempgen:visualforce
71 | - tempgen:node_modules
72 | - tempgen:app
73 | - tempgen:salesforce
74 | - tempgen:pxml
75 | - tempgen:meta-xml
76 | - package:node_modules
77 | - package:app
78 | - package-resources
79 | - tempgen
80 | - deploy:jsforce
81 | - **deploy**
82 | - **deploy:classes**
83 | + html.js - Contains the tasks that add template values to the html file and can also turn it into a visualforce page
84 | - **Tasks**
85 | - html:dev
86 | - html:prod
87 | - visualforce:dev
88 | - visualforce:prod
89 | - **watch:html**
90 | + scripts.js - Contains tasks that compile Typescript and move javascript files to the build directory
91 | - **Tasks**
92 | - typescript:dev
93 | - typescript:prod
94 | - javascript:dev
95 | - javascript:prod
96 | - **scripts:dev**
97 | - **scripts:prod**
98 | - **watch:scripts**
99 | + styles.js - Contains tasks that compile SASS and move css files to the build directory
100 | - **Tasks**
101 | - sass:dev
102 | - sass:prod
103 | - css:dev
104 | - css:prod
105 | - **styles:dev**
106 | - **styles:prod**
107 | - **watch:styles**
108 |
109 | The main gulp tasks are located in `gulpfile.js` in the root directory. They are:
110 |
111 | + serve - starts the local development server
112 | + watch:all - Watches scripts, styles, and html and compiles on change
113 | + default - starts the server and watches files
114 |
115 | #### src
116 |
117 | The `src` directory contains all of the Source files; Typescript, javascript, sass, html/visualforce, and salesforce specific such as APEX classes.
118 |
119 | ##### app
120 |
121 | The app directory contains all of the Angular 2 files. These are separated into categories such as `components`, `directives`, `pipes`, `resolves`, `services`, and `shared`.
122 |
123 | ##### salesforce
124 |
125 | The salesforce directory is packaged up and deployed with the resource. You can add any Salesforce files you want here such as APEX Classes.
126 |
127 | ##### styles
128 |
129 | Fairly self explanitory the styles directory contains global styles for the app.
130 |
131 |
132 | Thank you @tylerzika for the suggestion to add this section!
133 |
134 | ### Contributing
135 |
136 | If you find something wrong or come up with a better way to do things please fork and pull request. I check Github several times daily and love seeing that little notification bubble.
137 |
138 | ### Known Issues
139 |
140 | So far things seem to be working well for the most part. Some things that I have noticed are:
141 |
142 | + Visualforce Remoting and WebServices use different date formats. I have tried to compensate for those differences in the Salesforce service with the `parseSoapResult` and `convertDate` methods, but I may have missed an edge case.
143 |
144 | ### License
145 |
146 | The MIT License
147 |
148 | Copyright (c) 2010-2016 Chris Watson
149 |
150 | Permission is hereby granted, free of charge, to any person obtaining a copy
151 | of this software and associated documentation files (the "Software"), to deal
152 | in the Software without restriction, including without limitation the rights
153 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
154 | copies of the Software, and to permit persons to whom the Software is
155 | furnished to do so, subject to the following conditions:
156 |
157 | The above copyright notice and this permission notice shall be included in
158 | all copies or substantial portions of the Software.
159 |
160 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
161 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
162 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
163 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
164 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
165 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
166 | THE SOFTWARE.
167 |
168 | ### Changelog
169 |
170 | ##### [0.7.0] - 2016-10-28
171 | - Updated README to describe directory structure
172 | - Removed unnecessary typings
173 | - Modified `salesforce.service.ts` and added `self` variable as reference to `this`
174 | - Merged pull requests #5 and #6
175 |
176 | ##### [0.6.1] - 2016-09-01
177 | - Added Gravatar directive to get use pictures rather than pulling them from Salesforce
178 |
179 | ##### [0.6.0] - 2016-09-01
180 | - Updated Angular app to version 2.0.0-rc.6. See the [Angular 2 Changelog](https://github.com/angular/angular/blob/master/CHANGELOG.md) for breaking changes.
181 |
182 | ##### [0.5.0] - 2016-09-01
183 | - Restructured config files. Now using `config.js` rather than `yaml`
184 | - Took the `pxml.js` file which is used for `package.xml` generation and refactored it out into it's [own module](http://npmjs.org/package/pxml)
185 | - Refactored gulpfiles to fit with new config
186 |
187 | ##### [0.4.3] - 2016-08-30
188 | - Fixed bug with `ngZone` causing `execute` method to fire twice when within `ngOnInit`
189 |
190 | ##### [0.4.2] - 2016-08-30
191 | - Removed https requirement from local server. Use [http://localhost:8080](http://localhost:8080) now
192 | - Added components to `app.module.ts` declarations
193 | - Made changes to forked version of jsforce and updated `salesforce.service.ts` to reflect those changes.
194 |
195 | ##### [0.3.0] - 2016-08-27
196 | - Added SOQL class to build SOQL queries
197 |
198 | ##### [0.2.0] - 2016-08-27
199 | - Refactored some methods in the Salesforce service
200 | - Finished the ContactComponent (for now)
201 | - Added a CreateContactComponent
202 | - Fixed some bugs
203 |
204 | ##### [0.1.0] - 2016-08-23
205 | - Added this repo to github
206 |
--------------------------------------------------------------------------------
/src/app/services/salesforce.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, NgZone } from '@angular/core';
2 | import { Observable, Scheduler } from 'rxjs/Rx';
3 |
4 | import { LoggerService, LOG_LEVEL } from './index';
5 |
6 | import { ISObject } from '../shared/sobjects';
7 | import 'rxjs/add/operator/toPromise';
8 |
9 | import * as _ from 'lodash';
10 |
11 | let jsforce = require('jsforce');
12 | import * as moment from 'moment';
13 |
14 | export enum API {
15 | REST,
16 | VFR
17 | }
18 |
19 | export interface RemotingOptions {
20 | buffer?: boolean,
21 | escape?: boolean,
22 | timeout?: number
23 | }
24 |
25 | @Injectable()
26 | export class SalesforceService {
27 |
28 | public conn: any;
29 | public useRest: boolean = (window).local || false;
30 | public apiVersion: string = '34.0'
31 | private restConnectionType: ['soap', 'oauth'];
32 | public oauth2: any;
33 | public controller: string;
34 |
35 | public beforeHook: (controller?: string, method?: string, params?: Object, api?: API) => boolean;
36 | public afterHook: (error?: string, result?: any) => void;
37 |
38 | get instanceUrl(): string {
39 | if (this.conn) {
40 | return this.conn.instanceUrl;
41 | } else {
42 | return window.location.origin;
43 | }
44 | }
45 |
46 | constructor(private _zone: NgZone, private log: LoggerService) {}
47 |
48 | public authenticate(login_url: string,
49 | username: string,
50 | password: string,
51 | oauth2?: any): Promise {
52 | if (!this.conn) {
53 | this.log.debug('Authenticating with jsforce.');
54 | this.conn = new jsforce.Connection({
55 | loginUrl: login_url,
56 | version: this.apiVersion,
57 | proxyUrl: (window).local ? '/proxy/' : undefined,
58 | oauth2: oauth2
59 | });
60 |
61 | return this.conn.login(username, password);
62 | } else {
63 | this.log.warn('Already authenticated. No need to reauth.');
64 | return new Promise((resolve, reject) => {
65 | resolve(this.conn.userInfo);
66 | });
67 | }
68 | }
69 |
70 | /**
71 | * @param {string} controller - The APEX controller to use
72 | * @param {string} method - The method to execute on the controller. To use
73 | * both REST and Visualforce Remoting the methods
74 | * must be tagged with both `@RemoteAction` and `WebService`
75 | * @param {Object} params - Parameters to pass to the APEX method as an object with
76 | * the format `{ parameter_name: value }`
77 | * @param {RemotingOptions} vfrOptions - An object containing options to pass to the Visualforce
78 | * remoting call.
79 | * @return {Promise} Returns a promise with the result or rejects with the
80 | * remoting exception.
81 | */
82 | public execute(method: string, params?: Object, vfrOptions?: RemotingOptions): Promise {
83 | this.log.group('Executing method: ' + method, LOG_LEVEL.DEBUG);
84 | this.log.debug('Params:',params);
85 |
86 | let controller = this.controller;
87 | let p: Promise = new Promise((resolve, reject) => {
88 | if (this.useRest) {
89 | this.log.debug('Using REST API');
90 |
91 | let beforeHookResult = this.runBeforeHook(controller, method, params, API.REST);
92 |
93 | if (beforeHookResult) {
94 |
95 | this._zone.runOutsideAngular(() => {
96 | this.execute_rest(controller, method, params)
97 | .then((res) => {
98 | this.log.debug('Result: ', res);
99 | resolve(res);
100 | this.runAfterHook(null, res);
101 | }, (reason) => {
102 | this.log.error(reason);
103 | reject(reason);
104 | this.runAfterHook(reason, null);
105 | })
106 | .then(() => {
107 | this._zone.run(() => {});
108 | });
109 | });
110 |
111 | } else {
112 | let reason = 'Before hook failed';
113 | reject(reason);
114 | this.runAfterHook(reason, null);
115 | }
116 |
117 | } else {
118 | this.log.debug('Using Visualforce Remoting');
119 |
120 | let beforeHookResult = this.runBeforeHook(controller, method, params, API.VFR);
121 |
122 | if (beforeHookResult) {
123 | let tmp = [];
124 | for (let i in params) {
125 | tmp.push(params[i]);
126 | }
127 |
128 | this._zone.runOutsideAngular(() => {
129 | this.execute_vfr(method, tmp, vfrOptions)
130 | .then((res) => {
131 | this.log.debug('Result: ', res);
132 | resolve(res);
133 | this.runAfterHook(null, res);
134 | }, (reason) => {
135 | this.log.error(reason);
136 | reject(reason);
137 | this.runAfterHook(reason, null);
138 | })
139 | .then(() => {
140 | this._zone.run(() => {});
141 | });
142 | });
143 |
144 | } else {
145 | let reason = 'Before hook failed';
146 | reject(reason);
147 | this.runAfterHook(null, reason);
148 | }
149 | }
150 | });
151 |
152 | this.log.groupEnd(LOG_LEVEL.DEBUG);
153 | return p;
154 | }
155 |
156 | public execute_rest(pkg: string, method: string, params: Object): Promise {
157 |
158 | for (let key in params) {
159 | if (typeof(params[key]) === 'object' && !Array.isArray(params[key])) {
160 | params[key] = this.processSobject(params[key]);
161 | } else if (Array.isArray(params[key]) && params[key].length && typeof(params[key][0]) === 'object') {
162 | for (let i in params[key]) {
163 | params[key][i] = this.processSobject(params[key][i]);
164 | }
165 | }
166 | }
167 |
168 | return new Promise((resolve, reject) => {
169 | this.conn.execute(pkg, method, params, null)
170 | .then((res) => {
171 | res = this.parseResult(res);
172 | resolve(res);
173 | }, (reason) => {
174 | reject(reason);
175 | });
176 | });
177 | }
178 |
179 | private execute_vfr(method: string, params: Array, config?: RemotingOptions): Promise {
180 | // Set ctrl to the Visualforce Remoting controller
181 | let controller = this.controller;
182 | let ctrl: any = window[controller] || {};
183 | let self = this;
184 |
185 | config = config || { escape: false }
186 |
187 | // Make sure the controller has the method we're attempting to call
188 | if (ctrl.hasOwnProperty(method)) {
189 |
190 | let methodFunc = ctrl[method];
191 | let directCfg = methodFunc.directCfg;
192 |
193 | return new Promise((resolve, reject) => {
194 | // The wrong number of parameters were included
195 | if (params.length !== directCfg.method.len) {
196 | reject('Wrong number of parameters included');
197 | return;
198 | }
199 |
200 | let callback = function(res, err) {
201 | if (res) {
202 | res = self.parseResult(res);
203 | resolve(res);
204 | } else {
205 | reject(err.message);
206 | }
207 | }
208 |
209 | params.push(callback);
210 | params.push(config);
211 | ctrl[method].apply(null, params);
212 | });
213 | } else {
214 | return new Promise((resolve, reject) => {
215 | reject('The requested method does not exist on ' + controller);
216 | });
217 | }
218 | }
219 |
220 | public convertDate(date: string|number, dateTime: boolean = false): string|number {
221 | if (this.useRest) {
222 | if (date) {
223 | if (dateTime) {
224 | return moment(date).toISOString();
225 | } else {
226 | return moment(date).format('YYYY-MM-DD');
227 | }
228 | } else {
229 | return null;
230 | }
231 | } else if (date) {
232 | return moment(date).valueOf();
233 | } else {
234 | // Sets date to the epoch if falsy value for checking in apex controller
235 | /***** Example Controller Check *****
236 | if (w.End_Time__c.getTime() == 0) {
237 | w.End_Time__c = null;
238 | }
239 | ************************************/
240 | return 0;
241 | }
242 | }
243 |
244 | private processSobject(obj: ISObject) {
245 | let nullables: string[] = [];
246 | let tmp: ISObject = JSON.parse(JSON.stringify(obj));
247 | for (let key in tmp) {
248 | if (!tmp[key]) {
249 | delete tmp[key];
250 | nullables.push(key);
251 | }
252 | }
253 | tmp.fieldsToNull = nullables;
254 | return tmp;
255 | }
256 |
257 | private runBeforeHook(method: string, controller: string, params: Object, api: API): boolean {
258 | let beforeHookResult = true;
259 | if (this.beforeHook) {
260 | this.log.debug('Executing before hook');
261 | beforeHookResult = this.beforeHook.apply(this, [controller, method, params, API.REST]);
262 | this.log.debug('Before hook completed with status: ', beforeHookResult);
263 | }
264 | return beforeHookResult;
265 | }
266 |
267 | private runAfterHook(error: string, result: any): void {
268 | if (this.afterHook) {
269 | this.log.debug('Executing after hook');
270 | this.afterHook.apply(this, [error, result]);
271 | this.log.debug('After hook completed');
272 | }
273 | }
274 |
275 | private parseResult(result: any): Array