├── .bowerrc ├── .gitignore ├── elements ├── css │ ├── ProximaNovaSoft-Regular.otf │ ├── ratcheticons.css │ ├── responsive.css │ ├── responsive.shim.css │ └── styles.css ├── mobile-ui-elements.html ├── force-sobject-sync │ ├── force-sobject-sync.html │ └── force-sobject-sync.js ├── force-app │ ├── force-app.html │ └── force-app.js ├── force-sobject-layout │ ├── force-sobject-layout.html │ └── force-sobject-layout.js ├── force-sobject-store │ ├── force-sobject-store.html │ └── force-sobject-store.js ├── force-keyvalue-store │ └── force-keyvalue-store.html ├── force-sobject │ ├── force-sobject.html │ └── force-sobject.js ├── force-sobject-collection │ ├── force-sobject-collection.html │ └── force-sobject-collection.js ├── force-route │ └── force-route.html ├── force-ui-detail │ ├── force-ui-detail.html │ └── force-ui-detail.js ├── force-ui-search │ └── force-ui-search.html ├── force-ui-list │ └── force-ui-list.html ├── js │ └── jq-slide.js ├── force-ui-relatedlist │ ├── force-ui-relatedlist.html │ └── force-ui-relatedlist.js ├── force-signin │ ├── forcetk.ui.js │ └── force-signin.html └── force-ui-app │ └── force-ui-app.html ├── CODEOWNERS ├── package.json ├── bower.json ├── testsuite.html ├── LICENSE ├── proxy.js ├── Gruntfile.js ├── index.html ├── tests ├── force-sobject-store.test.js ├── force-sobject.test.js └── force-sobject-collection.test.js └── README.md /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "dependencies" 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | .DS_Store 3 | node_modules/ 4 | dependencies/ 5 | bower_components/ 6 | -------------------------------------------------------------------------------- /elements/css/ProximaNovaSoft-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forcedotcom/mobile-ui-elements/HEAD/elements/css/ProximaNovaSoft-Regular.otf -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Comment line immediately above ownership line is reserved for related gus information. Please be careful while editing. 2 | #ECCN:Open Source 3 | -------------------------------------------------------------------------------- /elements/mobile-ui-elements.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /elements/css/ratcheticons.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: Ratchicons; 3 | font-style: normal; 4 | font-weight: normal; 5 | src: url("../../dependencies/ratchet/dist/fonts/ratchicons.eot"); 6 | src: url("../../dependencies/ratchet/dist/fonts/ratchicons.eot?#iefix") format("embedded-opentype"), url("../../dependencies/ratchet/dist/fonts/ratchicons.woff") format("woff"), url("../../dependencies/ratchet/dist/fonts/ratchicons.ttf") format("truetype"), url("../../dependencies/ratchet/dist/fonts/ratchicons.svg#svgFontName") format("svg"); 7 | } -------------------------------------------------------------------------------- /elements/css/responsive.css: -------------------------------------------------------------------------------- 1 | .ui-responsive { overflow: hidden; } 2 | .ui-responsive .ui-block:first { clear: left; } 3 | .ui-block { 4 | width: 50%; 5 | margin: 0; 6 | padding: 0; 7 | border: 0; 8 | float: left; 9 | min-height: 1px; 10 | -webkit-box-sizing: border-box; 11 | -moz-box-sizing: border-box; 12 | -ms-box-sizing: border-box; 13 | box-sizing: border-box; 14 | } 15 | 16 | /* preset breakpoint to switch to stacked grid styles below 35em (560px) */ 17 | @media all and (max-width: 35em) { 18 | .ui-responsive .ui-block { 19 | width: 100%; 20 | float:none; 21 | } 22 | } -------------------------------------------------------------------------------- /elements/force-sobject-sync/force-sobject-sync.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "polymer2", 3 | "version": "0.0.0", 4 | "dependencies": { 5 | "express": "latest", 6 | "http-proxy": "latest" 7 | }, 8 | "devDependencies": { 9 | "cheerio": "~0.11.0", 10 | "grunt": "~0.4.1", 11 | "grunt-bower-install": "^1.6.0", 12 | "grunt-bower-task": "~0.3.2", 13 | "grunt-contrib-clean": "0.4.0", 14 | "grunt-contrib-uglify": "~0.2.7", 15 | "grunt-exec": "~0.4.2", 16 | "grunt-vulcanize": "0.1.2", 17 | "matchdep": "~0.1.2", 18 | "nopt": "~2.1.1", 19 | "polymer-shim-styles": "akhileshgupta/polymer-shim-styles", 20 | "time-grunt": "~0.1.0" 21 | }, 22 | "engines": { 23 | "node": ">=0.8.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /elements/force-app/force-app.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mobile-ui-elements", 3 | "version": "0.0.0", 4 | "dependencies": { 5 | "polymer": "Polymer/polymer#>=0.8.0", 6 | "iron-selector": "PolymerElements/iron-selector#>=0.8.0", 7 | "jquery": "latest", 8 | "underscore": "latest", 9 | "backbone": "latest", 10 | "mobilesdk-shared": "forcedotcom/SalesforceMobileSDK-Shared#unstable", 11 | "ratchet": "twbs/ratchet#>=2.0.2", 12 | "paper-elements": "PolymerElements/paper-elements#>=1.0.0", 13 | "layout": "Polymer/layout" 14 | }, 15 | "devDependencies": { 16 | "mocha": "~1.18.2", 17 | "should": "~3.3.1", 18 | "iron-icons": "PolymerElements/iron-icons#>=0.8.0" 19 | }, 20 | "resolutions": { 21 | "polymer": "^1.2.1" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /elements/force-sobject-layout/force-sobject-layout.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 21 | 22 | 23 | 24 | 25 | 28 | 29 | -------------------------------------------------------------------------------- /elements/force-sobject-store/force-sobject-store.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /testsuite.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Mocha Tests 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 35 | 36 | -------------------------------------------------------------------------------- /elements/css/responsive.shim.css: -------------------------------------------------------------------------------- 1 | polyfill-next-selector { 2 | content: ':host /deep/ .ui-responsive'; 3 | } 4 | 5 | ::content .ui-responsive, 6 | ::content>* /deep/ .ui-responsive, 7 | :host /deep/ .ui-responsive { 8 | overflow: hidden; 9 | } 10 | 11 | polyfill-next-selector { 12 | content: ':host /deep/ .ui-responsive .ui-block:first'; 13 | } 14 | 15 | ::content .ui-responsive .ui-block:first, 16 | ::content>* /deep/ .ui-responsive .ui-block:first, 17 | :host /deep/ .ui-responsive .ui-block:first { 18 | clear: left; 19 | } 20 | 21 | polyfill-next-selector { 22 | content: ':host /deep/ .ui-block'; 23 | } 24 | 25 | ::content .ui-block, 26 | ::content>* /deep/ .ui-block, 27 | :host /deep/ .ui-block { 28 | width: 50%; 29 | margin: 0; 30 | padding: 0; 31 | border: 0; 32 | float: left; 33 | min-height: 1px; 34 | -webkit-box-sizing: border-box; 35 | -moz-box-sizing: border-box; 36 | -ms-box-sizing: border-box; 37 | box-sizing: border-box; 38 | } 39 | 40 | /* preset breakpoint to switch to stacked grid styles below 35em (560px) */ 41 | 42 | @media all and (max-width: 35em) { 43 | polyfill-next-selector { 44 | content: ':host /deep/ .ui-responsive .ui-block'; 45 | } 46 | 47 | ::content .ui-responsive .ui-block, 48 | ::content>* /deep/ .ui-responsive .ui-block, 49 | :host /deep/ .ui-responsive .ui-block { 50 | width: 100%; 51 | float: none; 52 | } 53 | } -------------------------------------------------------------------------------- /elements/force-keyvalue-store/force-keyvalue-store.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013, salesforce.com, inc. 2 | // All rights reserved. 3 | // 4 | // Redistribution and use in source and binary forms, with or without modification, are permitted provided 5 | // that the following conditions are met: 6 | // 7 | // Redistributions of source code must retain the above copyright notice, this list of conditions and the 8 | // following disclaimer. 9 | // 10 | // Redistributions in binary form must reproduce the above copyright notice, this list of conditions and 11 | // the following disclaimer in the documentation and/or other materials provided with the distribution. 12 | // 13 | // Neither the name of salesforce.com, inc. nor the names of its contributors may be used to endorse or 14 | // promote products derived from this software without specific prior written permission. 15 | // 16 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED 17 | // WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 18 | // PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 19 | // ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 20 | // TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 21 | // HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 22 | // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 23 | // POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /elements/force-sobject/force-sobject.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 25 | 26 | 27 | 28 | 29 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /elements/force-sobject-collection/force-sobject-collection.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 23 | 24 | 25 | 26 | 27 | 28 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /elements/force-sobject-sync/force-sobject-sync.js: -------------------------------------------------------------------------------- 1 | (function(SFDC) { 2 | 3 | "use strict"; 4 | 5 | Polymer({ 6 | is: 'force-sobject-sync', 7 | properties: { 8 | sobject: String, 9 | query: String, 10 | fieldstoindex: { 11 | type: String, 12 | notify: true 13 | } 14 | }, 15 | observers: ["fetch(sobject, query)"], 16 | 17 | ready: function() { 18 | document.addEventListener('sync', this.syncEvent.bind(this)); 19 | }, 20 | fetch: function() { 21 | var store = this.$.sync_store; 22 | var that = this; 23 | if (SFDC.isOnline() && this.sobject && this.query) { 24 | $.when(store.cacheReady, SFDC.launcher) 25 | .then(function() { 26 | cordova.require('com.salesforce.plugin.smartsync').syncDown( 27 | {type:"soql", query:that.query}, 28 | store.cache.soupName, {}, 29 | function(result) { 30 | that.syncId = result._soupEntryId; 31 | // || result.syncId; 32 | } 33 | ); 34 | }); 35 | } 36 | }, 37 | syncEvent: function(e) { 38 | var syncId = e.detail._soupEntryId; 39 | // || e.detail.syncId; 40 | if (this.syncId >= 0 && syncId == this.syncId) { 41 | if (e.detail.status == 'DONE') this.fire('sync-complete', e.detail); 42 | else if (e.detail.status == 'RUNNING') { 43 | this.fire('sync-progress', e.detail); 44 | } 45 | } 46 | } 47 | }); 48 | 49 | })(window.SFDC); -------------------------------------------------------------------------------- /elements/force-route/force-route.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 16 | 57 | -------------------------------------------------------------------------------- /proxy.js: -------------------------------------------------------------------------------- 1 | var http = require('http'), 2 | httpProxy = require('http-proxy'), 3 | url = require('url'), 4 | express = require('express'); 5 | 6 | // Create a proxy server with custom application logic 7 | // 8 | var proxy = httpProxy.createProxyServer({ secure: true }); 9 | 10 | // 11 | // Create your custom server and just call `proxy.web()` to proxy 12 | // a web request to the target passed in the options 13 | // also you can use `proxy.ws()` to proxy a websockets request 14 | // 15 | var server = require('http').createServer(function(req, res) { 16 | // You can define here your custom logic to handle the request 17 | // and then proxy the request. 18 | var endpoint = req.headers['salesforceproxy-endpoint']; 19 | var target = 'http://localhost:8000'; 20 | 21 | if (endpoint && endpoint.length) { 22 | var auth = req.headers['x-authorization']; 23 | if (auth) req.headers['Authorization'] = auth; 24 | 25 | // Building the target host url from the endpoint information. 26 | var targetURL = url.parse(endpoint); 27 | target = targetURL.protocol + "//" + targetURL.host; 28 | 29 | // http-proxy module uses the url path to generate the path of new outgoing request 30 | req.url = endpoint; 31 | // Also set the host header to match the request host. HTTPS cert verification validates this for "secure: true" 32 | req.headers['host'] = targetURL.host; 33 | } 34 | 35 | proxy.web(req, res, { target: target }); 36 | }); 37 | 38 | // 39 | // Listen for the `error` event on `proxy`. 40 | server.on('error', function (err, req, res) { 41 | res.writeHead(500, { 42 | 'Content-Type': 'text/plain' 43 | }); 44 | 45 | res.end('Something went wrong. And we are reporting a custom error message.'); 46 | }); 47 | 48 | // 49 | // Listen for the `proxyRes` event on `proxy`. 50 | // 51 | server.on('proxyRes', function (res) { 52 | console.log('RAW Response from the target', JSON.stringify(res.headers, true, 2)); 53 | }); 54 | 55 | server.listen(process.env.PORT || 9000); 56 | console.log('Listening on port %d', server.address().port); 57 | 58 | 59 | /***** SETUP EXPRESS SERVER FOR DESIGNER ****/ 60 | 61 | // Create express application (http://expressjs.com) ### 62 | var app = express(); 63 | app.use(express.static(__dirname)); 64 | var appServer = app.listen(8000, function() { 65 | console.log('Listening on port %d', appServer.address().port); 66 | }); -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (grunt) { 4 | // load all grunt tasks 5 | require('matchdep').filterDev('grunt-*').forEach(grunt.loadNpmTasks); 6 | require('time-grunt')(grunt); 7 | 8 | grunt.initConfig({ 9 | clean: ['dist/*'], 10 | uglify: { 11 | dist: { 12 | options: { 13 | sourceMap: 'dist/mobile-ui-elements.min.js.map' 14 | }, 15 | files: { 16 | 'dist/mobile-ui-elements.min.js': ['dist/mobile-ui-elements.js'] 17 | } 18 | } 19 | }, 20 | exec: { 21 | shim_styles: { 22 | command: 'node node_modules/polymer-shim-styles/shim-styles.js dependencies/ratchet/dist/css/ratchet.css dependencies/ratchet/dist/css/ratchet.shim.css\n' + 23 | 'node node_modules/polymer-shim-styles/shim-styles.js elements/css/styles.css elements/css/styles.shim.css\n' + 24 | 'node node_modules/polymer-shim-styles/shim-styles.js elements/css/responsive.css elements/css/responsive.shim.css\n', 25 | stdout: true, 26 | stderr: true 27 | }, 28 | create_app: { 29 | command:'forceios create --apptype=hybrid_local --appname=SDKSample --companyid=com.mobileuielements --organization=MobileUIElements --outputdir=. --appid --callbackuri\n' + 30 | 'cp -r index.html elements dist dependencies SDKSample/www/.\n' + 31 | 'sed -i.orig -n \'1h; 1!H; ${ g; s///;p; }\' SDKSample/www/index.html\n' + 32 | 'cd SDKSample\n' + 33 | 'cordova platform add android\n' + 34 | 'node plugins/com.salesforce/tools/postinstall-android.js 19 true\n' + 35 | 'cordova build &\n' + 36 | 'cd ..', 37 | stdout: true, 38 | stderr: true 39 | } 40 | }, 41 | vulcanize: { 42 | default: { 43 | options: { 44 | csp: true 45 | }, 46 | files: { 47 | 'dist/mobile-ui-elements.html': 'elements/mobile-ui-elements.html' 48 | } 49 | } 50 | } 51 | }); 52 | 53 | grunt.registerTask('build', [ 54 | 'clean', 55 | 'exec:shim_styles' 56 | ]); 57 | 58 | grunt.registerTask('dist', [ 59 | 'build', 60 | 'vulcanize', 61 | 'uglify:dist' 62 | ]); 63 | 64 | grunt.registerTask('create_app', [ 65 | 'dist', 66 | 'exec:create_app' 67 | ]); 68 | 69 | grunt.registerTask('default', ['build']); 70 | }; 71 | -------------------------------------------------------------------------------- /elements/force-ui-detail/force-ui-detail.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 25 | 26 | 27 | 28 | 29 | 30 | 37 | 38 | 39 | 40 | 41 | 55 | 56 | 57 | 60 | 69 | -------------------------------------------------------------------------------- /elements/force-app/force-app.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | 3 | "use strict"; 4 | 5 | // The top level namespace 6 | var SFDC = window.SFDC || {}; 7 | var initialized = false; 8 | var readyDeferred = $.Deferred(); 9 | 10 | var authenticator = function(authProvider) { 11 | return function(client, callback, error) { 12 | if (readyDeferred.state() != 'pending') { 13 | readyDeferred = $.Deferred(); 14 | SFDC.launcher = readyDeferred.promise(); 15 | if (authProvider) authProvider(); 16 | } 17 | SFDC.launcher.done(callback).fail(error); 18 | } 19 | } 20 | 21 | // Global Events Dispatcher to loosely couple all the views 22 | SFDC.eventDispatcher = _.extend({}, Backbone.Events); 23 | SFDC.launcher = readyDeferred.promise(); 24 | 25 | // Forcing v29.0 as API version as all methods/components are currently built using that API. 26 | // This can't be changed by the others to make sure the components don't break due to API changes. 27 | var SFDC_API_VERSION = 'v29.0'; 28 | 29 | SFDC.isOnline = function() { 30 | // If we have cordova available, then use the bootstrap plugin to check network connection. 31 | if (window.cordova) return cordova.require('com.salesforce.util.bootstrap').deviceIsOnline(); 32 | else return navigator.onLine || 33 | (typeof navigator.connection != 'undefined' && 34 | navigator.connection.type !== Connection.UNKNOWN && 35 | navigator.connection.type !== Connection.NONE); 36 | } 37 | 38 | //SFDC.launch 39 | //TODO: Provide an auth provider as an argument so that the consumer can initiate fetch for new session tokens 40 | SFDC.launch = function(options, logLevel) { 41 | var opts = {userAgent: 'SalesforceMobileUI/alpha'}; 42 | options = _.extend(opts, options); 43 | if (!initialized) { 44 | 45 | initialized = true; 46 | Force.init(options, SFDC_API_VERSION, null, authenticator(options.authProvider)); 47 | 48 | if (navigator.smartstore) { 49 | navigator.smartstore.setLogLevel(logLevel || "info"); 50 | SFDC.metadataStore = new Force.StoreCache('sobjectTypes', [], 'type'); 51 | SFDC.metadataStore.init() 52 | .done(function() { 53 | readyDeferred.resolve(); 54 | }); 55 | } else { 56 | readyDeferred.resolve(); 57 | } 58 | } else { 59 | // Forcetk already initialized. So refresh the session info. 60 | Force.forcetkClient.impl.setSessionToken(options.accessToken, SFDC_API_VERSION, options.instanceUrl); 61 | readyDeferred.resolve(); 62 | } 63 | } 64 | 65 | SFDC.cacheMode = function() { 66 | return SFDC.isOnline() ? Force.CACHE_MODE.SERVER_FIRST : Force.CACHE_MODE.CACHE_ONLY; 67 | } 68 | 69 | // Key value object store to cache all the sobject type infos. 70 | var sobjectTypes = {}; 71 | 72 | // Utility method to get the cached instance of Force.SObjectType 73 | SFDC.getSObjectType = function(sobjectName) { 74 | sobjectName = sobjectName.toLowerCase(); 75 | var typeInfo = sobjectTypes[sobjectName]; 76 | 77 | if (!typeInfo) { 78 | typeInfo = new Force.SObjectType(sobjectName, SFDC.metadataStore, SFDC.cacheMode); 79 | sobjectTypes[sobjectName] = typeInfo; 80 | } 81 | return typeInfo; 82 | } 83 | 84 | window.SFDC = SFDC; 85 | 86 | }).call(this, jQuery); 87 | -------------------------------------------------------------------------------- /elements/force-ui-search/force-ui-search.html: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 13 | 17 | 100 | 101 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 44 | 82 | 83 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /elements/force-ui-list/force-ui-list.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 21 | 22 | 23 | 24 | 25 | 38 | 134 | -------------------------------------------------------------------------------- /elements/js/jq-slide.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | 3 | var vendor = (/webkit/i).test(navigator.appVersion) ? 'webkit' : 4 | (/firefox/i).test(navigator.userAgent) ? 'Moz' : 5 | 'opera' in window ? 'O' : ''; 6 | 7 | $.fn.changePage = function (to, reverse, callback) { 8 | var fromPage = this, 9 | toPage = (typeof to == 'object') ? to : $(to), 10 | parent = fromPage.parent(), 11 | parentWidth = parent.width(), 12 | onComplete, translateX; 13 | 14 | if (parent.is(toPage.parent())) { 15 | if (reverse) { 16 | translateX = 0; 17 | parent.css('-webkit-transition', 'none') 18 | .css('-webkit-transform', 'translate3d(' + (-1*parentWidth) + 'px, 0, 0)'); 19 | fromPage.css('-webkit-transform', 'translate3d(' + parentWidth + 'px, 0, 0)'); 20 | } else { 21 | translateX = -1*parentWidth; 22 | toPage.css('-webkit-transform', 'translate3d(' + parentWidth + 'px, 0, 0)'); 23 | } 24 | 25 | onComplete = function() { 26 | fromPage.css('visibility', 'hidden').css('-webkit-transform', 'none'); 27 | parent.css('-webkit-transition', 'none').css('-webkit-transform', 'none'); 28 | toPage.css('-webkit-transform', 'none'); 29 | /* Reset the transition property*/ 30 | setTimeout(function() { parent.css('-webkit-transition', ''); }, 0); 31 | if (typeof callback == 'function') callback(); 32 | } 33 | 34 | toPage.css('visibility', ''); 35 | parent.slide('translate3d(' + translateX + 'px, 0, 0)', onComplete); 36 | } 37 | }; 38 | 39 | $.fn.scrollHeight = function() { 40 | return this[0].scrollHeight; 41 | }; 42 | 43 | $.fn.slide = function(transform, callback) { 44 | var that = this, transitionEnd = false; 45 | 46 | var onComp = function(event) { 47 | if (!transitionEnd && (!event || that.is(event.target))) { 48 | transitionEnd = true; 49 | that.off('webkitTransitionEnd'); 50 | if(typeof callback == 'function') callback(); 51 | } 52 | }; 53 | this.off('webkitTransitionEnd').on('webkitTransitionEnd', onComp); 54 | this.css('visibility', 'visible'); 55 | 56 | setTimeout(function() { 57 | that.css('-webkit-transition', '-webkit-transform ease 0.4s') 58 | .css('-webkit-backface-visibility', 'hidden') 59 | .css(vendor + 'Transform', transform); 60 | }, 10); 61 | 62 | // Ensure we do callback no matter what 63 | setTimeout(onComp, 1000); 64 | 65 | return this; 66 | }; 67 | 68 | $.fn.slideIn = function(axis, initialPos, finalPos, callback) { 69 | var that = this; 70 | 71 | var initialTransform, finalTransform; 72 | switch(axis.toUpperCase()) { 73 | case 'X': 74 | initialTransform = 'translate3d(' + initialPos + 'px, 0px, 0px)'; 75 | finalTransform = 'translate3d(' + finalPos + 'px, 0px, 0px)'; 76 | break; 77 | case 'Y': 78 | initialTransform = 'translate3d(0px, ' + initialPos + 'px, 0px)'; 79 | finalTransform = 'translate3d(0px, ' + finalPos + 'px, 0px)'; 80 | break; 81 | }; 82 | 83 | this.css('visibility', 'hidden').css(vendor + 'TransitionProperty','none') 84 | .css(vendor + 'Transform', initialTransform); 85 | 86 | return this.slide(finalTransform, callback); 87 | }; 88 | 89 | $.fn.slideOut = function(axis, finalPos, callback){ 90 | var that = this; 91 | 92 | var finalTransform; 93 | switch(axis.toUpperCase()) { 94 | case 'X': 95 | finalTransform = 'translate3d(' + finalPos + 'px, 0px, 0px)'; 96 | break; 97 | case 'Y': 98 | finalTransform = 'translate3d(0px, ' + finalPos + 'px, 0px)'; 99 | break; 100 | }; 101 | 102 | var onComp = function() { 103 | that.css('visibility', 'hidden'); 104 | if(typeof callback == 'function') callback(); 105 | }; 106 | 107 | return this.slide(finalTransform, onComp); 108 | }; 109 | 110 | $.fn.slideInLeft = function(callback) { 111 | 112 | var leftPos = this.parent().width() - this.outerWidth(); // Element should be hidden before calculating width 113 | this.slideIn('X', this.parent().width(), leftPos, callback); 114 | }; 115 | 116 | $.fn.slideOutRight = function(callback) { 117 | 118 | if (this.css('display') == 'block') { 119 | this.slideOut('X', this.parent().width(), callback); 120 | } 121 | }; 122 | })(jQuery); -------------------------------------------------------------------------------- /elements/force-sobject-layout/force-sobject-layout.js: -------------------------------------------------------------------------------- 1 | (function(SFDC) { 2 | 3 | "use strict"; 4 | 5 | // Fetches the record type id for the required layout. 6 | // Returns a promise which is resolved when the record type id is fetched from the server. 7 | var fetchRecordTypeId = function() { 8 | var view = this; 9 | var fetchStatus = $.Deferred(); 10 | 11 | var resolveStatus = function(recordTypeId) { 12 | fetchStatus.resolve(view.sobject, recordTypeId); 13 | } 14 | 15 | // If record types are not present, then use the default recordtypeid 16 | if (!view.hasrecordtypes) resolveStatus('012000000000000AAA'); 17 | // If record types are present, then get the recordtypeid 18 | else { 19 | var sobjectElem = view.$.force_sobject; 20 | // If record type id is provided then use that. 21 | if (view.recordtypeid) resolveStatus(view.recordtypeid); 22 | // If not but the recordid is available, then get the recordtype info from sfdc 23 | else if (sobjectElem.fields.Id && sobjectElem.fields.Id.length) { 24 | // Fetch the record's recordtypeid 25 | sobjectElem.fetch({ 26 | success: function() { 27 | // Once we get the recordtypeid, fetch the layout 28 | resolveStatus(this.get('recordTypeId')); 29 | }, 30 | error: function() { 31 | fetchStatus.reject(view); 32 | } 33 | }); 34 | } 35 | } 36 | 37 | return fetchStatus.promise(); 38 | } 39 | 40 | var getLayoutInfo = function(sobject, recordtypeid) { 41 | return SFDC.getSObjectType(sobject) 42 | .describeLayout(recordtypeid); 43 | } 44 | Polymer({ 45 | is: 'force-sobject-layout', 46 | properties: { 47 | 48 | /** 49 | * (Required) Name of Salesforce sobject for which layout info will be fetched. 50 | * 51 | * @attribute sobject 52 | * @type String 53 | */ 54 | sobject: String, 55 | 56 | /** 57 | * (Optional) If false, the element returns the default layout. Set true if the sobject has recordtypes or if you are unsure. If set to true, `recordid` or `recordtypeid` must be provided. 58 | * 59 | * @attribute hasrecordtypes 60 | * @type Boolean 61 | * @default false 62 | */ 63 | hasrecordtypes: { 64 | type: Boolean, 65 | value: false 66 | }, 67 | 68 | /** 69 | * (Optional) Id of the record type for which layout has to be fetched. Required if `hasrecordtypes` is true and `recordid` is not provided. 70 | * 71 | * @attribute recordtypeid 72 | * @type String 73 | * @default null 74 | */ 75 | recordtypeid: { 76 | type: String, 77 | value: null 78 | }, 79 | 80 | /** 81 | * (Optional) Id of the record for which layout has to be fetched. Required if `hasrecordtypes` is true and `recordtypeid` is not provided. 82 | * 83 | * @attribute recordid 84 | * @type String 85 | * @default null 86 | */ 87 | recordid: { 88 | type: String, 89 | value: null 90 | }, 91 | 92 | /** 93 | * Returns an object with the complete layout information. 94 | * 95 | * @attribute fields 96 | * @type Object 97 | * @readOnly 98 | */ 99 | layout: { 100 | type: Object, 101 | readOnly: true, 102 | notify: true 103 | } 104 | }, 105 | observers: [ 106 | "_reset(sobject, hasrecordtypes, recordid, recordtypeid)" 107 | ], 108 | _reset: function() { 109 | if (this.hasrecordtypes || this._sobject != this.sobject) { 110 | this._sobject = this.sobject; 111 | this._setLayout(null); 112 | this.debounce("fetch-layout", this.fetch.bind(this)); 113 | } 114 | }, 115 | 116 | /** 117 | * Method to manually initiate the fetching of layout information. 118 | * 119 | * @method fetch 120 | */ 121 | fetch: function() { 122 | if (this.layout && !this.hasrecordtypes) return; 123 | if (this.sobject && typeof this.sobject === 'string') { 124 | SFDC.launcher 125 | .then(fetchRecordTypeId.bind(this)) 126 | .then(getLayoutInfo) 127 | .then(function(layout) { 128 | this._setLayout(layout); 129 | this.fire('layout-change'); 130 | }.bind(this)); 131 | } 132 | } 133 | }); 134 | 135 | }).call(this, SFDC); -------------------------------------------------------------------------------- /elements/force-ui-relatedlist/force-ui-relatedlist.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 23 | 24 | 27 | 28 | 29 | 30 | 42 | 43 | 59 | 123 | -------------------------------------------------------------------------------- /elements/force-signin/forcetk.ui.js: -------------------------------------------------------------------------------- 1 | (function (root) { 2 | 3 | /** 4 | * ForceOAuth constructor 5 | * 6 | * @param loginURL string Login url, typically it is: https://login.salesforce.com/ 7 | * @param consumerKey string Consumer Key from Setup | Develop | Remote Access 8 | * @param callbackURL string Callback URL from Setup | Develop | Remote Access 9 | * @param successCallback function Function that will be called on successful login, it accepts single argument with forcetk.Client instance 10 | * @param errorCallback function Function that will be called when login process fails, it accepts single argument with error object 11 | * 12 | * @constructor 13 | */ 14 | root.ForceOAuth = function (loginURL, consumerKey, callbackURL, successCallback, errorCallback, proxyUrl) { 15 | 16 | if (typeof loginURL !== 'string') throw new TypeError('loginURL should be of type String'); 17 | this.loginURL = loginURL; 18 | 19 | if (typeof consumerKey !== 'string') throw new TypeError('consumerKey should be of type String'); 20 | this.consumerKey = consumerKey; 21 | 22 | if (typeof callbackURL !== 'string') throw new TypeError('callbackURL should be of type String'); 23 | this.callbackURL = callbackURL; 24 | 25 | if (typeof successCallback !== 'function') throw new TypeError('successCallback should of type Function'); 26 | this.successCallback = successCallback; 27 | 28 | if (typeof errorCallback !== 'undefined' && typeof errorCallback !== 'function') 29 | throw new TypeError('errorCallback should of type Function'); 30 | this.errorCallback = errorCallback; 31 | 32 | }; 33 | 34 | root.ForceOAuth.prototype = { 35 | 36 | /** 37 | * Starts OAuth login process. 38 | */ 39 | login: function login(usePopupWindow) { 40 | var that = this; 41 | 42 | var winHeight = 524, 43 | winWidth = 674, 44 | centeredY = window.screenY + (window.outerHeight / 2 - winHeight / 2), 45 | centeredX = window.screenX + (window.outerWidth / 2 - winWidth / 2); 46 | 47 | var authUrl = that.loginURL + 'services/oauth2/authorize?' 48 | + '&response_type=token&client_id=' + encodeURIComponent(that.consumerKey) 49 | + '&redirect_uri=' + encodeURIComponent(that.callbackURL); 50 | 51 | if (usePopupWindow) { 52 | var loginWindow = window.open(authUrl, 53 | 'Login to Salesforce', 'height=' + winHeight + ',width=' + winWidth 54 | + ',toolbar=1,scrollbars=1,status=1,resizable=1,location=0,menuBar=0' 55 | + ',left=' + centeredX + ',top=' + centeredY); 56 | 57 | if (loginWindow) { 58 | // Creating an interval to detect popup window location change event 59 | var interval = setInterval(function () { 60 | if (loginWindow.closed) { 61 | // Clearing interval if popup was closed 62 | clearInterval(interval); 63 | } else { 64 | var loc = loginWindow.location.href; 65 | if (typeof loc !== 'undefined' && loc.indexOf(that.callbackURL) == 0) { 66 | loginWindow.close(); 67 | that.oauthCallback(loginWindow.location.hash); 68 | } 69 | } 70 | }, 250); 71 | 72 | loginWindow.focus(); 73 | } 74 | } else { 75 | if (window.location.href.indexOf(that.callbackURL) == 0) { 76 | that.errorCallback = function() { 77 | window.location = authUrl; 78 | } 79 | that.oauthCallback(window.location.hash); 80 | } else window.location = authUrl; 81 | } 82 | }, 83 | 84 | logout: function logout(logoutCallback) { 85 | 86 | }, 87 | 88 | oauthCallback: function oauthCallback(locHash) { 89 | var fragment = (locHash || window.location.hash).split("#")[1]; 90 | this.oauthResponse = {}; 91 | 92 | if (fragment) { 93 | this.oauthResponse['response'] = fragment; 94 | var nvps = fragment.split('&'); 95 | for (var nvp in nvps) { 96 | var parts = nvps[nvp].split('='); 97 | 98 | //Note some of the values like refresh_token might have '=' inside them 99 | //so pop the key(first item in parts) and then join the rest of the parts with = 100 | var key = parts.shift(); 101 | var val = parts.join('='); 102 | this.oauthResponse[key] = decodeURIComponent(val); 103 | } 104 | } 105 | 106 | if (typeof this.oauthResponse.access_token === 'undefined') { 107 | 108 | if (this.errorCallback) 109 | this.errorCallback({code: 0, message: 'Unauthorized - no OAuth response!'}); 110 | else 111 | console.log('ERROR: No OAuth response!') 112 | 113 | } else { 114 | 115 | if (this.successCallback) { 116 | this.successCallback(this.oauthResponse); 117 | window.location.hash = ""; 118 | } else 119 | console.log('INFO: OAuth login successful!') 120 | } 121 | } 122 | }; 123 | })(this); -------------------------------------------------------------------------------- /elements/force-ui-relatedlist/force-ui-relatedlist.js: -------------------------------------------------------------------------------- 1 | (function($, SFDC) { 2 | Polymer({ 3 | is: 'force-sobject-relatedlists', 4 | properties: { 5 | 6 | /** 7 | * (Required) Name of Salesforce sobject for which related list info will be fetched. 8 | * 9 | * @attribute sobject 10 | * @type String 11 | */ 12 | sobject: String, 13 | 14 | /** 15 | * Id of the record for which related list queries will be generated. These queries can be used for fetching related records. 16 | * 17 | * @attribute recordid 18 | * @type String 19 | */ 20 | recordid: { 21 | type: String, 22 | observer: 'generateRelatedLists' 23 | }, 24 | 25 | /** 26 | * (Optional) If false, the element returns the related list on default layout. Set true if the sobject has recordtypes or if you are unsure. If set to true, "recordid" or "recordtypeid" must be provided. 27 | * 28 | * @attribute hasrecordtypes 29 | * @type Boolean 30 | */ 31 | hasrecordtypes: String, 32 | 33 | /** 34 | * (Optional) Id of the record type for which layout has to be fetched. Required if "hasrecordtypes" is true and "recordid" is not provided. 35 | * 36 | * @attribute recordtypeid 37 | * @type String 38 | */ 39 | recordtypeid: String, 40 | 41 | /** 42 | * (Optional) A list of relationship names that should only be fetched. If null, it fetches all related lists that are queryable. 43 | * 44 | * @attribute relationships 45 | * @type String 46 | */ 47 | relationships: { 48 | type: String, 49 | observer: "relationshipsChanged" 50 | }, 51 | 52 | /** 53 | * Returns an array of all the related list information. 54 | * 55 | * @attribute relatedLists 56 | * @type Array 57 | * @readOnly 58 | */ 59 | relatedLists: { 60 | type: Array, 61 | readOnly: true, 62 | notify: true 63 | } 64 | }, 65 | relationshipsChanged: function() { 66 | // Execute generateRelatedLists after current process ends to allow processing all change handlers on parent. 67 | setTimeout(this.generateRelatedLists.bind(this), 0); 68 | }, 69 | generateRelatedLists: function(ev) { 70 | this._setRelatedLists([]); 71 | fetchRelatedLists(this); 72 | } 73 | }); 74 | 75 | var fetchRelatedLists = function(view) { 76 | var relsToKeep = typeof view.relationships === 'string' 77 | ? view.relationships.trim().split(/\s+/) : null; 78 | var parentType = SFDC.getSObjectType(view.sobject); 79 | 80 | var addConfigIfAllowed = function(related, childInfo, parentDescribe) { 81 | if (childInfo.objectDescribe.queryable) { 82 | view.push('relatedLists', related); 83 | generateQuery(view.recordid, related, parentDescribe); 84 | } 85 | } 86 | 87 | var generateRelatedListConfigs = function(relatedListInfo) { 88 | for (var idx in relatedListInfo) { 89 | var related = _.extend({}, relatedListInfo[idx]); 90 | if (!relsToKeep || _.contains(related.name)) { 91 | var childType = SFDC.getSObjectType(related.sobject); 92 | $.when(related, childType.getMetadata(), parentType.describe()) 93 | .then(addConfigIfAllowed); 94 | } 95 | } 96 | } 97 | 98 | // Generate related lists if the layout has been fetched 99 | if (view.recordid && view.$.sobject_layout.layout) { 100 | generateRelatedListConfigs(view.$.sobject_layout.layout.relatedLists); 101 | } 102 | } 103 | 104 | 105 | 106 | var generateQuery = function(recordid, related, describeResult) { 107 | var rel = _.findWhere(describeResult.childRelationships, {relationshipName : related.name}); 108 | // Column names are used for generating the soql query 109 | var colNameList = _.union(_.pluck(related.columns, "name"), ['Id']); 110 | related.soql = "SELECT " + colNameList.join(",") 111 | + " FROM " + related.sobject 112 | + " WHERE " + rel.field + " = '" + recordid + "'" 113 | + " ORDER BY " + related.sort[0].column + (related.sort[0].ascending ? ' asc' : ' desc') 114 | + " LIMIT " + related.limitRows; 115 | 116 | // the column field value should be used for generating the smartsql 117 | var fieldList = _.union(_.pluck(related.columns, "field"), [related.sobject + '.Id']); 118 | var wrapFieldName = function(name) { 119 | return "{" + name.replace(/\./, ':') + "}"; 120 | } 121 | related.smartSql = "SELECT " + _.map(fieldList, wrapFieldName).join(",") 122 | + " FROM {" + related.sobject + "}" 123 | + " WHERE {" + related.sobject + ":" + rel.field + "} = '" + recordid + "'" 124 | + " ORDER BY {" + related.sobject + ":" + related.sort[0].column + (related.sort[0].ascending ? '} asc' : '} desc') 125 | + " LIMIT " + related.limitRows; 126 | 127 | _.extend(related, { 128 | get query() { 129 | if (!SFDC.isOnline() && navigator.smartstore) 130 | return navigator.smartstore.buildSmartQuerySpec(related.smartSql); 131 | else return related.soql; 132 | }, 133 | get querytype() { 134 | return (!SFDC.isOnline() && navigator.smartstore) ? "cache" : "soql"; 135 | } 136 | }) 137 | } 138 | 139 | }).call(this, jQuery, window.SFDC); -------------------------------------------------------------------------------- /tests/force-sobject-store.test.js: -------------------------------------------------------------------------------- 1 | describe('force-sobject-store', function() { 2 | var sobjectStore; 3 | 4 | beforeEach(function() { 5 | sobjectStore = document.createElement('force-sobject-store'); 6 | SFDC.launch({ 7 | accessToken: 'mock_token', 8 | instanceUrl: 'https://mock.salesforce.com' 9 | }); 10 | }); 11 | 12 | describe('#cache', function() { 13 | it('all caches should be undefined when sobject type is not defined', function() { 14 | sobjectStore.should.have.property('cacheReady', undefined); 15 | sobjectStore.should.have.property('cache', undefined); 16 | sobjectStore.should.have.property('cacheForOriginals', undefined); 17 | }); 18 | it('should be valid when sobject type is defined', function(done) { 19 | sobjectStore.sobject = 'Mock__c'; 20 | Force.forcetkClient.impl.ajax = function(path, callback, error) { 21 | setTimeout(function() { callback({ fields: [] }); }, 0); 22 | }; 23 | sobjectStore.addEventListener('store-ready', function() { 24 | sobjectStore.cache.should.be.an.instanceOf(Force.StoreCache).and.be.ok; 25 | sobjectStore.cache.soupName.should.eql('mock__c'); 26 | sobjectStore.cacheForOriginals.soupName.should.eql('__mock__c__original'); 27 | sobjectStore.cache.keyField.should.eql('Id'); 28 | sobjectStore.cache.additionalIndexSpecs.should.have.length(1); 29 | sobjectStore.cache.additionalIndexSpecs.should.containEql({ 30 | path: 'attributes.type', 31 | type: 'string' 32 | }); 33 | done(); 34 | }); 35 | Platform.flush(); 36 | }); 37 | it('should have ExternalId as keyField when sobject type is External Data object', function(done) { 38 | sobjectStore.sobject = 'Mock__x'; 39 | Force.forcetkClient.impl.ajax = function(path, callback, error) { 40 | setTimeout(function() { callback({ fields: [] }); }, 0); 41 | }; 42 | sobjectStore.addEventListener('store-ready', function() { 43 | sobjectStore.cache.should.be.an.instanceOf(Force.StoreCache).and.be.ok; 44 | sobjectStore.cache.keyField.should.eql('ExternalId'); 45 | done(); 46 | }); 47 | Platform.flush(); 48 | }); 49 | it('should have additional indices for all sobject parent relationships', function(done) { 50 | sobjectStore.sobject = 'MockSObject1'; 51 | Force.forcetkClient.impl.ajax = function(path, callback, error) { 52 | setTimeout(function() { 53 | callback({ 54 | fields: [{ name: 'rel1', type: 'reference' }, 55 | { name: 'rel2', type: 'reference' }] 56 | }); 57 | }, 0); 58 | }; 59 | sobjectStore.addEventListener('store-ready', function() { 60 | sobjectStore.cache.should.be.an.instanceOf(Force.StoreCache).and.be.ok; 61 | sobjectStore.cache.additionalIndexSpecs.should.have.length(3); 62 | sobjectStore.cache.additionalIndexSpecs.should.containEql({ 63 | path: 'attributes.type', 64 | type: 'string' 65 | }).and.containEql({ 66 | path: 'rel1', 67 | type: 'string' 68 | }).and.containEql({ 69 | path: 'rel2', 70 | type: 'string' 71 | }); 72 | done(); 73 | }); 74 | Platform.flush(); 75 | }); 76 | it('should have additional indices for additional fieldstoindex', function(done) { 77 | sobjectStore.sobject = 'MockSObject2'; 78 | sobjectStore.fieldstoindex = 'Name Employees'; 79 | Force.forcetkClient.impl.ajax = function(path, callback, error) { 80 | setTimeout(function() { 81 | callback({ 82 | fields: [{ name: 'Name', type: 'string' }, 83 | { name: 'Employees', type: 'int' }] 84 | }); 85 | }, 0); 86 | }; 87 | sobjectStore.addEventListener('store-ready', function() { 88 | sobjectStore.cache.should.be.an.instanceOf(Force.StoreCache).and.be.ok; 89 | sobjectStore.cache.additionalIndexSpecs.should.have.length(3); 90 | sobjectStore.cache.additionalIndexSpecs.should.containEql({ 91 | path: 'attributes.type', 92 | type: 'string' 93 | }).and.containEql({ 94 | path: 'Name', 95 | type: 'string' 96 | }).and.containEql({ 97 | path: 'Employees', 98 | type: 'integer' 99 | }); 100 | done(); 101 | }); 102 | Platform.flush(); 103 | }); 104 | it('should not create index for non-existing fields in fieldstoindex', function(done) { 105 | sobjectStore.sobject = 'MockSObject3'; 106 | sobjectStore.fieldstoindex = 'Invalid'; 107 | Force.forcetkClient.impl.ajax = function(path, callback, error) { 108 | setTimeout(function() { 109 | callback({ 110 | fields: [{ name: 'Name', type: 'string' }, 111 | { name: 'Employees', type: 'int' }] 112 | }); 113 | }, 0); 114 | }; 115 | sobjectStore.addEventListener('store-ready', function() { 116 | sobjectStore.cache.should.be.an.instanceOf(Force.StoreCache).and.be.ok; 117 | sobjectStore.cache.additionalIndexSpecs.should.have.length(1); 118 | sobjectStore.cache.additionalIndexSpecs.should.containEql({ 119 | path: 'attributes.type', 120 | type: 'string' 121 | }); 122 | done(); 123 | }); 124 | Platform.flush(); 125 | }); 126 | }); 127 | }); -------------------------------------------------------------------------------- /elements/force-sobject-collection/force-sobject-collection.js: -------------------------------------------------------------------------------- 1 | (function(SFDC) { 2 | 3 | "use strict"; 4 | 5 | var viewProps = { 6 | 7 | /** 8 | * (Required) Name of Salesforce sobject against which fetch operations will be performed. 9 | * 10 | * @attribute sobject 11 | * @type String 12 | */ 13 | sobject: String, 14 | 15 | /** 16 | * (Optional) SOQL/SOSL/SmartSQL statement to fetch the records. Required when querytype is soql, sosl or cache. 17 | * 18 | * @attribute query 19 | * @type String 20 | * @default null 21 | */ 22 | query: { 23 | type: String, 24 | value: null 25 | }, 26 | 27 | /** 28 | * (Optional) Type of query (mru, soql, sosl, cache). Required if query attribute is specified. 29 | * 30 | * @attribute querytype 31 | * @type String 32 | * @default mru 33 | */ 34 | querytype: { 35 | type: String, 36 | value: "mru" 37 | }, 38 | 39 | /** 40 | * (Optional) Auto synchronize (fetch/save) changes to the model with the remote server/local store. If false, use fetch/save methods to commit changes to server or local store. 41 | * 42 | * @attribute autosync 43 | * @type Boolean 44 | * @default false 45 | */ 46 | autosync: Boolean, 47 | 48 | /** 49 | * (Optional) If positive, limits the maximum number of records fetched. 50 | * 51 | * @attribute maxsize 52 | * @type Number 53 | * @default -1 54 | */ 55 | maxsize: { 56 | type: Number, 57 | value: -1 58 | } 59 | }; 60 | 61 | var generateConfig = function(props) { 62 | var config = {}; 63 | 64 | // Fetch if only sobject type is specified. 65 | if (props.sobject && typeof props.sobject === 'string') { 66 | config.sobjectType = props.sobject; 67 | // Is device offline and smartstore is available 68 | if (!SFDC.isOnline() && navigator.smartstore) { 69 | // Only run cache queries. If none provided, fetch all data. 70 | config.type = 'cache'; 71 | if (props.querytype == 'cache' && props.query) config.cacheQuery = props.query; 72 | else config.cacheQuery = navigator.smartstore.buildAllQuerySpec('attributes.type'); 73 | } 74 | /* Query must be specified if Querytype is not mru */ 75 | else if (props.querytype == 'mru' || (props.querytype && props.query)) { 76 | 77 | // Send the user config for fetch 78 | config.type = props.querytype; 79 | if (props.querytype == 'cache') config.cacheQuery = props.query; 80 | else config.query = props.query; 81 | } 82 | } 83 | return (config.type) ? config : null; 84 | } 85 | 86 | //TBD: Make collection a private property. Then expose sobjects property which contains the array of models wrapped into SObjectViewModel. 87 | Polymer({ 88 | is: 'force-sobject-collection', 89 | 90 | /** 91 | * Fired when the collection's entire contents have been replaced. 92 | * 93 | * @event reset 94 | */ 95 | 96 | /** 97 | * Fired when the data has been successfully synced with the server. 98 | * 99 | * @event sync 100 | */ 101 | 102 | /** 103 | * Fired when a request to remote server has failed. 104 | * 105 | * @event error 106 | */ 107 | 108 | properties: _.extend({ 109 | 110 | /** 111 | * (Optional) A Promise that returns an instance of force-sobject-store on cache ready completion. 112 | * It is required to add offline capability to the component. 113 | * 114 | * @attribute cachePromise 115 | * @type Object 116 | */ 117 | cachePromise: Object, 118 | 119 | /** 120 | * Returns an instance of Force.SObjectCollection containing the list of records fetched by the query. 121 | * 122 | * @attribute collection 123 | * @type Object 124 | */ 125 | collection: { 126 | type: Object, 127 | value: function() { 128 | var collection = new Force.SObjectCollection(); 129 | collection.on('all', function(event) { 130 | this.fire(event); 131 | }.bind(this)); 132 | this.resetCount = 0; 133 | return collection; 134 | }, 135 | notify: true 136 | } 137 | }, viewProps), 138 | observers: [ 139 | 'reset(sobject, query, querytype)' 140 | ], 141 | 142 | /** 143 | * Replaces all the existing contents of the collection and initiates autosync if enabled. 144 | * 145 | * @method reset 146 | */ 147 | reset: function(sobject, query, querytype) { 148 | this.collection.config = generateConfig(_.pick(this, _.keys(viewProps))); 149 | this.collection.reset(); 150 | if (this.autosync) this.fetch(); 151 | }, 152 | /** 153 | * Initiates the fetching of records from the relevant data store (server/offline store). 154 | * 155 | * @method fetch 156 | */ 157 | fetch: function() { 158 | var onFetch = function() { 159 | if ((this.maxsize < 0 || this.maxsize > this.collection.length) 160 | && this.collection.hasMore()) 161 | this.collection.getMore().then(onFetch); 162 | }.bind(this); 163 | 164 | var operation = function() { 165 | var collection = this.collection; 166 | var store = this.$.store; 167 | 168 | if (collection.config) { 169 | // Define the collection model type. Set the idAttribute to 'ExternalId' if sobject is external object. 170 | collection.model = Force.SObject.extend({ 171 | idAttribute: this.sobject.toLowerCase().search(/__x$/) > 0 ? 'ExternalId' : 'Id' 172 | }); 173 | $.when(store.cacheReady, SFDC.launcher) 174 | .done(function() { 175 | collection.cache = store.cache; 176 | collection.cacheForOriginals = store.cacheForOriginals; 177 | collection.fetch({ reset: true, success: onFetch }); 178 | }); 179 | } 180 | }.bind(this); 181 | 182 | // Queue the operation for next cycle after all change watchers are fired. 183 | this.async(operation); 184 | } 185 | }); 186 | 187 | })(window.SFDC); 188 | -------------------------------------------------------------------------------- /elements/force-sobject-store/force-sobject-store.js: -------------------------------------------------------------------------------- 1 | (function(SFDC, Force) { 2 | 3 | "use strict"; 4 | 5 | var sobjectStores = {}; 6 | var originalSObjectStores = {}; 7 | 8 | var generateIndexSpec = function(describeResult, fieldsToIndex) { 9 | var indexSpecs = [{path: "attributes.type", type: "string"}]; 10 | if (describeResult) { 11 | describeResult.fields.forEach(function(field) { 12 | if (field.type == 'reference' || _.contains(fieldsToIndex, field.name)) { 13 | var storeType; 14 | switch(field.type) { 15 | case 'int': storeType = 'integer'; break; 16 | case 'double': storeType = 'real'; break; 17 | default: storeType = 'string'; 18 | } 19 | indexSpecs.push({ 20 | path: field.name, 21 | type: storeType 22 | }); 23 | } 24 | }); 25 | } else if (fieldsToIndex) { // If describe sobject is not available, just create index on specified fields. 26 | fieldsToIndex.forEach(function(field) { 27 | indexspec.push({ 28 | path: field, 29 | type: "string" 30 | }); 31 | }); 32 | } 33 | return indexSpecs; 34 | } 35 | 36 | // Returns either a promise to track store creation progress, or returns the store if ready. 37 | var createStores = function(sobject, keyField, fieldsToIndex) { 38 | var dataStore; 39 | var originalDataStore; 40 | var that = this; 41 | 42 | // Initiate store cache creation if none initiated already for this sobject 43 | if (sobject && !sobjectStores[sobject]) { 44 | var sobjectType = SFDC.getSObjectType(sobject); 45 | // Initiate store creation of working cache copy based on describe info and fieldstoindex. 46 | var storePromise = $.when(sobjectType.describe(), fieldsToIndex) 47 | .then(generateIndexSpec) 48 | .then(function(indexSpecs) { 49 | // Create store based on indexspec 50 | dataStore = new Force.StoreCache(sobject, indexSpecs, keyField); 51 | // Create store for original copy. No indexspec requird for original copy. 52 | originalDataStore = new Force.StoreCache("__" + sobject + "__original", null, keyField); 53 | return $.when(dataStore.init(), originalDataStore.init()); 54 | }).then(function() { 55 | sobjectStores[sobject] = dataStore; 56 | originalSObjectStores[sobject] = originalDataStore; 57 | that.fire('store-ready'); 58 | }); 59 | // Capture the store creation promise, until the real store gets assigned. 60 | // This will prevent creation of same store twice. 61 | sobjectStores[sobject] = storePromise; 62 | } 63 | return sobjectStores[sobject]; 64 | } 65 | 66 | var processName = function(sobject) { 67 | return typeof sobject === 'string' ? sobject.toLowerCase() : undefined; 68 | } 69 | 70 | //TBD: Add a property "autoIndex" to generate indexes based on object describe result 71 | Polymer({ 72 | is: 'force-sobject-store', 73 | properties: { 74 | 75 | /** 76 | * (Required) Type of sobject that you would like to store in this cache. 77 | * 78 | * @attribute sobject 79 | * @type String 80 | */ 81 | sobject: String, 82 | 83 | /** 84 | * (Optional) Addition fields (given by their name) that you want to have indexes on. 85 | * Provide a space delimited list. Also the field names are case sensitive. 86 | * 87 | * @attribute fieldstoindex 88 | * @type String 89 | * @default null 90 | */ 91 | fieldstoindex: { 92 | type: String, /*TBD: Should switch to array */ 93 | value: null 94 | } 95 | }, 96 | observers: [ 97 | "init(sobject, fieldstoindex)" 98 | ], 99 | // cacheReady: Returns a promise to track store cache creation progress. 100 | /* TBD: Evaluate moving to computed properties */ 101 | get cacheReady() { 102 | return this.init(); 103 | }, 104 | // cache: Returns an instance of Force.StoreCache when it's ready to store/retrieve data. 105 | get cache() { 106 | var cache = sobjectStores[processName(this.sobject)]; 107 | if (cache instanceof Force.StoreCache) return cache; 108 | }, 109 | // cacheForOriginals: Returns an instance of Force.StoreCache to be used to keep data copy for conflict resolution. 110 | get cacheForOriginals() { 111 | var cache = originalSObjectStores[processName(this.sobject)]; 112 | if (cache instanceof Force.StoreCache) return cache; 113 | }, 114 | init: function() { 115 | var that = this; 116 | var sobject = processName(this.sobject); 117 | 118 | // Create StoreCache if smartstore is available. Also check if sobject is properly set 119 | if (navigator.smartstore && sobject) { 120 | var keyField = ((sobject && sobject.indexOf('__x') > 0) 121 | ? 'ExternalId' : 'Id'); 122 | var fieldsToIndex = typeof this.fieldstoindex === 'string' ? 123 | this.fieldstoindex.trim().split(/\s+/) : []; 124 | 125 | // Create offline stores if launcher is complete 126 | return SFDC.launcher.then(function() { 127 | var storeCreator = createStores.bind(that); 128 | return storeCreator(sobject, keyField, fieldsToIndex); 129 | }); 130 | } 131 | }, 132 | /** 133 | * Removes the soup from smartstore. Returns a promise to track the completion of process. 134 | * 135 | * @method destroy 136 | */ 137 | destroy: function() { 138 | var cacheDestroy, cacheForOriginalsDestroy; 139 | var that = this; 140 | 141 | if (this.cache) { 142 | cacheDestroy = Force.smartstoreClient.removeSoup(this.cache.soupName); 143 | delete sobjectStores[processName(this.sobject)]; 144 | } 145 | if (this.cacheForOriginals) { 146 | cacheForOriginalsDestroy = Force.smartstoreClient.removeSoup(this.cacheForOriginals.soupName); 147 | delete originalSObjectStores[processName(this.sobject)]; 148 | } 149 | return $.when(cacheDestroy, cacheForOriginalsDestroy).then(function() { 150 | // Fire this only when actual remove soup operation has happened 151 | if (cacheDestroy) that.fire('store-destroy'); 152 | }); 153 | } 154 | }); 155 | 156 | }).call(this, window.SFDC, window.Force); 157 | -------------------------------------------------------------------------------- /elements/force-ui-app/force-ui-app.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 23 | 24 | 25 | 26 | 53 | 54 | 55 | 56 | 67 | 68 | 198 | -------------------------------------------------------------------------------- /tests/force-sobject.test.js: -------------------------------------------------------------------------------- 1 | describe('force-sobject', function() { 2 | var sobject; 3 | var origAjax; 4 | var smartstore; 5 | 6 | /* Disable smartstore for this testsuite */ 7 | before(function() { 8 | smartstore = navigator.smartstore; 9 | navigator.smartstore = undefined; 10 | }); 11 | 12 | after(function() { 13 | navigator.smartstore = smartstore; 14 | }); 15 | 16 | beforeEach(function() { 17 | sobject = document.createElement('force-sobject'); 18 | sobject.autosync = false; 19 | origAjax = Force.forcetkClient.impl.ajax; 20 | }); 21 | 22 | afterEach(function() { 23 | Force.forcetkClient.impl.ajax = origAjax; 24 | }); 25 | 26 | describe('#model', function() { 27 | it('should be undefined when sobject type is not defined', function(){ 28 | sobject.should.not.have.property('_model'); 29 | }); 30 | it('should be defined when sobject type is defined', function(done) { 31 | sobject.sobject = 'asdf'; 32 | sobject.async(function() { 33 | sobject.should.have.property('_model'); 34 | done(); 35 | }); 36 | }); 37 | it('should have idAttribute as "Id" when standard sobject', function(done) { 38 | sobject.sobject = 'Account'; 39 | sobject.async(function() { 40 | sobject._model.idAttribute.should.eql('Id'); 41 | done(); 42 | }); 43 | }); 44 | it('should have idAttribute as "Id" when custom sobject', function(done) { 45 | sobject.sobject = 'Custom__c'; 46 | sobject.async(function() { 47 | sobject._model.idAttribute.should.eql('Id'); 48 | done(); 49 | }); 50 | }); 51 | it('should have idAttribute as "ExternalId" when external data sobject', function(done) { 52 | sobject.sobject = 'External__x'; 53 | sobject.async(function() { 54 | sobject._model.idAttribute.should.eql('ExternalId'); 55 | done(); 56 | }); 57 | }); 58 | }); 59 | 60 | describe('#fieldlist', function() { 61 | it('should create fields hashmap for all fields specified in fieldlist', function(done) { 62 | sobject.sobject = 'account'; 63 | sobject.recordid = '001000fakeid'; 64 | sobject.fieldlist = 'Name BillingCity Phone'; 65 | sobject.async(function() { 66 | sobject.fields.should.have.properties('Name', 'BillingCity', 'Phone'); 67 | done(); 68 | }); 69 | }); 70 | }); 71 | 72 | describe('#autosync', function() { 73 | it('should auto fetch data when sobject and recordid are set', function(done) { 74 | sobject.sobject = 'account'; 75 | sobject.recordid = '001000fakeid'; 76 | sobject.autosync = true; 77 | Force.forcetkClient.impl.ajax = function(path, callback, error) { 78 | done(); 79 | } 80 | Platform.flush(); 81 | }); 82 | it('should not auto fetch data when autosync is false', function(done) { 83 | sobject.sobject = 'account'; 84 | sobject.recordid = '001000fakeid'; 85 | Force.forcetkClient.impl.ajax = function(path, callback, error) { 86 | false.should.be.ok; //throw error 87 | } 88 | sobject.async(done); 89 | }); 90 | }); 91 | 92 | describe('#fetch()', function() { 93 | it('should trigger "invalid" event and return self when sobject type is not defined', function(done) { 94 | sobject.addEventListener('invalid', function() { 95 | done(); 96 | }); 97 | sobject.fetch().should.be.equal(sobject); 98 | 99 | }); 100 | it('should trigger "invalid" event and return self when recordid is not defined', function(done) { 101 | sobject.sobject = 'account'; 102 | sobject.addEventListener('invalid', function() { 103 | done(); 104 | }); 105 | sobject.fetch().should.be.equal(sobject); 106 | 107 | }); 108 | it('should throw error event when xhr fails', function(done) { 109 | sobject.sobject = 'account'; 110 | sobject.recordid = '001000fakeid'; 111 | Force.forcetkClient.impl.ajax = function(path, callback, error) { 112 | error(); 113 | }; 114 | sobject.addEventListener('error', function() { 115 | done(); 116 | }); 117 | sobject.fetch(); 118 | }); 119 | it('should throw change event when model changes', function(done) { 120 | sobject.sobject = 'account'; 121 | sobject.recordid = '001000fakeid'; 122 | sobject.addEventListener('change', function() { 123 | sobject.fields.Name.should.eql('Mock Account'); 124 | done(); 125 | }); 126 | sobject.async(function() { 127 | sobject._model.set('Name', 'Mock Account'); 128 | }); 129 | }); 130 | it('should fetch data and throw sync event when sobject and recordid are set', function(done) { 131 | sobject.sobject = 'account'; 132 | sobject.recordid = '001000fakeid'; 133 | Force.forcetkClient.impl.ajax = function(path, callback, error) { 134 | callback({ 135 | attributes: {type: this.sobject}, 136 | Id: '001000fakeid', 137 | Name: 'Mock Account' 138 | }); 139 | }; 140 | sobject.addEventListener('sync', function() { 141 | sobject.fields.Name.should.eql('Mock Account'); 142 | done(); 143 | }); 144 | sobject.fetch(); 145 | }); 146 | }); 147 | 148 | describe('#save()', function() { 149 | it('should trigger "invalid" event and return self when sobject type is not defined', function(done) { 150 | sobject.addEventListener('invalid', function() { 151 | done(); 152 | }); 153 | sobject.save().should.be.equal(sobject); 154 | 155 | }); 156 | it('should throw error event when xhr fails', function(done) { 157 | sobject.sobject = 'account'; 158 | Force.forcetkClient.impl.ajax = function(path, callback, error) { 159 | error(); 160 | }; 161 | sobject.addEventListener('error', function() { 162 | done(); 163 | }); 164 | sobject.save(); 165 | }); 166 | it('should update record id when insert is successful', function(done) { 167 | sobject.sobject = 'account'; 168 | Force.forcetkClient.impl.ajax = function(path, callback, error) { 169 | callback({ 170 | id: '001000fakeid', 171 | success: true 172 | }); 173 | }; 174 | sobject.addEventListener('sync', function() { 175 | sobject.fields.Id.should.eql('001000fakeid'); 176 | sobject.recordid.should.eql('001000fakeid'); 177 | done(); 178 | }); 179 | sobject.save(); 180 | }); 181 | it('should throw sync event when update is successful', function(done) { 182 | sobject.sobject = 'account'; 183 | sobject.recordid = '001000fakeid'; 184 | Force.forcetkClient.impl.ajax = function(path, callback, error) { 185 | callback({ 186 | id: '001000fakeid', 187 | success: true 188 | }); 189 | }; 190 | sobject.addEventListener('sync', function() { 191 | done(); 192 | }); 193 | sobject.save(); 194 | }); 195 | it('should save only specified fields when fieldlist is specified on save', function(done) { 196 | sobject.sobject = 'account'; 197 | sobject.recordid = '001000fakeid'; 198 | Force.forcetkClient.impl.ajax = function(path, callback, error, method, payload) { 199 | payload = eval('(' + payload + ')'); 200 | payload.should.have.keys('BillingCity'); 201 | payload.should.have.property('BillingCity', 'San Francisco'); 202 | done(); 203 | }; 204 | sobject.async(function() { 205 | sobject._model.set({'Name': 'Mock Account', 'BillingCity': 'San Francisco'}) 206 | sobject.save({fieldlist: ['BillingCity']}); 207 | }); 208 | }); 209 | it('should save only changed fields when no fieldlist is specified', function(done) { 210 | sobject.sobject = 'account'; 211 | sobject.recordid = '001000fakeid'; 212 | Force.forcetkClient.impl.ajax = function(path, callback, error, method, payload) { 213 | payload = eval('(' + payload + ')'); 214 | payload.should.have.keys('BillingCity'); 215 | payload.should.have.property('BillingCity', 'San Francisco'); 216 | done(); 217 | }; 218 | sobject.async(function() { 219 | sobject._model.set({'Name': 'Mock Account'}); 220 | sobject._changedAttributes.should.containEql('Name'); 221 | // Reset changedAttributes to track future changes 222 | sobject._changedAttributes = []; 223 | sobject._model.set({'BillingCity': 'San Francisco'}); 224 | sobject.save(); 225 | }); 226 | }); 227 | }); 228 | 229 | describe('#destroy()', function() { 230 | it('should trigger "invalid" event and return self when sobject type is not defined', function(done) { 231 | sobject.addEventListener('invalid', function() { 232 | done(); 233 | }); 234 | sobject.destroy().should.be.equal(sobject); 235 | 236 | }); 237 | it('should trigger "invalid" event and return self when recordid is not defined', function(done) { 238 | sobject.sobject = 'account'; 239 | sobject.addEventListener('invalid', function() { 240 | done(); 241 | }); 242 | sobject.destroy().should.be.equal(sobject); 243 | 244 | }); 245 | it('should throw error event when xhr fails', function(done) { 246 | sobject.sobject = 'account'; 247 | sobject.recordid = '001000fakeid'; 248 | Force.forcetkClient.impl.ajax = function(path, callback, error) { 249 | error(); 250 | }; 251 | sobject.addEventListener('error', function() { 252 | done(); 253 | }); 254 | sobject.destroy(); 255 | }); 256 | it('should throw destroy event when delete is successful', function(done) { 257 | sobject.sobject = 'account'; 258 | sobject.recordid = '001000fakeid'; 259 | Force.forcetkClient.impl.ajax = function(path, callback, error) { 260 | callback({ 261 | id: '001000fakeid', 262 | success: true 263 | }); 264 | }; 265 | sobject.addEventListener('destroy', function() { 266 | done(); 267 | }); 268 | sobject.destroy(); 269 | }); 270 | }); 271 | }); -------------------------------------------------------------------------------- /elements/force-signin/force-signin.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 29 | 30 | 31 | 32 | 33 | 265 | 266 | -------------------------------------------------------------------------------- /elements/css/styles.css: -------------------------------------------------------------------------------- 1 | html, body, div, span, applet, object, iframe, 2 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 3 | a, abbr, acronym, address, big, cite, code, 4 | del, dfn, em, img, ins, kbd, q, s, samp, 5 | small, strike, strong, sub, sup, tt, var, 6 | b, u, i, center, 7 | dl, dt, dd, ol, ul, li, 8 | fieldset, form, label, legend, 9 | table, caption, tbody, tfoot, thead, tr, th, td, 10 | article, aside, canvas, details, embed, 11 | figure, figcaption, footer, header, hgroup, 12 | menu, nav, output, ruby, section, summary, 13 | time, mark, audio, video { 14 | margin: 0; 15 | padding: 0; 16 | border: 0; 17 | font: inherit; 18 | font-size: 100%; 19 | vertical-align: baseline; } 20 | 21 | html { 22 | line-height: 1; } 23 | 24 | ol, ul { 25 | list-style: none; } 26 | h1 { 27 | font-size: 1.75rem; } 28 | 29 | h2 { 30 | font-size: 1.25rem; } 31 | 32 | h3 { 33 | font-size: 1.125rem; } 34 | 35 | h4 { 36 | font-size: 1rem; } 37 | 38 | p { 39 | font-size: 0.875rem; } 40 | 41 | @font-face { 42 | font-family: 'ProximaNovaSoft-Regular'; 43 | src: url("ProximaNovaSoft-Regular.otf"); 44 | font-weight: normal; 45 | font-style: normal; } 46 | 47 | @font-face { 48 | font-family: 'ProximaNovaSoft-Semibold'; 49 | src: url("ProximaNovaSoft-Semibold.otf"); 50 | font-weight: normal; 51 | font-style: normal; } 52 | 53 | @font-face { 54 | font-family: 'ProximaNovaSoft-Medium'; 55 | src: url("ProximaNovaSoft-Medium.otf"); 56 | font-weight: normal; 57 | font-style: normal; } 58 | 59 | @font-face { 60 | font-family: 'ProximaNovaSoft-Bold'; 61 | src: url("ProximaNovaSoft-Bold.otf"); 62 | font-weight: normal; 63 | font-style: normal; } 64 | 65 | html { 66 | font-size: 100%; } 67 | 68 | body { 69 | font-family: "ProximaNovaSoft-Regular", Arial, sans-serif; 70 | color: #7e7f80; 71 | -webkit-font-smoothing: antialiased; 72 | /* Disable tap overlay color on links - set alpha to %0 = invisible */ 73 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 74 | } 75 | 76 | h1, h2, h3, h4, h5 { 77 | color: #454747; } 78 | 79 | p { 80 | color: #525354; } 81 | 82 | a { 83 | display: inline-block; 84 | text-decoration: none; } 85 | a:link { 86 | color: #2d95d6; } 87 | a:visited { 88 | color: #2d95d6; } 89 | a:hover { 90 | color: #2584bf; } 91 | a:active { 92 | color: #2d95d6; } 93 | 94 | 95 | .sf-layout-item, 96 | .input-block { 97 | position: relative; 98 | padding-left: 0.875em; 99 | padding-right: 0.875em; 100 | margin-bottom: 0.875em; } 101 | .input-block.picklist--publisher label { 102 | position: absolute; 103 | z-index: 2; 104 | padding: 0.875em; } 105 | .input-block.picklist--publisher input.input-picklist { 106 | padding-left: 3.6em; } 107 | .input-block.poll-choice { 108 | padding-right: 2.5em; } 109 | .input-block.poll-choice:after { 110 | position: absolute; 111 | right: 8px; 112 | top: 13px; 113 | color: #969899; 114 | font-family: "SSPika"; 115 | font-style: normal; 116 | font-weight: normal; 117 | text-decoration: none; 118 | text-rendering: optimizeLegibility; 119 | white-space: nowrap; 120 | -webkit-font-feature-settings: "liga"; 121 | -moz-font-feature-settings: "liga=1"; 122 | -moz-font-feature-settings: "liga"; 123 | -ms-font-feature-settings: "liga" 1; 124 | -o-font-feature-settings: "liga"; 125 | font-feature-settings: "liga"; 126 | -webkit-font-smoothing: antialiased; 127 | content: 'delete'; } 128 | 129 | .sf-layout-item-label, 130 | .input-label { 131 | /*font-family: "ProximaNovaSoft-Regular";*/ 132 | margin-bottom: 0.25em; } 133 | 134 | .sf-layout-item-label, 135 | label { 136 | color: #969899; 137 | font-size: 0.875rem; } 138 | label.input-label--checkbox, label.input-label--radio { 139 | color: #525354; } 140 | 141 | .sf-layout-item-value { 142 | display: -webkit-box; } 143 | 144 | .sf-layout-item-value, 145 | .sf-layout-item-value input, 146 | .sf-layout-item-value select, 147 | .sf-layout-item-value textarea { 148 | display: -webkit-box; 149 | -webkit-box-flex: 1.0; 150 | margin: 0px; 151 | -moz-box-sizing: border-box; 152 | -webkit-box-sizing: border-box; 153 | box-sizing: border-box; 154 | -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 1, 0.04); 155 | -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 1, 0.04); 156 | box-shadow: inset 0 1px 2px rgba(0, 0, 1, 0.04); 157 | border: 1px solid #ced0d2; 158 | border-radius: 5px; 159 | padding: .7em .7em .5em; 160 | /*font-family: "ProximaNovaSoft-Regular";*/ 161 | color: #525354; 162 | font-size: 1rem; 163 | background-color: #e9e9eb; } 164 | input:focus, textarea:focus { 165 | background-color: white; 166 | border: 1px solid #b9babc; 167 | box-shadow: none; 168 | outline: none; } 169 | input[disabled], textarea[disabled] { 170 | background-color: #e5e6e7; 171 | border: none; 172 | color: #969899; } 173 | 174 | .edit .sf-layout-item-value span { 175 | display: -webkit-box; 176 | margin: 0.1em; 177 | } 178 | .edit .sf-layout-item-value span:first-child { 179 | margin-left: 0; } 180 | .edit .sf-layout-item-value span:last-child { 181 | -webkit-box-flex: 1.0; } 182 | 183 | .sf-layout-item-value, 184 | input[class*="readonly"], 185 | textarea[class*="readonly"] { 186 | margin: 0; 187 | padding: 0; 188 | background: none; 189 | background-color: transparent; 190 | -webkit-box-shadow: none; 191 | -moz-box-shadow: none; 192 | box-shadow: none; 193 | border: none; } 194 | 195 | .sf-layout-item-value select, input[class*="picklist"] { 196 | background: white -webkit-gradient(linear, 50% 100%, 50% 0%, color-stop(0%, #ededed), color-stop(100%, #f7f7f7)); 197 | background: white -webkit-linear-gradient(bottom, #ededed 0%, #f7f7f7 100%); 198 | background: white -moz-linear-gradient(bottom, #ededed 0%, #f7f7f7 100%); 199 | background: white -o-linear-gradient(bottom, #ededed 0%, #f7f7f7 100%); 200 | background: white linear-gradient(bottom, #ededed 0%, #f7f7f7 100%); 201 | border: 1px solid #dbdbdb; 202 | -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04), inset 0 0 0 rgba(255, 255, 255, 0.75); 203 | -moz-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04), inset 0 0 0 rgba(255, 255, 255, 0.75); 204 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04), inset 0 0 0 rgba(255, 255, 255, 0.75); 205 | font-size: 15px; 206 | -webkit-appearance: none} 207 | select:active, input[class*="picklist"]:active { 208 | border: 1px solid #dbdbdb; 209 | background: #e9e9e9; 210 | background-image: none; 211 | -webkit-box-shadow: none; 212 | -moz-box-shadow: none; 213 | box-shadow: none; } 214 | 215 | input[type="checkbox"], 216 | input[type="radio"] { 217 | position: relative; 218 | -webkit-appearance: none; 219 | display: inline-block; 220 | height: 22px; 221 | margin: 0 6px 0 0; 222 | border: 1px solid #c3c6c9; 223 | padding: 0; 224 | width: 22px; 225 | vertical-align: middle; 226 | background: white -webkit-linear-gradient(bottom, rgba(0, 1, 1, 0.05) 0%, rgba(255, 255, 255, 0.05) 100%); 227 | box-shadow: 0 1px 0 rgba(0, 0, 0, 0.05), inset 0 0 1px 1px white; } 228 | input[type="checkbox"]:focus, input[type="checkbox"]:active, 229 | input[type="radio"]:focus, 230 | input[type="radio"]:active { 231 | background-color: #eee; } 232 | input[type="checkbox"][class*="disabled"], 233 | input[type="radio"][class*="disabled"] { 234 | opacity: 0.4; } 235 | input[type="checkbox"]:checked, 236 | input[type="radio"]:checked { 237 | border: 1px solid #2c75a3; 238 | background-color: #3b9fdd; 239 | -webkit-box-shadow: 0 0 2px rgba(0, 0, 0, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.2); 240 | -moz-box-shadow: 0 0 2px rgba(0, 0, 0, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.2); 241 | box-shadow: 0 0 2px rgba(0, 0, 0, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.2); 242 | background-image: -webkit-gradient(linear, 50% 100%, 50% 0%, color-stop(0%, #3b9fdd), color-stop(100%, #3b9fdd)); 243 | background-image: -webkit-linear-gradient(bottom, #3b9fdd 0%, #3b9fdd 100%); 244 | background-image: -moz-linear-gradient(bottom, #3b9fdd 0%, #3b9fdd 100%); 245 | background-image: -o-linear-gradient(bottom, #3b9fdd 0%, #3b9fdd 100%); background-image: linear-gradient(bottom, #3b9fdd 0%, #3b9fdd 100%); } 246 | input[type="checkbox"]:checked::after, 247 | input[type="radio"]:checked::after { 248 | position: absolute; 249 | content: ''; } 250 | 251 | input[type="checkbox"]:checked::after { 252 | left: 4px; 253 | top: 5px; 254 | height: 18%; 255 | width: 10px; 256 | border-bottom: 4px solid white; 257 | border-left: 4px solid white; 258 | /*box-shadow: -1px 1px 1px rgba(0, 0, 0, .1);*/ 259 | -webkit-transform: rotate(-45deg); } 260 | 261 | input[type="radio"] { 262 | border-radius: 11px; } 263 | input[type="radio"]:checked::after { 264 | left: 6px; 265 | top: 6px; 266 | height: 10px; 267 | width: 10px; 268 | /*border: 1px solid #455463;*/ 269 | border-radius: 100%; 270 | background: white; 271 | /*box-shadow: 0 0 2px rgba(0,0,0,.3), inset 0 1px 0 rgba(255,255,255,.2);*/ } 272 | 273 | .page-header--details { 274 | padding: 0.875em; 275 | margin-bottom: -15px; 276 | background-color: white; 277 | border-bottom: 2px solid #dadbdc; 278 | text-align: left; } 279 | .page-header--details .item-header-details { 280 | float: none; } 281 | .page-header--details .item-header h1, .page-header--details .item-header--small h1 { 282 | margin-bottom: .15em; 283 | font-size: 1.5em; 284 | font-family: 'ProximaNovaSoft-Semibold'; } 285 | .page-header--details .item-header h2, .page-header--details .item-header--small h2 { 286 | color: #969899; 287 | font-size: 1.1em; 288 | font-family: 'ProximaNovaSoft-Medium'; } 289 | .page-header--details .item-header-details { 290 | margin: 0; } 291 | .page-header--details .item-header h1, .page-header--details .item-header--small h1 { 292 | color: #454747; } 293 | 294 | 295 | /*@ sourceMappingURL=styles.css.map */ 296 | /* Localized */ 297 | 298 | .sf-list-item { 299 | font-family: "ProximaNovaSoft-Regular"; 300 | color: #525354 301 | } 302 | 303 | .sf-layout-item-error { 304 | color: red; 305 | font-size: 0.9em; 306 | font-family: "ProximaNovaSoft-Regular"; 307 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAYAAAA7MK6iAAAC90lEQVRIx7WXTWwSQRTHsVWPxngxPfmZNEZ78ubFePXQHky8eDIxRuNBe1OhtCwsVKSmtiDaJg0NiSbqRTEaE9OkrakxUalCWb6kEAWCgJRWJTS243sLi6xAdwbqJC+zXzO/mXn/efNWpWqixM1D7SGD6ZBPZzgq6Az7VJtZSORr5RoAh8EugrnAfoGROvZF4PhxqE+DdbQGH5vYCjNyQEfLDWCNLAntepuC+jn+JHTwmxFYMaFUh8MGUyf2l7k1ogyFBqZmgXUGkIW6W1zB+LfGUPiwb7Og1RbQG481FBIs7wn4qLhhJwN64lVrawyfK8AXg3pjR10hwcuCEjQ8aiOr+TwpZjIVw/uw1a4IB9W7yhOU+fWx4pIh2HqH1CsLWh2dzzn+eAUK698Jvs3RNERAveK5qqEVm+e9WrutJCiOv0ArEs81TQ10Nb9MvNe1tEL7EZSEBqN4KlCCUUjF7HcZuJhOlwRGv8WuSP7NMoHTGRk4/8lDFvoGWLbXQ4RuFxj2IwKWAFRdlj7Ms4LdCN7vYwW7P8rAqVdT1KouL3USwV1M4H6OxJ+4ZGC8x+e0MRzDKIJ3s4ITrmcycGzSSRO5quExBLexxt3IvXEZGIMKy6kF23daUnWABYzhUQYesbGBdQarBLYxnTTmIZKZe0Myr+dIenqW+AdvMq0YxOozErib9dAPmC0kCr7FmtFVaZjxQRH8mR/ciQ6naggiijqcZH1tTVxmrBcnHNTiEnT8c3kCwPHnaSNXIZGU+XglECReTT9NKkSCBtPfM5nr6ZHgM0pRDAErobAMnHO7qSIX9H9JPPsfPKpJ8HbBS7/ymWyHWSdE6M9olIRuW6ni8zu1ti1lGW6Qc3H8kf+Qc8U2zqNfTkkq3wsDmG85w+R4tPtwvUUxtY0Yb4g1LMsO8Le9RfC5F5d72//9M6FN7A9AJzM+utRoHSwFA54kDqcIzA2Psv9NFGx3q5PBLhjEWZjFWDkxfIuhFiCzKBx4bgE7Bdd7aPr+A/AoPXcSPsT7AAAAAElFTkSuQmCC); 308 | background-repeat: no-repeat; 309 | padding-left: 15px; 310 | background-size: 12px; 311 | word-wrap: break-word; 312 | } 313 | 314 | .ui-header, .ui-footer { 315 | background-color: #3291db; 316 | background-image: -webkit-linear-gradient(bottom, rgba(0, 0, 0, 0.07) 0%, rgba(255, 255, 255, 0.07) 100%); 317 | background-image: linear-gradient(bottom, rgba(0, 0, 0, 0.07) 0%, rgba(255, 255, 255, 0.07) 100%); 318 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.1); 319 | -moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.1); 320 | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.1); } 321 | 322 | .ui-header a { 323 | text-decoration: none; 324 | font-size: 1.3em; 325 | color: inherit; } 326 | .ui-header a:before { 327 | line-height: 0; 328 | vertical-align: -6px; } 329 | 330 | .ui-title { 331 | color: white; 332 | font-family: "ProximaNovaSoft-Regular"; 333 | } 334 | 335 | .sf-layout-section-heading { 336 | color: rgb(156, 161, 165); 337 | font-size: 0.9em; 338 | margin: 15px 0px; 339 | text-transform: uppercase; 340 | padding: 0 15px; 341 | background-color: rgb(228, 231, 231); 342 | line-height: 20px; 343 | font-weight: bold; 344 | } 345 | 346 | .edit .ui-content, 347 | #detail .ui-content { 348 | padding: 0px; 349 | } 350 | 351 | .absolute { 352 | position: absolute; 353 | top: 0; bottom: 0; 354 | left: 0; right: 0;} -------------------------------------------------------------------------------- /elements/force-sobject/force-sobject.js: -------------------------------------------------------------------------------- 1 | (function(SFDC) { 2 | 3 | var createModel = function(sobject) { 4 | sobject = sobject.toLowerCase(); 5 | 6 | return new (Force.SObject.extend({ 7 | sobjectType: sobject.toLowerCase(), 8 | idAttribute: sobject.search(/__x$/) > 0 ? 'ExternalId' : 'Id' 9 | })); 10 | } 11 | 12 | var SObjectViewModel = function(model) { 13 | var _self = this; 14 | 15 | var setupProps = function(props) { 16 | props.forEach(function(prop) { 17 | Object.defineProperty(_self, prop, { 18 | get: function() { 19 | return model.get(prop); 20 | }, 21 | set: function(val) { 22 | model.set(prop, val); 23 | }, 24 | enumerable: true 25 | }); 26 | }); 27 | } 28 | // Review all fields in fieldlist to pick the first part of the reference fields. 29 | // eg. for "Owner.Name" pick "Owner" 30 | var addFields = _.map(model.fieldlist, function(prop) { return prop.split('.')[0]; }); 31 | // Create object map 32 | setupProps(_.union(_.keys(model.attributes), addFields)); 33 | 34 | // Setup an event listener to update object map when fieldlist changes on model 35 | model.on('change', function() { 36 | setupProps(_.difference(_.keys(model.attributes), _.keys(_self))); 37 | }); 38 | } 39 | 40 | function processFieldlist(fieldlist) { 41 | if (typeof fieldlist === 'string') 42 | return fieldlist.trim().split(/\s+/); 43 | else 44 | return fieldlist; 45 | } 46 | 47 | Polymer({ 48 | is: 'force-sobject', 49 | properties: { 50 | /** 51 | * (Required) Name of Salesforce sobject against which CRUD operations will be performed. 52 | * 53 | * @attribute sobject 54 | * @type String 55 | */ 56 | sobject: String, 57 | 58 | /** 59 | * (Required) Id of the record on which CRUD operations will be performed. 60 | * 61 | * @attribute recordid 62 | * @type String 63 | */ 64 | recordid: String, 65 | 66 | /** 67 | * (Optional) List of field names that need to be fetched for the record. 68 | * Provide a space delimited list. Also the field names are case sensitive. 69 | * 70 | * @attribute fieldlist 71 | * @type String 72 | * @default All fields 73 | */ 74 | fieldlist: { 75 | type: String, 76 | value: null 77 | }, 78 | 79 | /** 80 | * (Optional) Auto synchronize (fetch/save) changes to the model with the remote server/local store. 81 | * If false, use fetch/save methods to commit changes to server or local store. (TBD: autosync not working for "save" operations) 82 | * 83 | * @attribute autosync 84 | * @type Boolean 85 | * @default false 86 | */ 87 | autosync: String, 88 | 89 | /** 90 | * (Optional) The cache mode (server-first, server-only, cache-first, cache-only) to use during CRUD operations. 91 | * 92 | * @attribute cachemode 93 | * @type String 94 | * @default SFDC.cacheMode() 95 | */ 96 | cachemode: { 97 | type: String, 98 | value: function() { return SFDC.cacheMode(); } 99 | }, 100 | 101 | /** 102 | * (Optional) The merge mode to use when saving record changes to salesforce. 103 | * 104 | * @attribute mergemode 105 | * @type String 106 | * @default Force.MERGE_MODE.OVERWRITE 107 | */ 108 | mergemode: { 109 | type: String, 110 | value: Force.MERGE_MODE.OVERWRITE 111 | }, 112 | 113 | /** 114 | * Returns a map of fields to values for a specified record. Update this map to change SObject field values. 115 | * 116 | * @attribute fields 117 | * @type Object 118 | */ 119 | fields: { 120 | type: Object, 121 | notify: true 122 | } 123 | }, 124 | 125 | observers: [ 126 | "_init(sobject, recordid, fieldlist)", 127 | "_updateCacheMode(cachemode)" 128 | ], 129 | 130 | /** 131 | * Initiate the fetching of record data from the relevant data store (server/offline store). 132 | * 133 | * @method fetch 134 | */ 135 | fetch: function(opts) { 136 | 137 | var operation = function() { 138 | var model = this._model; 139 | if (model && model.id) { 140 | this._whenModelReady().then(function() { 141 | model.fetch(opts); 142 | }); 143 | } else if (!this.autosync) { 144 | //if sync was not auto initiated, trigger a 'invalid' event 145 | this.fire('invalid', 'sobject Type and recordid required for fetch.'); 146 | } 147 | } 148 | // Queue the operation for next cycle after all change watchers are fired. 149 | this.async(operation.bind(this)); 150 | return this; 151 | }, 152 | 153 | /** 154 | * Initiate the saving of record data to the relevant data store (server/offline store). 155 | * If the model was modified locally, it saves all the updateable fields on the sobject back to server. 156 | * If fieldlist property is specified on the options, only the specified fields are included during the save operation. 157 | * 158 | * @method save 159 | */ 160 | save: function(options) { 161 | var timingtag = Date.now() + ':force-sobject:save:' + this.id; 162 | console.time(timingtag); 163 | console.log(timingtag); 164 | console.log(JSON.stringify(this.fields)); 165 | console.trace(); 166 | 167 | var operation = function() { 168 | var that = this, 169 | model = that._model, 170 | changedAttributes = this._changedAttributes; 171 | 172 | options = _.extend({ mergeMode: this.mergemode }, options); 173 | 174 | var successCB = options.success; 175 | options.success = function() { 176 | that.recordid = model.id; 177 | that.fire('save'); 178 | if (successCB) successCB(arguments); 179 | } 180 | 181 | var getEditableFieldList = function() { 182 | return SFDC.getSObjectType(that.sobject) 183 | .describe() 184 | .then(function(describeResult) { 185 | 186 | return _.pluck(_.filter(describeResult.fields, function(fieldInfo) { 187 | return fieldInfo.updateable; 188 | }), "name"); 189 | }); 190 | } 191 | 192 | if (model) { 193 | 194 | // Setup the fieldlist for save operation 195 | this._whenModelReady().then(function() { 196 | 197 | // Check if fieldlist is not specified. If not, then generate that list based on changed attributes. 198 | if (!options.fieldlist) { 199 | options.fieldlist = changedAttributes; 200 | 201 | // Check if the record was modified locally and the current save operation is not cache only. 202 | // If yes, we will need to send all the updateable fields for save to server. 203 | var cacheMode = options.cacheMode || model.cacheMode; 204 | cacheMode = _.isFunction(cacheMode) ? cacheMode('save') : cacheMode; 205 | 206 | if (model.get('__local__') && cacheMode != Force.CACHE_MODE.CACHE_ONLY) { 207 | // Get all the updatable fields and union them with the list provided by the user. 208 | return getEditableFieldList().then(function(fieldlist) { 209 | options.fieldlist = _.union( 210 | _.intersection(_.keys(model.attributes), fieldlist), 211 | options.fieldlist || [] 212 | ); 213 | }); 214 | } 215 | } 216 | }).then(function() { 217 | // During create, add the attibutes field in the fieldlist for save. 218 | // We use attributes property to index data offline 219 | if (model.isNew()) 220 | options.fieldlist = _.union(['attributes'], options.fieldlist); 221 | // Perform save (upsert) against the server 222 | model.save(null, options); 223 | }); 224 | } else if (!this.autosync) { 225 | //if sync was not auto initiated, trigger a 'invalid' event 226 | this.fire('invalid', 'sobject Type required for save.'); 227 | } 228 | } 229 | 230 | // Queue the operation for next cycle after all change watchers are fired. 231 | this.async(operation.bind(this)); 232 | return this; 233 | }, 234 | 235 | /** 236 | * Initiate the deleting of record data from the relevant data store (server/offline store). 237 | * 238 | * @method destroy 239 | */ 240 | destroy: function(options) { 241 | 242 | var operation = function() { 243 | var model = this._model; 244 | options = _.extend({mergeMode: this.mergemode, wait: true}, options); 245 | if (model && model.id) { 246 | this._whenModelReady().then(function() { 247 | // Perform delete of record against the server 248 | model.destroy(options); 249 | }); 250 | } else if (!this.autosync) { 251 | //if sync was not auto initiated, trigger a 'invalid' event 252 | this.fire('invalid', 'sobject Type and recordid required for delete.'); 253 | } 254 | } 255 | 256 | // Queue the operation for next cycle after all change watchers are fired. 257 | this.async(operation.bind(this)); 258 | return this; 259 | }, 260 | 261 | // Resets all the properties on the model. 262 | // Recreates model if sobject type or id of model has changed. 263 | _init: function() { 264 | var that = this, 265 | model; 266 | 267 | if (this.sobject && typeof this.sobject === 'string') { 268 | that._changedAttributes = []; 269 | model = this._model = createModel(this.sobject); 270 | model.cacheMode = this.cachemode; 271 | model.fieldlist = processFieldlist(this.fieldlist); 272 | model.set(model.idAttribute, this.recordid); 273 | model.set({attributes: {type: this.sobject}}); 274 | model.on('all', function(event) { 275 | switch(event) { 276 | case 'change': 277 | var changedFields = _.keys(model.changedAttributes()); 278 | changedFields = changedFields.filter(function(field) { 279 | return field.indexOf('__') != 0; 280 | }) 281 | that._changedAttributes = _.union(that._changedAttributes, changedFields); 282 | break; 283 | case 'sync': that._changedAttributes = []; 284 | } 285 | that.fire(event); 286 | }); 287 | 288 | this.fields = new SObjectViewModel(model); 289 | if (this.autosync) this.fetch(); 290 | } 291 | }, 292 | // All CRUD operations should ensure that the model is ready by checking this promise. 293 | _whenModelReady: function() { 294 | var model = this._model; 295 | var store = this.$.store; 296 | return $.when(store.cacheReady, SFDC.launcher) 297 | .then(function() { 298 | model.cache = store.cache; 299 | model.cacheForOriginals = store.cacheForOriginals; 300 | }); 301 | }, 302 | _updateCacheMode: function() { 303 | if (this._model) this._model.cacheMode = this.cachemode; 304 | } 305 | }); 306 | 307 | })(window.SFDC); 308 | -------------------------------------------------------------------------------- /tests/force-sobject-collection.test.js: -------------------------------------------------------------------------------- 1 | describe('force-sobject-collection', function() { 2 | 3 | describe('Online', function() { 4 | 5 | var sobjectColl; 6 | var origAjax; 7 | var smartstore; 8 | 9 | /* Disable smartstore for this testsuite */ 10 | before(function() { 11 | smartstore = navigator.smartstore; 12 | navigator.smartstore = undefined; 13 | }); 14 | 15 | after(function() { 16 | navigator.smartstore = smartstore; 17 | }); 18 | 19 | beforeEach(function() { 20 | sobjectColl = document.createElement('force-sobject-collection'); 21 | sobjectColl.autosync = false; 22 | origAjax = Force.forcetkClient.impl.ajax; 23 | }); 24 | 25 | afterEach(function() { 26 | Force.forcetkClient.impl.ajax = origAjax; 27 | }); 28 | 29 | describe('#collection', function() { 30 | it('should always be defined', function(){ 31 | sobjectColl.collection.should.be.ok.and.be.an.instanceOf(Force.SObjectCollection); 32 | }); 33 | it('should have undefined config when sobject type is not defined', function(){ 34 | sobjectColl.collection.should.not.have.property('config'); 35 | }); 36 | it('should be empty when sobject type is not defined', function(){ 37 | sobjectColl.collection.should.have.length(0); 38 | }); 39 | it('should not fetch data when sobject type is not defined', function(done) { 40 | sobjectColl.autosync = true; 41 | Force.forcetkClient.impl.ajax = function(path, callback, error) { 42 | false.should.be.ok; //throw error 43 | } 44 | sobjectColl.async(done); 45 | }); 46 | }); 47 | 48 | describe('#autosync', function() { 49 | it('should auto fetch data when sobject and query options set', function(done) { 50 | sobjectColl.sobject = 'account'; 51 | sobjectColl.querytype = 'mru'; 52 | sobjectColl.autosync = true; 53 | Force.forcetkClient.impl.ajax = function(path, callback, error) { 54 | done(); 55 | } 56 | Platform.flush(); 57 | }); 58 | it('should not auto fetch data when autosync is false', function(done) { 59 | sobjectColl.sobject = 'account'; 60 | sobjectColl.recordid = '001000fakeid'; 61 | Force.forcetkClient.impl.ajax = function(path, callback, error) { 62 | false.should.be.ok; //throw error 63 | } 64 | sobjectColl.async(done); 65 | }); 66 | }); 67 | 68 | describe('#querytype', function() { 69 | it('should not fetch data when querytype is not defined', function() { 70 | sobjectColl.sobject = 'account'; 71 | Force.forcetkClient.impl.ajax = function(path, callback, error) { 72 | false.should.be.ok; //throw error 73 | } 74 | sobjectColl.fetch(); 75 | }); 76 | it('should fetch recent items when querytype is mru', function(done) { 77 | sobjectColl.sobject = 'account'; 78 | sobjectColl.querytype = 'mru'; 79 | Force.forcetkClient.impl.ajax = function(path, callback, error) { 80 | path.should.endWith('/sobjects/account/'); 81 | callback({ 82 | recentItems: [{ 83 | attributes: {type: 'account'}, 84 | Id: 'mockid', 85 | Name: 'mockname' 86 | }] 87 | }); 88 | } 89 | sobjectColl.addEventListener('sync', function() { 90 | sobjectColl.collection.should.have.length(1); 91 | sobjectColl.collection.models[0].id.should.eql('mockid'); 92 | sobjectColl.collection.models[0].attributes.should.have.property('Name', 'mockname'); 93 | done(); 94 | }); 95 | sobjectColl.fetch(); 96 | }); 97 | it('should execute soql when querytype is soql', function(done) { 98 | sobjectColl.sobject = 'account'; 99 | sobjectColl.querytype = 'soql'; 100 | sobjectColl.query = "select id, name from account"; 101 | Force.forcetkClient.impl.ajax = function(path, callback, error) { 102 | path.should.endWith('/query?q=' + encodeURI(sobjectColl.query)); 103 | callback({ 104 | records: [{ 105 | attributes: {type: 'account'}, 106 | Id: 'mockid', 107 | Name: 'mockname' 108 | }], 109 | totalSize: 1 110 | }); 111 | } 112 | sobjectColl.addEventListener('sync', function() { 113 | sobjectColl.collection.should.have.length(1); 114 | sobjectColl.collection.models[0].id.should.eql('mockid'); 115 | sobjectColl.collection.models[0].attributes.should.have.property('Name', 'mockname'); 116 | done(); 117 | }); 118 | sobjectColl.fetch(); 119 | }); 120 | it('should execute sosl when querytype is sosl', function(done) { 121 | sobjectColl.sobject = 'account'; 122 | sobjectColl.querytype = 'sosl'; 123 | sobjectColl.query = "FIND {*} IN ALL FIELDS RETURNING Account (Id, Name)"; 124 | Force.forcetkClient.impl.ajax = function(path, callback, error) { 125 | path.should.endWith('/search?q=' + encodeURI(sobjectColl.query)); 126 | callback([{ 127 | attributes: {type: 'account'}, 128 | Id: 'mockid', 129 | Name: 'mockname' 130 | }]); 131 | } 132 | sobjectColl.addEventListener('sync', function() { 133 | sobjectColl.collection.should.have.length(1); 134 | sobjectColl.collection.models[0].id.should.eql('mockid'); 135 | sobjectColl.collection.models[0].attributes.should.have.property('Name', 'mockname'); 136 | done(); 137 | }); 138 | sobjectColl.fetch(); 139 | }); 140 | }); 141 | 142 | describe('#maxsize', function() { 143 | it('should fetch more items when maxsize is -1', function(done) { 144 | sobjectColl.sobject = 'account'; 145 | sobjectColl.querytype = 'soql'; 146 | sobjectColl.query = "select id, name from account"; 147 | Force.forcetkClient.impl.ajax = function(path, callback, error) { 148 | if (path.indexOf('query') >= 0) { 149 | callback({ 150 | records: [{ 151 | attributes: {type: 'account'}, 152 | Id: 'mockid1' 153 | }], 154 | totalSize: 1, 155 | nextRecordsUrl: '/getmore' 156 | }); 157 | } else if (path.indexOf('getmore') >= 0) { 158 | callback({ 159 | records: [{ 160 | attributes: {type: 'account'}, 161 | Id: 'mockid2' 162 | }], 163 | totalSize: 1 164 | }); 165 | } 166 | } 167 | sobjectColl.addEventListener('sync', function() { 168 | sobjectColl.collection.should.have.length(2); 169 | sobjectColl.collection.models[0].id.should.eql('mockid1'); 170 | sobjectColl.collection.models[1].id.should.eql('mockid2'); 171 | done(); 172 | }); 173 | sobjectColl.fetch(); 174 | }); 175 | it('should not fetch more items when maxsize is reached', function(done) { 176 | sobjectColl.sobject = 'account'; 177 | sobjectColl.querytype = 'soql'; 178 | sobjectColl.query = "select id, name from account"; 179 | sobjectColl.maxsize = 1; 180 | Force.forcetkClient.impl.ajax = function(path, callback, error) { 181 | if (path.indexOf('query') >= 0) { 182 | callback({ 183 | records: [{ 184 | attributes: {type: 'account'}, 185 | Id: 'mockid1' 186 | }], 187 | totalSize: 1, 188 | nextRecordsUrl: '/getmore' 189 | }); 190 | } else false.should.be.ok; 191 | } 192 | sobjectColl.addEventListener('sync', function() { 193 | sobjectColl.collection.should.have.length(1); 194 | sobjectColl.collection.models[0].id.should.eql('mockid1'); 195 | done(); 196 | }); 197 | sobjectColl.fetch(); 198 | }); 199 | }); 200 | 201 | describe('#fetch()', function() { 202 | it('should throw reset event when collection is fetched', function(done) { 203 | sobjectColl.sobject = 'account'; 204 | sobjectColl.querytype = 'mru'; 205 | Force.forcetkClient.impl.ajax = function(path, callback, error) { 206 | callback({ recentItems: [] }); 207 | } 208 | sobjectColl.addEventListener('reset', function() { 209 | done(); 210 | }); 211 | sobjectColl.fetch(); 212 | }); 213 | it('should throw sync event when collection is fetched', function(done) { 214 | sobjectColl.sobject = 'account'; 215 | sobjectColl.querytype = 'mru'; 216 | Force.forcetkClient.impl.ajax = function(path, callback, error) { 217 | callback({ recentItems: [] }); 218 | } 219 | sobjectColl.addEventListener('sync', function() { done(); }); 220 | sobjectColl.fetch(); 221 | }); 222 | it('should throw error event when collection fetch fails', function(done) { 223 | sobjectColl.sobject = 'account'; 224 | sobjectColl.querytype = 'mru'; 225 | Force.forcetkClient.impl.ajax = function(path, callback, error) { 226 | error(); 227 | } 228 | sobjectColl.addEventListener('error', function() { done(); }); 229 | sobjectColl.fetch(); 230 | }); 231 | }); 232 | 233 | describe('#reset()', function() { 234 | it('should empty the collection and throw reset event when reset', function(done) { 235 | sobjectColl.sobject = 'account'; 236 | sobjectColl.querytype = 'mru'; 237 | sobjectColl.collection.add({Id: 'Mock'}); 238 | sobjectColl.addEventListener('reset', function() { 239 | sobjectColl.collection.should.have.length(0); 240 | done(); 241 | }); 242 | sobjectColl.async(function() { 243 | sobjectColl.reset(); 244 | }); 245 | }); 246 | }); 247 | }); 248 | 249 | describe('Offline', function() { 250 | 251 | var isOnline; 252 | var sobjectColl; 253 | var records = [ 254 | {attributes: {type: 'account'}, Id:"007", Name:"JamesBond"}, 255 | {attributes: {type: 'account'}, Id:"008", Name:"Agent008"}, 256 | {attributes: {type: 'account'}, Id:"009", Name:"JamesOther"} 257 | ]; 258 | 259 | /* Disable isOnline for this testsuite */ 260 | before(function() { 261 | isOnline = SFDC.isOnline; 262 | SFDC.isOnline = function() { return false; } 263 | }); 264 | 265 | after(function() { 266 | SFDC.isOnline = isOnline; 267 | }); 268 | 269 | beforeEach(function(done) { 270 | sobjectColl = document.createElement('force-sobject-collection'); 271 | sobjectColl.sobject = 'account'; 272 | sobjectColl.autosync = false; 273 | 274 | var store = sobjectColl.$.store; 275 | 276 | store.cacheReady.then(function() { 277 | return store.cache.saveAll(records); 278 | }).then(function() { 279 | done(); 280 | }); 281 | }); 282 | 283 | afterEach(function(done) { 284 | sobjectColl.$.store.destroy().then(function() { 285 | done(); 286 | }); 287 | }); 288 | 289 | describe('#querytype', function() { 290 | it('should fetch all data from cache when querytype is not defined', function(done) { 291 | sobjectColl.fetch(); 292 | sobjectColl.addEventListener('sync', function() { 293 | sobjectColl.collection.should.have.length(3); 294 | sobjectColl.collection.models[0].id.should.eql('007'); 295 | sobjectColl.collection.models[1].id.should.eql('008'); 296 | sobjectColl.collection.models[2].id.should.eql('009'); 297 | done(); 298 | }); 299 | }); 300 | 301 | it('should fetch data from cache using cache query when querytype is cache and query is defined', function(done) { 302 | sobjectColl.querytype = 'cache'; 303 | sobjectColl.query = navigator.smartstore.buildExactQuerySpec('Id', '007'); 304 | sobjectColl.fetch(); 305 | sobjectColl.addEventListener('sync', function() { 306 | sobjectColl.collection.should.have.length(1); 307 | sobjectColl.collection.models[0].id.should.eql('007'); 308 | done(); 309 | }); 310 | }); 311 | }); 312 | }); 313 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mobile UI Elements (BETA) # 2 | 3 | ## Upgrade Steps ## 4 | To get the latest Polymer library and all the bug fixes, please re-run `npm install`, `bower install` and `grunt` commands. 5 | 6 | 7 | ### [Try out the Designer!!](https://sfdc-designer.herokuapp.com) 8 | ### [Watch this Designer video!!](http://youtu.be/67FjSemJ7uQ) 9 | ### [Material Design Contact Manager](https://sfdc-sobject-editor.herokuapp.com/) 10 | ### [Source Code: Material Design Contact Manager](https://github.com/ForceDotComLabs/paper-sobject-editor) 11 | 12 | 13 | Mobile UI Elements is a free, open-source Force.com (unsupported) library to simplify the development of mobile apps. The library, based on the [Google’s Polymer framework](http://www.polymer-project.org), provides the fundamental building blocks for creating HTML5 apps that run well on smartphones and tablets. The elements can be mixed together to create fairly complex force.com applications and the apps can be deployed in the browser or embedded inside Container from the Salesforce Mobile SDK. 14 | Note: The library is still in heavy development and is missing certain features as well as complete documentation. 15 | This document is intended to introduce you to the app's architecture and design and make it as easy as possible for you to jump in, run it, and start contributing. 16 | 17 | - What is it? 18 | - Setup 19 | - Available UI Elements 20 | - Third-party Code 21 | - FAQ 22 | - Mobile UI Elements License 23 | 24 | ## What is it? ## 25 | Mobile UI Elements is a set of web components built using [Google’s Polymer framework](http://www.polymer-project.org). The library utilizes the future of HTML5 standards, such as Custom Elements, ShadowDOM, Templates, HTML imports etc., to provide a set of new HTML tags that generate the Saleforce driven UI for your mobile application. It's built on top of [Salesforce Mobile SDK](http://www2.developerforce.com/en/mobile/services/mobile-sdk) and extends the open source frameworks such as [Backbone.js](http://backbonejs.org/) and [Undescore.js](http://underscorejs.org/) and [JQuery](http://jquery.com/). It also comes with some stylesheets, providing the responsive design for tablets and phones, and Sample Apps to showcase how to use them in a real application. You can easily combine and extend this library to develop UI specific to your application. 26 | 27 | ## Setup ## 28 | 29 | ### Global Dependencies 30 | 31 | Install 32 | 33 | * [node.js](http://nodejs.org) 34 | * [GitHub Client](http://mac.github.com/) (with Git Terminal option) 35 | * [Safari](http://www.apple.com/safari/) 36 | 37 | and then open Terminal: 38 | 39 | $ sudo npm install -g grunt-cli 40 | 41 | ### Project Setup 42 | 43 | $ git clone https://github.com/ForceDotComLabs/mobile-ui-elements.git 44 | $ cd mobile-ui-elements 45 | $ npm install 46 | $ bower prune (Do this if you are updating an old copy of mobile-ui-elements to remove core-bind dependency.) 47 | $ bower install 48 | 49 | To build the project, execute: 50 | 51 | $ grunt 52 | 53 | To build the project for distribution, execute (all assets will be generated in dist directory): 54 | 55 | $ grunt dist 56 | 57 | Run a local node server: 58 | 59 | $ node proxy.js 60 | 61 | You can now launch the [Sample App](http://localhost:9000/index.html). It will go through the OAuth flow to obtain user session and render data. 62 | 63 | To create a mobile sdk app, run the following command. Make sure that the [forceios](https://www.npmjs.org/package/forceios) tool is already installed: 64 | 65 | $ grunt create_app 66 | 67 | ## Available UI Elements ## 68 | 1. __force-signin__: This element allows an easy way to initiate OAuth into salesforce via web or mobile SDK. 69 | 70 | Supported attributes include: 71 | - `auto`: (Optional) Automatically trigger user authentication as soon as the component is ready. If accesstoken and instanceurl are set via attributes, OAuth will not trigger automatically. 72 | - `consumerkey`: (Optional) Consumer key for initiating OAuth based salesforce authentication. It's required only for web based applications. For SDK based applications, specify the consumer key in the bootconfig.json. 73 | - `callbackurl`: (Optional) Callback URL property for OAuth based authentication. It's required only for web based applications. For SDK based applications, specify the callback URL in the bootconfig.json. 74 | - `loginurl`: (Optional) Login Host for salesforce authentication. It's required only for web based applications. For SDK based applications, specify the login host in application settings. Default value is https://login.salesforce.com 75 | - `proxyurl`: (Optional) Custom proxy host setting for web based application. If specified, all the HTTP requests will be sent to this proxy host with "Salesforce-Endpoint" header for actual host URL. This allows cross-domain calls restricted by browsers. 76 | - `usePopupWindow`: (Optional) Set as true if you want OAuth flow to be started in a new child window. 77 | - `accesstoken`: Salesforce session ID for API requests. It is set by the component after successful completion of Salesforce OAuth. 78 | - `instanceurl`: Salesforce instance URL for API requests. It is set by the component after successful completion of Salesforce OAuth. 79 | - `id-url`: Salesforce user identity URL. It is set by the component after successful completion of Salesforce OAuth. 80 | - `userInfo`: (Read Only) Returns basic user information of currently active session. It is set by the component after successful completion of Salesforce OAuth. 81 | 82 | Methods: 83 | - `authenticate`: Authenticate the user with salesforce via OAuth. 84 | - `logout`: Initiates the logout of the current user session. 85 | 86 | Events: 87 | - `success`: when the OAuth flow is successfully completed and the accesstoken is obtained from salesforce. 88 | - `error`: when OAuth flow ends in an error. 89 | - `offline`: when the device is offline and authentication cannot complete. The UI Elements are launched with empty session in that scenario. 90 | 91 | Example (when using inside Visualforce): 92 | 93 | ``` 94 | 95 | ``` 96 | 97 | 2. __force-sobject-collection__: This element provides a custom component for `Force.SObjectCollection` from Mobile SDK's SmartSync JS. It allows apps to easily fetch a list of records from a salesforce sobject in both online & offline modes. For Offline use, the application should first create a Smartstore soup with same name as the target sobject. 98 | 99 | Supported attributes include: 100 | - `sobject`: (Required) Name of Salesforce sobject against which fetch operations will be performed. 101 | - `query`: (Optional) SOQL/SOSL/SmartSQL statement to fetch the records. Required when querytype is soql, sosl or cache. 102 | - `querytype`: (Optional) Default: mru. Type of query (mru, soql, sosl, cache). Required if query attribute is specified. 103 | - `autosync`: (Optional) Auto synchronize (fetch/save) changes to the model with the remote server/local store. If false, use fetch/save methods to commit changes to server or local store. 104 | - `maxsize`: (Optional) Default: -1. If positive, limits the maximum number of records fetched. 105 | 106 | Methods: 107 | - `fetch`: Initiates the fetching of records from the relevant data store (server/offline store). 108 | - `reset`: Replaces all the existing contents of the collection and initiates autosync if enabled. 109 | 110 | Events: 111 | - `reset`: when the collection's entire contents have been replaced. 112 | - `sync`: when the collection has been successfully synced with the server 113 | - `error`: when a request to remote server has failed. 114 | 115 | Example: 116 | 117 | ``` 118 | 119 | ``` 120 | 121 | 3. __force-sobject__: This element provides a custom component for `Force.SObject` from Mobile SDK's SmartSync JS. It allows apps to easily perform CRUD operations against a salesforce sobject in both online & offline modes. For Offline use, the application should first create a Smartstore soup with same name as the target sobject. 122 | 123 | Supported attributes include: 124 | - `sobject`: (Required) Name of Salesforce sobject against which CRUD operations will be performed. 125 | - `recordid`: (Required) Id of the record on which CRUD operations will be performed. 126 | - `fieldlist`: (Optional) Default: All fields. List of field names that need to be fetched for the record. Provide a space delimited list. Also the field names are case sensitive. 127 | - `autosync`: (Optional) Auto synchronize (fetch/save) changes to the model with the remote server/local store. If false, use fetch/save methods to commit changes to server or local store. 128 | - `cachemode`: (Optional) Default `SFDC.cacheMode()`. The cache mode (server-first, server-only, cache-first, cache-only) to use during CRUD operations. 129 | - `mergemode`: (Optional) Default `Force.MERGE_MODE.OVERWRITE`. The merge model to use when saving record changes to salesforce. 130 | - `fields`: Returns a map of fields to values for a specified record. Update this map to change SObject field values. 131 | 132 | Methods: 133 | - `fetch`: Initiate the fetching of record data from the relevant data store (server/offline store). 134 | - `save`: Initiate the saving of record data to the relevant data store (server/offline store). 135 | - `destroy`: Initiate the deleting of record data from the relevant data store (server/offline store). 136 | 137 | Events: 138 | - `save`: when the data has been successfully saved to the server. 139 | - `sync`: when the data has been successfully synced with the server. 140 | - `destroy`: when a record is deleted. 141 | - `error`: when a request to remote server has failed. 142 | - `invalid`: when the data validation fails on the client. 143 | 144 | Example: 145 | 146 | ``` 147 | 148 | ``` 149 | 150 | 4. __force-sobject-store__: This element provides a custom component for `Force.StoreCache` from Mobile SDK's SmartSync JS. It allows an app to quickly create and manage Smartstore soup for a salesforce sobject. 151 | 152 | Supported attributes include: 153 | - `sobject`: (Required) Type of sobject that you would like to store in this cache. 154 | - `fieldstoindex`: (Optional) Addition fields (given by their name) that you want to have indexes on. 155 | - `cacheReady`: Returns a promise to track store cache creation progress. 156 | - `cache`: Returns an instance of Force.StoreCache when it's ready to store/retrieve data. 157 | - `cacheForOriginals`: Returns an instance of Force.StoreCache to be used to keep data copy for conflict resolution. 158 | 159 | Methods: 160 | - `destroy`: Removes the soup from smartstore. Returns a promise to track the completion of process. 161 | 162 | Events: 163 | - `store-ready`: Fires this event when the store cache has been successfully created and ready to use. 164 | - `store-destroy`: Fires this event when the store cache has been successfully removed. 165 | 166 | Example: 167 | 168 | ``` 169 | 170 | ``` 171 | 172 | 5. __force-sobject-layout__: This web component provides the layout information for a particular sobject type or record. Layout information is cached in memory for existing session and is also stored in smartstore if used with Mobile SDK. `force-ui-detail` and `force-sobject-related` use this web component to obtain layout information. 173 | 174 | Supported attributes include: 175 | - `sobject`: (Required) Name of Salesforce sobject for which layout info will be fetched. 176 | - `hasrecordtypes`: (Optional) Default: false. If false, the element returns the default layout. Set true if the sobject has recordtypes or if you are unsure. If set to true, `recordid` or `recordtypeid` must be provided. 177 | - `recordtypeid`: (Optional) Default: null. Id of the record type for which layout has to be fetched. Required if `hasrecordtypes` is true and `recordid` is not provided. 178 | - `recordid`: (Optional) Default: null. Id of the record for which layout has to be fetched. Required if `hasrecordtypes` is true and `recordtypeid` is not provided. 179 | - `layout`: (Read Only) Returns an object with the complete layout information. 180 | 181 | Methods: 182 | - `fetch`: Method to manually initiate the fetching of layout information. 183 | 184 | Example: 185 | 186 | ``` 187 | 188 | ``` 189 | 190 | 6. __force-sobject-relatedlists__: This element allows fetching related lists configuration of a sobject record. It embeds the `force-sobject-layout` element to fetch the related list setup from the page layout. If `recordid` attribute is provided, it also generates a soql/cache query to fetch the related record items. 191 | 192 | Supported attributes include: 193 | - `sobject`: (Required) Name of Salesforce sobject for which related list info will be fetched. 194 | - `recordid`: (Required) Id of the record for which related list queries will be generated. These queries can be used for fetching related records. 195 | - `hasrecordtypes`: (Optional) Default: false. If false, the element returns the default layout. Set true if the sobject has recordtypes or if you are unsure. If set to true, `recordid` or `recordtypeid` must be provided. 196 | - `recordtypeid`: (Optional) Default: null. Id of the record type for which layout has to be fetched. Required if `hasrecordtypes` is true and `recordid` is not provided. 197 | - `relationships`: (Optional) Default: null. A list of relationship names that should only be fetched. If null, it fetches all related lists that are queryable. 198 | - `relatedLists`: Returns an array of all the related list information. 199 | 200 | Example: 201 | 202 | ``` 203 | 204 | ``` 205 | 206 | 7. __force-ui-app__: This element is a top level UI element that provides the basic styling and structure for the application. This element uses polymer layout features to enable flexible sections on the page. This is useful in single page view with split view panels. All the children of the main section must have the class "content" specified on them to apply the right styles. 207 | 208 | Supported attributes include: 209 | - `multipage`: (Optional) Default: false. When true, force-ui-app shows only one direct child, with class="page", at a time and allows navigation to other child elements. 210 | - `startpage`: (Optional) Default: first direct child element with class="page". Instance of the DOM element, with class="page", that should be shown first when the app loads. 211 | 212 | Example (when using inside Visualforce): 213 | 214 | ``` 215 | 216 | ``` 217 | 218 | 8. __force-ui-list__: This element enables the rendering of simple list of salesforce records driven by a `force-sobject-collection`. It uses the iron-selector element to detect record selection based on user's tap actions. This element should always be a child of `force-ui-app` element to inherit the appropriate styles. 219 | 220 | Supported attributes include: 221 | - `sobject`: (Required) Name of Salesforce sobject for which record list will be generated. 222 | - `query`: (Optional) SOQL/SOSL/SmartSQL statement to fetch the records. Required when querytype is soql, sosl or cache. 223 | - `querytype`: (Optional) Default: mru. Type of query (mru, soql, sosl, cache). Required if query attribute is specified. 224 | - `labelfield`: (Optional) Default: "Name". Name of the field to be used as label on each list element. 225 | - `sublabelfield`: (Optional) Name of the field to be used as the sublabel on each list element. 226 | - `selected`: Returns the value of "idfield" of the selected records. 227 | 228 | Example: 229 | 230 | ``` 231 | 232 | ``` 233 | 234 | 9. __force-ui-detail__: This element enables the rendering of full view of a salesforce record. This element uses the `force-sobject-layout` element to fetch the page layout for the record. This element also embeds a `force-sobject` element to allow all the CRUD operations on an SObject. This element should always be a child of `force-ui-app` element to inherit the default styles. 235 | 236 | Supported attributes include: 237 | - `sobject`: (Required) Name of Salesforce sobject for which detail view will be rendered. 238 | - `recordid`: (Required) Id of the record for which detail view will be rendered. 239 | - `hasrecordtypes`: (Optional) Default: false. If false, the element returns the default layout. Set true if the sobject has recordtypes or if you are unsure. If set to true, `recordid` or `recordtypeid` must be provided. 240 | - `recordtypeid`: (Optional) Default: null. Id of the record type for which layout has to be fetched. Required if `hasrecordtypes` is true and `recordid` is not provided. 241 | - `fieldlist`: (Optional) Default: All fields on the layout. A list of fields that should be displayed for the record. 242 | - `fieldlabels`: (Optional) Default: Actual field labels. A list of labels for fields provided in fieldlist attribute. The order of labels should be same as the order of fields in the fieldlist attribute. 243 | - `foredit`: (Optional) Default: false. Display edit view of the detail. 244 | 245 | Example: 246 | 247 | ``` 248 | 249 | ``` 250 | 251 | 10. __force-ui-relatedlist__: This element renders a list of records for a SObject's related list configuration. It uses the iron-selector element to detect record selection based on user's tap actions. This element should always be a child of `force-ui-app` element to inherit the default styles. 252 | 253 | Supported attributes include: 254 | - `related`: (Required) Related list configuration obtained from `force-sobject-relatedlist`. 255 | - `selected`: Returns the value of "idfield" of the selected records. 256 | 257 | Example: 258 | 259 | ``` 260 | 261 | ``` 262 | 263 | ## Third-party Code ## 264 | 265 | This library makes use of a number of third-party components: 266 | 267 | - [Polymer](http://www.polymer-project.org/), a JavaScript library to add new extensions and features to modern HTML5 browsers. It's built on top of Web Components, and designed to leverage the evolving web platform on modern browsers. 268 | - [jQuery](http://jquery.com), the JavaScript library to make it easy to write javascript. 269 | - [Backbonejs](http://backbonejs.org), a JavaScript library providing the model–view–presenter (MVP) application design paradigm. 270 | - [Underscorejs](http://underscorejs.org/), a utility-belt library for JavaScript. 271 | - [Ratchet](http://goratchet.com), Prototype iPhone apps with simple HTML, CSS, and JS components. 272 | 273 | 274 | ## FAQ ## 275 | 276 | __Polymer is still "alpha" project. How should I use it?__ 277 | 278 | Polymer as an overall project is still a work in progress. We feel that the underlying platform code leveraged for UI Elements is stable enough to start creating new apps for learning and prototyping purposes. Polymer will continue to be tweaked as the Web Components standard reaches its final stage. Various building blocks of Web Components, including Shadow DOM, are now natively supported in Chrome. This enables better performance for your mobile applications. 279 | 280 | __Polymer doesn't work inside the WebView on Android below 4.4__ 281 | 282 | This is limitation of the older version of WebKit used for the WebView on pre 4.4 devices. Polymer does work in the Android Mobile browsers along with all of the popular evergreen browsers. If Android WebView is a real limitation, please let us know. 283 | 284 | __Does it work with other devices?__ 285 | 286 | We got our samples to work on IE10 running on Windows Mobile, Safari on iOS6/7, Chrome, Safari, and Firefox 287 | 288 | __What's the level of support for this project?__ 289 | 290 | Mobile UI Elements is an unsupported project. It's a way for us to share our code with the community that might be beneficial for certain use cases. We'd love to build a vibrant community for this project. A lot depends on the level of interest. 291 | 292 | __I don't see any data show up in my components__ 293 | 294 | Please check the JavaScript console to be sure of the error. You might not be getting the data because your session has expired. If that's the case get a new session id. Or you might have enabled cross-domain scripting in Chrome or your other browser. 295 | 296 | 297 | ## Mobile UI Elements License ## 298 | Copyright (c) 2015, salesforce.com, inc. All rights reserved. 299 | 300 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 301 | 302 | - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 303 | - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 304 | - Neither the name of salesforce.com, inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 305 | 306 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 307 | -------------------------------------------------------------------------------- /elements/force-ui-detail/force-ui-detail.js: -------------------------------------------------------------------------------- 1 | (function($, SFDC, Path) { 2 | 3 | Polymer({ 4 | is: 'force-ui-detail', 5 | properties: { 6 | 7 | /** 8 | * (Required) Name of Salesforce sobject for which detail view will be rendered. 9 | * 10 | * @attribute sobject 11 | * @type String 12 | */ 13 | sobject: String, 14 | 15 | /** 16 | * (Required) Id of the record for which detail view will be rendered. 17 | * 18 | * @attribute recordid 19 | * @type String 20 | */ 21 | recordid: String, 22 | 23 | /** 24 | * (Optional) If false, the element returns the default layout. Set true if the sobject has recordtypes or if you are unsure. If set to true, "recordid" or "recordtypeid" must be provided. 25 | * 26 | * @attribute hasrecordtypes 27 | * @type Boolean 28 | * @default false 29 | */ 30 | hasrecordtypes: Boolean, 31 | 32 | /** 33 | * (Optional) Id of the record type for which layout has to be fetched. Required if "hasrecordtypes" is true and "recordid" is not provided. 34 | * 35 | * @attribute recordtypeid 36 | * @type String 37 | * @default null 38 | */ 39 | recordtypeid: { 40 | type: String, 41 | value: null 42 | }, 43 | 44 | /** 45 | * (Optional) Display edit view of the detail. 46 | * 47 | * @attribute foredit 48 | * @type Boolean 49 | * @default false 50 | */ 51 | foredit: { 52 | type: Boolean, 53 | value: false 54 | }, 55 | 56 | /** 57 | * (Optional) A list of fields that should be displayed for the record. 58 | * 59 | * @attribute fieldlist 60 | * @type String 61 | * @default null 62 | */ 63 | fieldlist: { 64 | type: String, /*TBD: Should move it to array */ 65 | value: null 66 | }, 67 | 68 | /** 69 | * (Optional) A list of labels for fields provided in fieldlist attribute. The order of labels should be same as the order of fields in the fieldlist attribute. 70 | * 71 | * @attribute fieldlabels 72 | * @type String 73 | * @default null 74 | */ 75 | fieldlabels: { 76 | type: String, /*TBD: Should move it to array */ 77 | value: null 78 | }, 79 | viewClass: { 80 | type: String, 81 | computed: '_viewClass(foredit)' 82 | }, 83 | model: { 84 | type: Object, 85 | observer: "renderModel", 86 | notify: true 87 | } 88 | }, 89 | observers: [ 90 | "renewTemplate(foredit, fieldlist, fieldlabels)" 91 | ], 92 | renewTemplate: function() { 93 | // Clean up older templates 94 | if (this._templateInfo) { 95 | this.$.viewContainer.innerHTML = ''; 96 | this._templateInfo = null; 97 | } 98 | // Generate new templates and update view 99 | SFDC.launcher.then(generateViewTemplate.bind(this)) 100 | .then(function(templateInfo) { 101 | // TemplateInfo may be null if no layout info has been fetched yet. 102 | if (templateInfo) { 103 | this._templateInfo = templateInfo; 104 | // Attach template to the DOM 105 | var viewContainer = Polymer.dom(this.$.viewContainer); 106 | this.$.viewContainer.innerHTML = templateInfo.templateString; 107 | templateInfo.template = viewContainer.querySelector('template'); 108 | //Polymer.dom(this.$.viewContainer).appendChild(templateInfo.template); 109 | // Render model and template 110 | this.renderModel(); 111 | } 112 | }.bind(this)); 113 | }, 114 | renderModel: function() { 115 | // Template Info may not generated yet 116 | if (this._templateInfo) this.async(updateViewModel); 117 | }, 118 | compileTemplate: function(layoutSections) { 119 | return compileTemplateForLayout(layoutSections); 120 | }, 121 | _viewClass: function() { 122 | return this.foredit ? 'edit' : ''; 123 | }, 124 | save: function(options) { 125 | var that = this; 126 | var originalErrorCB = options.error; 127 | var onError = function(model, xhr) { 128 | var viewErrors = {messages: []}; 129 | var error = new Force.Error(xhr); 130 | if (error.type === "RestError") { 131 | _.each(error.details, function(detail) { 132 | if (detail.fields == null || detail.fields.length == 0) { 133 | viewErrors.messages.push(detail.message); 134 | } else { 135 | _.each(detail.fields, function(field) { 136 | viewErrors[field] = detail.message; 137 | }); 138 | } 139 | }); 140 | } 141 | else if (error.type === "ConflictError") { 142 | _.each(error.remoteChanges, function(field) { 143 | var conflict = error.conflictingChanges.indexOf(field) >=0; 144 | viewErrors[field] = "Conflicting with server value: " + error.theirs[field]; 145 | }); 146 | } 147 | 148 | that.viewModel["__errors__"] = viewErrors; 149 | if (typeof originalErrorCB == 'function') originalErrorCB.apply(null, arguments); 150 | } 151 | options.error = onError; 152 | that.$.force_sobject.save(options); 153 | } 154 | }); 155 | 156 | // Returns whether the current view has an overriden layout. 157 | // i.e. if someone extends the force-ui-detail but doesn't include the shadow tag. 158 | var isLayoutOverriden = function(elem) { 159 | return false; // TBD: broken in 0.8 160 | /* 161 | var shadowRoot = elem.shadowRoot; 162 | while (shadowRoot.olderShadowRoot) { 163 | if (shadowRoot.olderShadowRoot.olderShadowRoot) 164 | shadowRoot = shadowRoot.olderShadowRoot; 165 | else break; 166 | } 167 | return elem.shadowRoot.olderShadowRoot && shadowRoot.querySelector('shadow') == null;*/ 168 | } 169 | 170 | var describeField = function(sobject, fieldname) { 171 | var sobjectType = SFDC.getSObjectType(sobject); 172 | // split the field path to get the base reference. eg. for field Owner.Name 173 | //var fieldPathParts = Path.get(fieldname); 174 | 175 | var fieldPicker = function(describeInfo) { 176 | var fieldInfos = describeInfo.fields; 177 | // if relationship path, i.e. more than 1 parts after split 178 | //if (fieldPathParts.length > 1) { 179 | // Find the corresponding relationship field. 180 | // var propFilter = {relationshipName: fieldPathParts[0]}; 181 | // var referenceField = _.findWhere(fieldInfos, propFilter); 182 | // If the referenceField is found, then get the field info for rest of the path 183 | //if (referenceField) { 184 | // return describeField(referenceField.referenceTo[0], 185 | // fieldPathParts.slice(1).join('.')); 186 | //} 187 | //} else { 188 | var propFilter = {name: fieldname}; 189 | return _.findWhere(fieldInfos, propFilter); 190 | //} 191 | } 192 | 193 | return sobjectType.describe().then(fieldPicker); 194 | } 195 | 196 | // Given the sobject name and fieldlist, it fetches the field infos for each field. 197 | // Returns a promise, which on resolution returns a map of fieldnames as keys and the respective field info as value. 198 | var fetchFieldInfos = function(sobject, fields) { 199 | var sobjectType = SFDC.getSObjectType(sobject); 200 | var fieldInfos = {}; 201 | var infoStatus = $.Deferred(); 202 | var describeTracker = []; 203 | 204 | fields.forEach(function(fieldPath) { 205 | var fieldDescribeStatus = describeField(sobject, fieldPath) 206 | .then(function(fieldInfo) { 207 | if (fieldInfo) fieldInfos[fieldPath] = fieldInfo; 208 | }); 209 | // Capture the promise in an array to track status 210 | describeTracker.push(fieldDescribeStatus); 211 | }); 212 | // When all field describe promises are done, then resolve the infoStatus promise with field infos. 213 | // This also handles FLS cases where the end user may not be able to see field info on a field. 214 | $.when.apply($, describeTracker).then(function() { 215 | infoStatus.resolve(fieldInfos); 216 | }); 217 | 218 | return infoStatus.promise(); 219 | } 220 | 221 | // Returns a promise, which on resolution returns an object with template information for rendering the view. 222 | // FIXME: Need to handle the case where multiple calls to prepareLayout happen and then have to kill older async processes. 223 | var generateViewTemplate = function() { 224 | 225 | var view = this; 226 | 227 | // Check if default layout is overriden. Don't do anything if yes. 228 | if (!isLayoutOverriden(view) && view.sobject) { 229 | if (view.fieldlist && typeof view.fieldlist === 'string' && view.fieldlist.trim().length) { 230 | // Parse the labels and generate fieldname to fieldlabel mapping. 231 | var fieldLabelMap = {}; 232 | var fieldsArray = view.fieldlist.trim().split(/\s+/); 233 | var fieldLabelsArray = typeof view.fieldlabels === 'string' 234 | ? view.fieldlabels.trim().split(',') : []; 235 | for (var idx in fieldsArray) { 236 | var label = fieldLabelsArray[idx]; 237 | if (label) fieldLabelMap[fieldsArray[idx]] = label.trim(); 238 | } 239 | 240 | // Fetch field infos and then generate template 241 | return fetchFieldInfos(view.sobject, fieldsArray) 242 | .then(function(fieldInfos) { 243 | return view.compileTemplate(compileTemplateForFields(fieldInfos, fieldLabelMap, view.foredit)); 244 | }); 245 | } else if (view.$.sobject_layout.layout) { 246 | // Return a promise to keep the return type consistent 247 | return $.when(view.compileTemplate( 248 | view.foredit ? view.$.sobject_layout.layout.editLayoutSections 249 | : view.$.sobject_layout.layout.detailLayoutSections 250 | )); 251 | } 252 | } 253 | } 254 | 255 | var updateViewModel = function() { 256 | 257 | var attachModel = function() { 258 | //Attach the template to the current view model 259 | this._templateInfo.template.model = this.viewModel; 260 | } 261 | 262 | // Fetch only if the current view model is not for same recordid 263 | if (this.recordid && 264 | (!this.viewModel || !this.viewModel.Id || this.viewModel.Id != this.recordid)) { 265 | 266 | // Remove the current model 267 | this._templateInfo.template.model = null; 268 | 269 | // Update the instance of current view model 270 | this.viewModel = new SObjectViewModel(this.model, this._templateInfo.fieldInfos); 271 | 272 | if (this.recordid) { 273 | // Perform data fetch for the fieldlist used in template 274 | this.$.force_sobject.fetch({ 275 | fieldlist: this._templateInfo.fields, 276 | cacheMode: this.fetchCacheMode, 277 | success: attachModel.bind(this) 278 | }); 279 | } else attachModel.apply(this); 280 | } 281 | } 282 | 283 | var SObjectViewModel = function(model, fieldInfos) { 284 | var _self = this; 285 | 286 | var dateTimeToString = function(type, value) { 287 | if (type == 'date') value = new Date(value).toDateString(); 288 | else if (type == 'datetime' && value) { 289 | var a = value.split(/[^0-9]/); 290 | value = new Date(a[0],a[1]-1,a[2],a[3],a[4],a[5]).toLocaleString(); 291 | } 292 | return value; 293 | } 294 | 295 | var setupProps = function(props) { 296 | props.forEach(function(prop) { 297 | Object.defineProperty(_self, prop, { 298 | get: function() { 299 | var fieldInfo = fieldInfos[prop]; 300 | var value = model[prop]; 301 | 302 | if (fieldInfo && fieldInfo.type) 303 | return dateTimeToString(fieldInfo.type, value); 304 | else return value; 305 | }, 306 | set: function(val) { 307 | var fieldInfo = fieldInfos[prop]; 308 | 309 | if (fieldInfo && fieldInfo.type && fieldInfo.type == 'base64') { 310 | var reader = new FileReader(); 311 | reader.onloadend = function () { 312 | model[prop] = reader.result; 313 | } 314 | if (file) reader.readAsDataURL(file); 315 | } 316 | else model[prop] = val; 317 | }, 318 | enumerable: true 319 | }); 320 | }); 321 | } 322 | // review all fields to pick the first part of the reference fields. eg. for "Owner.Name" pick "Owner" 323 | var attributes = _.map(_.keys(fieldInfos), function(prop) { 324 | return prop.split('.')[0]; 325 | }); 326 | setupProps(attributes); 327 | //TBD: Test if change in property list also updates the view model. Since the change observer was removed. 328 | } 329 | 330 | //------------------------- INTERNAL METHODS ------------------------- 331 | var getTemplateFor = function(template){ 332 | if (template) { 333 | if (_.isString(template)) return document.getElementById(template); 334 | else if (template instanceof HTMLTemplateElement) return template; 335 | } 336 | } 337 | 338 | // Utility method to ensure that input object is an array. 339 | // If not, wraps the input object into array. 340 | var modArray = function(obj) { 341 | if (!(obj instanceof Array)) 342 | if (obj) return [obj]; 343 | else return []; 344 | else return obj; 345 | } 346 | 347 | var createTemplateFromMarkup = function (markup, bindingDelegate) { 348 | // Templatize the markup 349 | var helperTemplate = document.createElement('template'); 350 | helperTemplate.setAttribute('is', 'dom-bind'); 351 | helperTemplate.innerHTML = markup; 352 | 353 | /* 354 | HTMLTemplateElement.decorate(helperTemplate); 355 | if (bindingDelegate) helperTemplate.bindingDelegate = bindingDelegate;*/ 356 | 357 | return helperTemplate; 358 | } 359 | 360 | //--------------------- DEFAULT TEMPLATES & GENERATION ------------------------ 361 | 362 | // Generates layout template for specific fields. Used by the DetailController. 363 | // TBD: Support the parent look up fields 364 | var compileTemplateForFields = function(fieldInfoMap, fieldLabelMap, foredit) { 365 | var row = {layoutItems: [], columns: 2}, 366 | column = 1, item, 367 | section = {heading: '', columns: 2, layoutRows:[]}; 368 | 369 | _.keys(fieldInfoMap).forEach(function(field) { 370 | item = { 371 | placeholder: false, 372 | editable: foredit, 373 | label: fieldLabelMap[field] || fieldInfoMap[field].label, 374 | layoutComponents: { 375 | type: 'Field', 376 | value: field, 377 | details: fieldInfoMap[field] 378 | } 379 | }; 380 | row.layoutItems.push(item); 381 | 382 | if (column++ == 2) { 383 | section.layoutRows.push(row); 384 | row = {layoutItems: [], columns: 2}; 385 | column = 1; 386 | } 387 | }); 388 | if (row.layoutItems.length) section.layoutRows.push(row); 389 | 390 | return [section]; 391 | } 392 | 393 | // Generates handlebar template for a layout object, which is returned by describeLayout api call. 394 | /* Sample template HTML: 395 | ```html 396 |
397 |

