├── .forceignore
├── .gitignore
├── README.md
├── config
└── project-scratch-def.json
├── force-app
└── main
│ └── default
│ ├── aura
│ └── URL_CreateRecordCmp
│ │ ├── URL_CreateRecordCmp.cmp
│ │ ├── URL_CreateRecordCmp.cmp-meta.xml
│ │ ├── URL_CreateRecordCmpController.js
│ │ └── URL_CreateRecordCmpHelper.js
│ └── classes
│ ├── URL_CreateRecordController.cls
│ ├── URL_CreateRecordController.cls-meta.xml
│ ├── URL_CreateRecordControllerTest.cls
│ └── URL_CreateRecordControllerTest.cls-meta.xml
├── images
├── cancel_save_button_behavior.gif
├── contact_url_hack_cmp.png
└── record-create-url-cmp-cancel.png
└── sfdx-project.json
/.forceignore:
--------------------------------------------------------------------------------
1 | # List files or directories below to ignore them when running force:source:push, force:source:pull, and force:source:status
2 | # More information: https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_exclude_source.htm
3 | #
4 |
5 | package.xml
6 |
7 | # LWC configuration files
8 | **/jsconfig.json
9 | **/.eslintrc.json
10 |
11 | # LWC Jest
12 | **/__tests__/**
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .sfdx
2 | .idea
3 | .vscode
4 | IlluminatedCloud
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Legitimate "URL Hacking" in Lightning Experience
2 | ================================================
3 |
4 | 
5 |
6 | Inspired by [Brian Kwong](https://twitter.com/Kwongerific)'s [blog post](https://thewizardnews.com/2018/08/02/url-hack-functionality-lightning/)
7 | on "URL Hacking" with supported features in Lightning Experience.
8 |
9 | This project's purpose is to determine a way to provide the "URL Hacking" in Lightning Experience
10 | with supported features in Lightning Experience but without the need of a Flow so that we can have
11 | a reusable solution for any object and supporting arbitrary number of fields and data types.
12 | This is actually one of the wish items Brian talks about at the end of his blog post.
13 |
14 | Creating Custom Button URL
15 | --------------------------
16 |
17 | The start of the URL should be `/lightning/cmp/c__URL_CreateRecordCmp?`, this tells Lightning Experience which component to navigate to.
18 | The below table describes the supported parameters to append to the URL. Separate each `parameter=value` pair with `&`.
19 |
20 | | Name | Description | Required? |
21 | |------|-------------|-----------|
22 | | `objectName` | API name of the object whose create form to show. | Yes |
23 | | `recordTypeId` | ID of the record type of the new record. | No |
24 | | `recordId` | ID of the current record where user is clicking the button from. Workaround for no cancel behavior customization. | No |
25 | | field name | One or more API field names whose default values to populate (e.g. FirstName, Phone, Active__c) | No |
26 |
27 |
28 | Examples
29 | --------
30 |
31 | Account button that opens the new contact form, pre-populating the first and last name fields:
32 | * `/lightning/cmp/c__URL_CreateRecordCmp?recordId={!Account.Id}&objectName=Contact&FirstName=Doug&LastName=Ayers`
33 |
34 | Design
35 | ------
36 |
37 | In [Summer '18](https://releasenotes.docs.salesforce.com/en-us/summer18/release-notes/rn_lc_components_navigation.htm),
38 | a new interface was introduced, [lightning:isUrlAddressable](https://developer.salesforce.com/docs/component-library/bundle/lightning:isUrlAddressable/documentation),
39 | which allows us to create URLs and pass arbitrary parameters to Lightning Components. Great! So that part is done, no
40 | need for Flow, we can create URL buttons similar to how we did in Classic so many moons ago.
41 |
42 | However, by navigating to this lightning component as a URL, you are navigating away from the original
43 | record page you were on when you clicked the button. The problem with that is there is no way to control
44 | what happens when the user clicks the **cancel** or **save** buttons in the modal dialog.
45 |
46 | When the user clicks the **save** button, then they are taken to view the new record.
47 | This default behavior is acceptable, for now.
48 |
49 | When the user clicks the **cancel** button, though, the modal simply disappears.
50 | The user is not redirected back to where they had clicked the button, so they are left with a blank page.
51 | This default behavior is not acceptable.
52 |
53 | 
54 |
55 | To compensate for the lack of control of the cancel behavior, the component supports a `recordId` URL parameter.
56 | When specified in the URL, the record is loaded in the background when the create record modal appears. By the time the user clicks the
57 | cancel button, the original record will have been loaded and the user won't notice that they had navigated away to begin with.
58 |
59 | 
60 |
61 | Next Steps
62 | ----------
63 |
64 | Please vote for the `force:recordCreate` event to expose callbacks to customize the modal dialog button behavior:
65 | * [Callback method for force:createRecord event to redirect or refresh after save](https://success.salesforce.com/ideaView?id=0873A0000003V4hQAE)
66 | * [Allow redirect after creating a new record using force:createRecord](https://success.salesforce.com/ideaView?id=0873A0000003VnmQAE)
67 |
--------------------------------------------------------------------------------
/config/project-scratch-def.json:
--------------------------------------------------------------------------------
1 | {
2 | "orgName" : "Company",
3 | "edition" : "Developer",
4 | "settings" : {
5 | "orgPreferenceSettings" : {
6 | "s1DesktopEnabled" : true,
7 | "s1EncryptedStoragePref2" : false
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/force-app/main/default/aura/URL_CreateRecordCmp/URL_CreateRecordCmp.cmp:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/force-app/main/default/aura/URL_CreateRecordCmp/URL_CreateRecordCmp.cmp-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 45.0
4 | URL_CreateRecordCmp
5 |
6 |
--------------------------------------------------------------------------------
/force-app/main/default/aura/URL_CreateRecordCmp/URL_CreateRecordCmpController.js:
--------------------------------------------------------------------------------
1 | ({
2 | // this method is called when component initializes
3 | onInit: function( component, event, helper ) {
4 | helper.handleShowCreateForm( component );
5 | }
6 | })
--------------------------------------------------------------------------------
/force-app/main/default/aura/URL_CreateRecordCmp/URL_CreateRecordCmpHelper.js:
--------------------------------------------------------------------------------
1 | ({
2 | handleShowCreateForm: function( component ) {
3 |
4 | /*
5 | * Supported URL parameters:
6 | * objectName - API name of a standard or custom object (e.g. Account or Project__c)
7 | * recordTypeId - which record type to use, if not specified then the user's default record type is assumed
8 | *
9 | * All other key=value URL parameters are
10 | * assumed as default field values for the record form.
11 | *
12 | * URL parameter names must match the API field names exactly (case-sensitive).
13 | * For example, "phone" and "PHONE" will not pre-populate the standard Contact "Phone" field.
14 | *
15 | * Example Custom Button URL:
16 | * "/lightning/cmp/c__URL_CreateRecordCmp?objectName=Contact&FirstName=Astro&LastName=Nomical&AccountId={!Account.Id}"
17 | */
18 |
19 | let helper = this;
20 |
21 | let pageRef = component.get( 'v.pageReference' );
22 |
23 | // Retrieve specific parameters from the URL.
24 | // For case-insensitivity, the properties are lowercase.
25 | let urlParamMap = {
26 | 'objectname' : '', // object whose create form to display
27 | 'recordtypeid' : '', // record type for new record (optional)
28 | 'recordid' : '' // id of record where button was clicked
29 | };
30 |
31 | for ( let key in pageRef.state ) {
32 | let lowerKey = key.toLowerCase();
33 | if ( urlParamMap.hasOwnProperty( lowerKey ) ) {
34 | urlParamMap[lowerKey] = pageRef.state[key];
35 | }
36 | }
37 |
38 | console.log( 'urlParamMap', urlParamMap );
39 |
40 | Promise.resolve()
41 | .then( function() {
42 | if ( !$A.util.isEmpty( urlParamMap.recordid ) ) {
43 | // workaround for not being able to customize the cancel
44 | // behavior of the force:createRecord event. instead of
45 | // the user seeing a blank page, instead load in the background
46 | // the very record the user is viewing so when they click cancel
47 | // they are still on the same record.
48 | helper.navigateToUrl( '/' + urlParamMap.recordid );
49 | // give the page some time to load the new url
50 | // otherwise we end up firing the show create form
51 | // event too early and the page navigation happens
52 | // afterward, causing the quick action modal to disappear.
53 | return new Promise( function( resolve, reject ) {
54 | setTimeout( resolve, 1000 );
55 | });
56 | }
57 | })
58 | .then( function() {
59 | helper.showCreateForm( component, urlParamMap, pageRef );
60 | });
61 |
62 | },
63 |
64 | // -----------------------------------------------------------------
65 |
66 | showCreateForm: function( component, urlParamMap, pageRef ) {
67 |
68 | let helper = this;
69 |
70 | helper.enqueueAction( component, 'c.getFieldDescribeMap', {
71 |
72 | 'objectName' : urlParamMap.objectname
73 |
74 | }).then( $A.getCallback( function( fieldDescribeMap ) {
75 |
76 | console.log( 'fieldDescribeMap', fieldDescribeMap );
77 |
78 | let eventParamMap = {
79 | 'defaultFieldValues' : {}
80 | };
81 |
82 | if ( !$A.util.isEmpty( urlParamMap.objectname ) ) {
83 | eventParamMap['entityApiName'] = urlParamMap.objectname;
84 | }
85 |
86 | if ( !$A.util.isEmpty( urlParamMap.recordtypeid ) ) {
87 | eventParamMap['recordTypeId'] = urlParamMap.recordtypeid;
88 | }
89 |
90 | // ensure only fields the current user has permission to create are set
91 | // otherwise upon attempt to save will get component error
92 | for ( let fieldName in pageRef.state ) {
93 | if ( fieldDescribeMap.hasOwnProperty( fieldName ) && fieldDescribeMap[fieldName].createable ) {
94 | // avoid setting lookup fields to undefined, get Error ID: 1429293140-211986 (-590823013), assign to null instead
95 | eventParamMap.defaultFieldValues[fieldName] = pageRef.state[fieldName] || null;
96 | }
97 | }
98 |
99 | return eventParamMap;
100 |
101 | })).then( $A.getCallback( function( eventParamMap ) {
102 |
103 | console.log( 'eventParamMap', eventParamMap );
104 |
105 | $A.get( 'e.force:createRecord' ).setParams( eventParamMap ).fire();
106 |
107 | })).catch( $A.getCallback( function( err ) {
108 |
109 | helper.logActionErrors( err );
110 |
111 | }));
112 |
113 | },
114 |
115 | navigateToUrl: function( url ) {
116 |
117 | console.log( 'navigating to url', url );
118 |
119 | if ( !$A.util.isEmpty( url ) ) {
120 | $A.get( 'e.force:navigateToURL' ).setParams({ 'url': url }).fire();
121 | }
122 |
123 | },
124 |
125 | enqueueAction: function( component, actionName, params, options ) {
126 |
127 | let helper = this;
128 |
129 | return new Promise( function( resolve, reject ) {
130 |
131 | component.set( 'v.showSpinner', true );
132 |
133 | let action = component.get( actionName );
134 |
135 | if ( params ) {
136 | action.setParams( params );
137 | }
138 |
139 | if ( options ) {
140 | if ( options.background ) { action.setBackground(); }
141 | if ( options.storable ) { action.setStorable(); }
142 | }
143 |
144 | action.setCallback( helper, function( response ) {
145 |
146 | component.set( 'v.showSpinner', false );
147 |
148 | if ( component.isValid() && response.getState() === 'SUCCESS' ) {
149 |
150 | resolve( response.getReturnValue() );
151 |
152 | } else {
153 |
154 | console.error( 'Error calling action "' + actionName + '" with state: ' + response.getState() );
155 |
156 | helper.logActionErrors( response.getError() );
157 |
158 | reject( response.getError() );
159 |
160 | }
161 | });
162 |
163 | $A.enqueueAction( action );
164 |
165 | });
166 | },
167 |
168 | logActionErrors : function( errors ) {
169 | if ( errors ) {
170 | if ( errors.length > 0 ) {
171 | for ( let i = 0; i < errors.length; i++ ) {
172 | console.error( 'Error: ' + errors[i].message );
173 | }
174 | } else {
175 | console.error( 'Error: ' + errors );
176 | }
177 | } else {
178 | console.error( 'Unknown error' );
179 | }
180 | }
181 | })
--------------------------------------------------------------------------------
/force-app/main/default/classes/URL_CreateRecordController.cls:
--------------------------------------------------------------------------------
1 |
2 | /**
3 | * Author: Doug Ayers
4 | * Website: https://douglascayers.com
5 | * GitHub: https://github.com/douglascayers/sfdx-record-create-url-component
6 | * License: BSD 3-Clause License
7 | */
8 | public class URL_CreateRecordController {
9 |
10 | /**
11 | * Returns basic field describe to know which fields
12 | * are or are not createable and updateable.
13 | *
14 | * @param objectName
15 | * API name of the object whose field describe to return
16 | */
17 | @AuraEnabled( cacheable = true )
18 | public static Map getFieldDescribeMap( String objectName ) {
19 |
20 | Map fieldDescribeMap = new Map();
21 |
22 | // Performance trick to use Type.forName instead of Schema.getGlobalDescribe()
23 | // https://salesforce.stackexchange.com/a/32538/987
24 | // https://salesforce.stackexchange.com/a/219010/987
25 | Type reflector = Type.forName( objectName );
26 | SObject obj = (SObject) reflector.newInstance();
27 | SObjectType objType = obj.getSObjectType();
28 |
29 | DescribeSObjectResult describe = objType.getDescribe();
30 | Map fieldsMap = describe.fields.getMap();
31 | for ( String fieldName : fieldsMap.keySet() ) {
32 | DescribeFieldResult fieldDescribe = fieldsMap.get( fieldName ).getDescribe();
33 | fieldDescribeMap.put( fieldDescribe.getName(), new Map{
34 | 'accessible' => fieldDescribe.isAccessible(),
35 | 'createable' => fieldDescribe.isCreateable(),
36 | 'updateable' => fieldDescribe.isUpdateable()
37 | });
38 | }
39 |
40 | return fieldDescribeMap;
41 | }
42 |
43 | }
44 | /*
45 | BSD 3-Clause License
46 | Copyright (c) 2018, Doug Ayers, douglascayers.com
47 | All rights reserved.
48 | Redistribution and use in source and binary forms, with or without
49 | modification, are permitted provided that the following conditions are met:
50 | * Redistributions of source code must retain the above copyright notice, this
51 | list of conditions and the following disclaimer.
52 | * Redistributions in binary form must reproduce the above copyright notice,
53 | this list of conditions and the following disclaimer in the documentation
54 | and/or other materials provided with the distribution.
55 | * Neither the name of the copyright holder nor the names of its
56 | contributors may be used to endorse or promote products derived from
57 | this software without specific prior written permission.
58 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
59 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
60 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
61 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
62 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
63 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
64 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
65 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
66 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
67 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
68 | */
--------------------------------------------------------------------------------
/force-app/main/default/classes/URL_CreateRecordController.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 45.0
4 | Active
5 |
6 |
--------------------------------------------------------------------------------
/force-app/main/default/classes/URL_CreateRecordControllerTest.cls:
--------------------------------------------------------------------------------
1 | /**
2 | * Author: Doug Ayers
3 | * Website: https://douglascayers.com
4 | * GitHub: https://github.com/douglascayers/sfdx-record-create-url-component
5 | * License: BSD 3-Clause License
6 | */
7 | @IsTest
8 | private class URL_CreateRecordControllerTest {
9 |
10 | @IsTest
11 | private static void test_get_field_describe() {
12 |
13 | Test.startTest();
14 |
15 | Map fieldDescribeMap = URL_CreateRecordController.getFieldDescribeMap( 'Account' );
16 |
17 | System.debug( fieldDescribeMap );
18 |
19 | System.assertEquals( true, ( (Map) fieldDescribeMap.get( 'Name' ) ).get( 'createable' ) );
20 | System.assertEquals( false, ( (Map) fieldDescribeMap.get( 'Id' ) ).get( 'updateable' ) );
21 |
22 | Test.stopTest();
23 |
24 | }
25 |
26 | }
27 | /*
28 | BSD 3-Clause License
29 | Copyright (c) 2018, Doug Ayers, douglascayers.com
30 | All rights reserved.
31 | Redistribution and use in source and binary forms, with or without
32 | modification, are permitted provided that the following conditions are met:
33 | * Redistributions of source code must retain the above copyright notice, this
34 | list of conditions and the following disclaimer.
35 | * Redistributions in binary form must reproduce the above copyright notice,
36 | this list of conditions and the following disclaimer in the documentation
37 | and/or other materials provided with the distribution.
38 | * Neither the name of the copyright holder nor the names of its
39 | contributors may be used to endorse or promote products derived from
40 | this software without specific prior written permission.
41 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
42 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
43 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
44 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
45 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
46 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
47 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
48 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
49 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
50 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
51 | */
--------------------------------------------------------------------------------
/force-app/main/default/classes/URL_CreateRecordControllerTest.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 45.0
4 | Active
5 |
6 |
--------------------------------------------------------------------------------
/images/cancel_save_button_behavior.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/douglascayers/sfdx-record-create-url-component/7f6de003af4683bc9338ea9da470508cf9b56f34/images/cancel_save_button_behavior.gif
--------------------------------------------------------------------------------
/images/contact_url_hack_cmp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/douglascayers/sfdx-record-create-url-component/7f6de003af4683bc9338ea9da470508cf9b56f34/images/contact_url_hack_cmp.png
--------------------------------------------------------------------------------
/images/record-create-url-cmp-cancel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/douglascayers/sfdx-record-create-url-component/7f6de003af4683bc9338ea9da470508cf9b56f34/images/record-create-url-cmp-cancel.png
--------------------------------------------------------------------------------
/sfdx-project.json:
--------------------------------------------------------------------------------
1 | {
2 | "packageDirectories": [
3 | {
4 | "path": "force-app",
5 | "default": true
6 | }
7 | ],
8 | "namespace": "",
9 | "sfdcLoginUrl": "https://login.salesforce.com",
10 | "sourceApiVersion": "45.0"
11 | }
12 |
--------------------------------------------------------------------------------