{{Section Heading}}

398 |
399 |
400 |
{{Item Label}}
401 | {{#if forEdit}}
{{Save Error for related fields}}
{{/if}} 402 | ... 403 |
404 | 405 | {{#if not forEdit}}{{fieldValue}}{{/if}} 406 | {{#if forEdit}}{{view Ember.InputView valueBinding="fieldValue"}}{{/if}} 407 | 408 | ... 409 |
410 |
411 | ... 412 |
413 | ... 414 |
415 | ... 416 | ``` 417 | */ 418 | //TBD: Allow way to hide empty values 419 | //TBD: Allow way to show selective field types 420 | var compileTemplateForLayout = function(layoutSections, model) { 421 | 422 | var view = this; 423 | // Utility method to return input element type for a corresponding salesforce field type. 424 | var inputType = function(fieldType) { 425 | switch(fieldType) { 426 | case "int": return "number"; 427 | case "double": return "number"; 428 | case "percent": return "number"; 429 | case "phone": return "tel"; 430 | case "date": return "date"; 431 | case "datetime": return "datetime"; 432 | case "time": return "time"; 433 | case "url": return "url"; 434 | case "email": return "email"; 435 | case "base64": return "file"; 436 | default: return "text"; 437 | } 438 | } 439 | 440 | // Generates and returns a Handlebar template for a specific field. 441 | // If forEdit is true and if field is editable, method returns an input type element. 442 | var generateFieldTemplate = function(fieldName, fieldInfo, displayField, forEdit) { 443 | var fieldType = fieldInfo.type, 444 | html = ''; 445 | 446 | if (forEdit) { 447 | if (fieldType == 'boolean') 448 | html += (''); 449 | else if (fieldType == 'picklist') { 450 | html += ''; 455 | } else if (fieldType == 'textarea') 456 | html += (''); 457 | else 458 | html += (''); 459 | } 460 | else { 461 | if (fieldType == 'boolean') 462 | html += (''); 463 | else if (fieldInfo.htmlFormatted) //TBD: See if we need to do anything for HTML type fields in polymer. 464 | html += ''; 465 | else html += ('[[' + displayField + ']]'); 466 | } 467 | return html + ''; 468 | } 469 | 470 | // Generates and returns the handlebar template for the layout sections. 471 | var html = ''; 472 | var layoutFieldsInfoMap = {}; 473 | 474 | layoutSections.forEach(function(section, sectionIndex) { 475 | html += '
'; 476 | html += ('

' + section.heading + '

'); 477 | // Iterate over layout rows in each section 478 | modArray(section.layoutRows).forEach(function(row) { 479 | html += '
'; 480 | // Iterate over layout items in each row 481 | modArray(row.layoutItems).forEach(function(item) { 482 | html += '
'; 483 | if (!item.placeholder) { 484 | html += ('
' + item.label + '
'); 485 | var errorHtml = ''; 486 | var valueHtml = '
'; 487 | // Iterate over layout item component in each item 488 | modArray(item.layoutComponents).forEach(function(comp) { 489 | var isFieldEditable = false; 490 | if (comp.type == 'Separator') valueHtml += comp.value; 491 | else if (comp.type == 'Field' && !/__XyzEncoded__s$/.test(comp.value)) { // Add a special case to ingnore weird geo location field which adds internal field to layout (*__XyzEncoded__s) 492 | var displayField = comp.value; // Default display field as the field of component 493 | var fieldInfo = comp.details; // Fetch the field info to check if it's a relationship 494 | layoutFieldsInfoMap[comp.value] = fieldInfo; // Track the field required for this layout 495 | if (fieldInfo.type == 'reference') { 496 | displayField = fieldInfo.relationshipName; 497 | displayField += (fieldInfo.referenceTo == 'Case' ? '.CaseNumber' : '.Name'); 498 | layoutFieldsInfoMap[displayField] = fieldInfo; 499 | } 500 | // check if field is editable based on the field type information and the layout settings. Also ignore refrence type fields as we don't currently support the edit for that. 501 | isFieldEditable = (item.editable && fieldInfo.type != 'reference' && fieldInfo.updateable); 502 | valueHtml += generateFieldTemplate(comp.value, fieldInfo, 'model.' + displayField, isFieldEditable); 503 | if (isFieldEditable) errorHtml += '
[[model.__errors__.' + comp.value + ']]
'; 504 | } 505 | }); 506 | html += (errorHtml + valueHtml + '
'); 507 | } 508 | html += '
'; 509 | }); 510 | html += '
'; 511 | }); 512 | html += '
'; 513 | }); 514 | 515 | // Templatize the markup 516 | return { 517 | templateString: '", 518 | //template: createTemplateFromMarkup(html), 519 | fields: _.keys(layoutFieldsInfoMap), 520 | fieldInfos: layoutFieldsInfoMap 521 | }; 522 | } 523 | 524 | })(jQuery, window.SFDC, window.Path); 525 | --------------------------------------------------------------------------------