1

I have been developing a visualforce page that renders product catalog PDFs from a PriceBook2. It loops over every Product2 in the PriceBook2 and puts all their field values into a nice table. The catalogs also include product photos, which are currently being manually uploaded to the Documents object and their URLs are copied and pasted by hand into a custom URL field on the Product2 object.

I would like to add a button to my Product2 page layout to automate the process of uploading product images. I want the user to be able to click a button to upload an image (probably to the Documents object) and then automatically insert the URL of the newly uploaded image into a custom field on the Product2 record.

I am aware that the Notes & Attachments related list offers some similar functionality, but as each product only has one image, I would prefer to just have something that stores them in Documents and then inserts the URL for that image into a text/URL field instead. That way I do not have to write any extra code in my product catalog generator to loop through Notes & Attachments and parse product photos from there.

Currently I have a custom URL field Product2.Product_Image__c. I would like to just be able to have a button on the Product layout that the user clicks, brings up a little upload widget, and then inserts the URL of that image automatically into that field so that it can be accessed in my visualforce page.

I am assuming I will need to make a custom image upload widget in Visualforce, add a button for it on the Product2 page layout and have some kind of Apex trigger or workflow rule that automatically populates the image URL into Product2.Product_Image__c

Does anyone know a quick and easy way to do this?

  • Why don't you use the SharinPix app from the AppExchange? It offers all the features you need for your business case. appexchange.salesforce.com/… – Kevan Oct 6 '17 at 18:33
  • Kevan, I already built a custom solution that addresses this issue (salesforce.stackexchange.com/a/194687/49567) and my organization is probably not going to pay for some third party app that is probably a lot more bloated and complex than the solution I built that does this. I also find it hard to believe that it provides "unlimited storage." There is no such thing as "unlimited" in salesforce. – ag415 Oct 6 '17 at 19:29
2

If you used a Rich Text Area (RTA), you could capture the image, then put it into the RTA field. For example, you could have an <input type="file" /> next to each product, and when the user selects the image from browsing, use readAsDataURL to get a data URL, then put the data into the field:

// Written as apex code as an example
Product2 someRecord = [SELECT Id FROM Product2 WHERE Id = :selectedProductId];
someRecord.RTA_Field__c = '<img src="'+ dataUri + '" >';
update someRecord;

When you do this, Salesforce converts the data URI into a file that will be stored in Salesforce's CDN, and you can then display the image simply by using the field. No triggers needed, no complicated work, and only about 20-30 lines of code in JavaScript. Similarly, though, you could also choose to use an <apex:inputFile />, and do the entire thing in Apex Code:

    Product2 someRecord = [SELECT Id FROM Product2 WHERE Id = :selectedProductId];
    String dataUri = 'data:'+ contentType + ';base64,'+EncodingUtil.base64Encode(fileBody);
    someRecord.RTA_Field__c = '<img src="' + dataUri + '" />';
    update someRecord;

Other similar solutions exist without any complicated designs necessary, but hopefully this will give you some ideas to get started.

  • I don't really understand how to reconcile this with my Visualforce widget. I tried Hemant Jain's solution but there's swathes of missing code. After much struggling this is what I have come up with: pastebin.com/4rHMWvPq . Please tell me how I can make this actually work to do what I need. I still don't even understand why I need to use a rich text field. To be very clear, I do not care about displaying the image on the product page layout. All I care about is getting it into the documents object, and getting the URL to that file into a field. That is all I need. Please let me know. Thanks. – ag415 Oct 3 '17 at 0:13
  • The reason why I ONLY need the URL is because I have already built a PDF renderer visualforce page. This page simply references a plain text URL field to display images in the PDF. I do not need rich text. All I need is a path to a file. – ag415 Oct 3 '17 at 0:15
  • 1
    @ag415 The RTA holds the data, and provides a URL. I wrote up some working code, and put it in to a gist for you. Actually running this code, you'll see exactly how it works. – sfdcfox Oct 3 '17 at 2:57
  • I appreciate you guys trying to help me, but I ended up rolling my own custom solution that does not touch rich text fields at all. I've posted an answer here to show what I did. My method uploads the files straight into the Documents object, so the limit per upload is 5mb and it seems to work pretty nice. That being said, I'm not an expert on Apex or VF so I imagine I may have done some things not so elegantly and there may be issues here that I'm not aware of, so I would appreciate your feedback on it. I'm curious to know how my solution stacks up against the others that were suggested here – ag415 Oct 5 '17 at 23:26
1

As advised by sfdcfox, you can use Rich Text Area(RTA) field and display the image into it.

I had implemented a similar kind of requirement wherein user would upload a image through a input file(or capture using phone's camera), which would be displayed in the rich text area field as a URL.

Below is the code that was done for this requirement.

This is the VF page code:

<!-----------------Visualforce Page: Start---------------------->
<!-----------------input type file---------------------->
<input id="cameraImageId" type="file" accept="image/*;capture=camera"/>

<!--Action funtion uploads the data to document object-->
<apex:actionFunction name="callUploadMethod" action="{!upload}" oncomplete="fillRichTextArea('{!imageURL}')">
    <apex:param name="theURL" value=""/>
</apex:actionFunction>
<div id="waitMessage" style="display: none">Please wait...</div>

<script>
// Camera Change Event: The below code is used to capture and upload a image to the Visit Notes field
j$('#cameraImageId').change(function(event) {
    var file = event.target.files[0];
    // generate a new FileReader object
    var reader = new FileReader();
    // inject an image with the src url
    reader.onload = function(event) {      
       dataurl =event.target.result;
        j$("#waitMessage").show();                    
        callUploadMethod(dataurl);
    }
    // when the file is read it triggers the onload event above.
    reader.readAsDataURL(file);

});

// Display Capurted Image in Rich Text Area
function fillRichTextArea(imageURL) {
    var heightValue = "375";
    var widthValue = "500";
    for(var i in CKEDITOR.instances) {
        CKEDITOR.instances[i].insertHtml("<img src='" + imageURL + "' height = '" + heightValue + "' width = '" + widthValue + "'/>");
    }  
    j$("#waitMessage").hide();
}    
</script>
<!------------VisualForce Page:End-------------------->

Below is the Apex Method, that creates the document record.

//Apex Class Method(Called from Action Function)
// method to create an attachment!
public PageReference upload() { 

    String theURL = ApexPages.currentPage().getParameters().get('theURL');
    theURL = theURL.substring(theURL.indexOf(',') +1);

    Blob encodedBlob = EncodingUtil.base64Decode(theURL);
    document.body =   encodedBlob;

    // put it in running user's folder 
    document.AuthorId = UserInfo.getUserId(); 
    document.FolderId = UserInfo.getUserId();
    document.Name='Product-Name'; 
    try { 
        insert document;
        imageURL='/servlet/servlet.FileDownload?file=';
        imageURL=imageURL + document.id;
    } catch (DMLException e) { ApexPages.addMessage(new ApexPages.message(ApexPages.severity.ERROR,'Error uploading file'));return null; } 
    // clears the viewstate 
    finally { document.body = null; document = new Document(); } 
    return null; 
}

You might need to change fillRichTextArea method, to insert the URL at the right place.

Hope this helps.

  • Hi, I am trying to implement your Apex method in an extension class for the standard product controller (I assume I need to extend it to actually be able to embed a VF page that can use the method in the standard product page layout). However, I'm getting a bunch of errors: A value cannot be stored to Body in type Document A value cannot be stored to AuthorId in type Document A value cannot be stored to FolderId in type Document A value cannot be stored to Name in type Document Variable does not exist: document Variable does not exist: imageURL (x3) and a bunch of other errors – ag415 Oct 2 '17 at 18:40
  • 1
    I did a walkthrough of the code, I couldn’t find any rich text area input field on the visualforce page. Can you try adding a RTA field and check if the image gets populated in the field? – Hemant Jain Oct 3 '17 at 7:09
  • 1
    Also have a look on the documents, whether the record gets inserted or not? – Hemant Jain Oct 3 '17 at 7:11
  • 1
    Hey, was the issue resolved? – Hemant Jain Oct 5 '17 at 3:29
  • 1
    I would advise against the use of rich text fields for storage of images as those images will not be available to mobile applications built with the Salesforce Mobile SDK. – Joseph Nardone Nov 7 '17 at 15:32
1

Well, after spending a bunch of time struggling with the methods that were suggested, I decided to just roll my own solution using a pure documents approach.

This method does not do anything with rich text fields and uploads the files directly to the documents object. I'm not really an expert on Apex or Visualforce so I imagine there are some things I did here that aren't quite as elegant as they can be, but I am pretty pleased with the results and wanted to share this with you guys to gather feedback on how I can improve it and how it stacks up against the methods you guys suggested.

Here is my Apex class:

public class ProductPhotosExtension {
    private final Product2 prod;
    private String imageURL;
    private String errorURL = '/apex/ProductPhotoError';

    public transient Blob fileBody {
        get; set;
    }

    public String fileName {
        get; set;
    }

    public ProductPhotosExtension(ApexPages.StandardController stdController) {
        stdController.addFields(new String[] {
            'Id',
            'Name',
            'Product_Image_ID__c',
            'Product_Image__c'
        });
        this.prod = (Product2)stdController.getRecord();
    }

    private PageReference showError(String errorTitle, String errorText) {
            errorTitle = EncodingUtil.urlEncode(errorTitle, 'UTF-8');
            errorText = EncodingUtil.urlEncode(errorText, 'UTF-8');
            errorURL = errorURL + '?errorTitle=' + errorTitle;
            errorURL = errorURL + '&errorText=' + errorText;
            errorURL = errorURL + '&id=' + prod.Id;
            PageReference errPage = new PageReference(errorURL);
            return errPage;
    }

    public PageReference deletePhoto() {
        try { 
            Document document = [SELECT Id FROM Document WHERE Id = :prod.Product_Image_ID__c];
            prod.Product_Image_ID__c = null;
            prod.Product_Image__c = null;
            update prod;
            delete document;
        } catch (Exception e) {
            String errorTitle = 'Error deleting photo';
            String errorText = 'The photo may have already been deleted.';
            return showError(errorTitle, errorText);
        }

        return null;
    }    

    public PageReference upload() {
        Product2 refreshed_prod = [SELECT Id, Product_Image_ID__c FROM Product2 WHERE Id = :prod.Id];
        if (refreshed_prod.Product_Image_ID__c != null) {
            String errorTitle = 'Error uploading photo';
            String errorText = 'This product already has a photo.';
            return showError(errorTitle, errorText);           
        }


        Document document = new Document();
        document.Body = fileBody;
        document.Name = fileName;
        document.FolderId = '00l33000001ko9v';
        try {
            imageURL = '/servlet/servlet.FileDownload?file=';
            insert document;
            imageURL = imageURL + document.id;
            prod.Product_Image_ID__c = document.id; 
            prod.Product_Image__c = imageURL;
            update prod;            
        } catch (Exception e) { 
            String errorTitle = 'Error uploading photo';
            String errorText = 'Please make sure that the image file is not over 5MB' + 
                               ' and that you have enough space in your Documents';
            return showError(errorTitle, errorText);
        }

        return null;
    }

    public Blob getFileBody() {
        return fileBody;
    }

    public String getFileName() {
        return fileName;
    }

    public String getProductImage() {
        return prod.Product_Image__c;
    }

    public String getProductImageID() {
        return prod.Product_Image_ID__c;
    }

}

Here is the product uploader widget I embedded in my Product page layout (UploadProductPhoto.vfp):

<apex:page standardController="Product2" extensions="ProductPhotosExtension" sidebar="false" showheader="false">
    <apex:stylesheet value="{!$Resource.photouploaderstyles}"/>
    <apex:form id="photoUploaderForm" styleClass="photoUploaderForm">
        <apex:pageBlock mode="maindetail" id="sectionsContainer">
            <apex:pageBlockSection columns="1" id="input">
                <apex:pageBlockSectionItem id="inputSection" rendered="{!ISBLANK(ProductImageID)}">
                    <apex:outputLabel value="Product Image" for="fileImageId"/>
                    <apex:inputFile id="fileImageId" value="{!fileBody}" styleClass="inputFile"
                                    filename="{!fileName}" onchange="uploadFile(event);"/>
                </apex:pageBlockSectionItem>
                <div id="waitMessage" class="waitMessage">
                    Uploading file... Please wait.
                </div>
            </apex:pageBlockSection>
            <apex:pageBlockSection id="preview" columns="1" rendered="{!NOT(ISBLANK(ProductImageID))}">
                <apex:pageBlockSectionItem id="previewSection">
                    <apex:outputLabel value="Product Image" for="previewImageId"/>
                    <a href="{!ProductImage}" target="blank">
                        <img id="imagePreview" src="{!ProductImage}" class="previewImage"/>
                    </a>                
                    <a href="javascript:deleteFile();" class="deleteButton">X</a>
                </apex:pageBlockSectionItem>
            </apex:pageBlockSection>
        </apex:pageBlock>
        <apex:actionFunction name="callUploadMethod" action="{!upload}"/>   
        <apex:actionFunction name="callDeleteMethod" action="{!deletePhoto}"/>  
    </apex:form>

    <script>
        var input_divs = document.getElementsByClassName('inputFile');
        var input_div = input_divs[0];
        var wait_div = document.getElementById('waitMessage');    

        function getOffset(el) {
            el = el.getBoundingClientRect();
            return {
                left: el.left + window.scrollX,
                top: el.top + window.scrollY
            }
        }

        function deleteFile(event) {
            callDeleteMethod();
        }

        function uploadFile(event) {
            var file = event.target.files[0];
            var reader = new FileReader();

            reader.addEventListener("load", function () {
                callUploadMethod();
                input_div.setAttribute('type', 'hidden');
                input_rect = getOffset(input_div);
                wait_div.style.left = input_rect.left + 'px';
                wait_div.style.top = input_rect.top + 'px';
                wait_div.style.display = 'block';
            }, false);
            reader.readAsDataURL(file);             
        } 

    </script>
</apex:page>

Here is the stylesheet for the photo widget (photouploaderstyles.css):

.photoUploaderForm {
    width: 94%; 
    margin-left: 18%;
    position: fixed;
    overflow: hidden;
}

@media (max-width: 563px) {
    .photoUploaderForm {
        width: 155%;
        margin-left: 5%;
    }
}

.inputFile {
    width: 80%;
}

@media (max-width: 563px) {
    .inputFile {
        width: 45%;
    }
}

.waitMessage {
    position: absolute;
    text-align: center;
    padding: 5px 2% 5px 2%;
    background: #ffffff;
    border: 1px solid;
    border-color: #000000;
    display: none;
    width: 50%;
}

.previewImage {
    border: 1px dashed;
    border-color: #cccccc;
    height: 50px;
    padding: 2px;
}

.deleteButton {
    top: 10px;
    font-size: 0.6em;
    font-weight: 800;
    position: absolute; 
    background: #ff8989;
    margin: -1px 0px 0px 2px;
    padding: 1px 5px 1px 5px;
    text-decoration: none;
    border: 1px solid;
    border-color: #ff0000;
}

.deleteButton:link, .deleteButton:visited { 
    color: #ffffff;
}

.deleteButton:hover {
    background: #ff0000;
    text-decoration: none;
}

.deleteButton:active {
    color: #ff0000;
    background: #ffa0a0;
}

I also made a neat little error widget (ProductPhotoError) for handling errors, since the standard apex error messages look hideous in small-sized embedded VF pages:

<apex:page sidebar="false" showheader="false">
    <apex:stylesheet value="{!$Resource.photouploadererrorstyles}"/>
    <div class="errorMsgBox">
        <div class="errorMsgIcon">
            <svg viewBox="0 0 30 30" xmlns="http://www.w3.org/2000/svg">
                <circle cx="15" cy="15" r="14px" fill="#a50000"/>
                <text x="9" y="21" fill="#ffffff" font-size="2em">x</text>
            </svg>            
        </div>

        <div class="errorMsgTitleBar">
            <p class="errorMsgTitle">
                {!$CurrentPage.parameters.errorTitle}
            </p>            
        </div>

        <p class="errorMsgText">
            {!$CurrentPage.parameters.errorText}
        </p>
        <a href="/apex/UploadProductPhoto?id={!$CurrentPage.parameters.Id}" class="errorOKButton">OK</a>
    </div>
</apex:page>

And here is the style sheet for the error page (photouploadererrorstyles.css):

.errorMsgBox {
    margin: 0px 4% 0px 4%;
    border: 1px solid;
    border-color: #000000;
    padding: 2px;
    font-size: 1em;
    word-wrap: break-word;
    position: relative;
    display: block;
}

.errorMsgIcon {
    display: block;
    height: 30px;
    width: 30px;
    position: absolute;
    left: 5px;
    top: 5px;
}

.errorMsgTitleBar {
    text-align: center;
    margin: 0px;
    padding: 0px;
}

.errorMsgTitle {
    display: inline-block;
    margin: 0px 0px 2px 0px;
    padding: 0px;
    font-weight: bold;
}

.errorMsgText {
    margin: 0px 0px 0px 8%;
    padding: 0px 5px 2px 5px;
    text-align: center;
}

.errorOKButton {
    display: block;
    margin: 0px;
    padding: 0px;
    text-align: center;
}

What do you guys think?

P.S. the widget is embedded in the first column in a 2-column section on the page layout with a width value of "100%" and a height of "75px"

0

Regarding "That way I do not have to write any extra code in my product catalog generator to loop through Notes & Attachments and parse product photos from there."

What's so difficult about doing that? Any standard Visualforce controller can pull the object's attachments. Suppose you ensure that the product image file attachment always has the name "Product Image" in it. Then in Visualforce you can do something like this:

<apex:repeat value="{!Record.attachments}" var="att">
   <apex:image url="{!URLFOR($Action.Attachment.Download, att.id)}" 
     rendered='{!contains(att.Name, "Product Image")}' width="100px"/>
</apex:repeat>

Note that old-style Attachments are on their way out, so you'd do something similar with Files except I don't think the $Action syntax works for those. So it would look more like:

<apex:repeat value="{!Record.ContentDocumentLinks}" var="cdl">
   <apex:image url="{!'/sfc/servlet.shepherd/version/download/'+cdl.ContentDocument.LatestPublishedVersionId}" 
     rendered='{!contains(cdl.ContentDocument.Name, "Product Image")}' width="100px"/>
</apex:repeat>

If you're not using a standard controller then just ensure the relevant fields are queried.

  • My PDF generator has a very large number of lines that look like this: <apex:image value="{!p.Product2.Product_Image__c}" styleClass="product-photo" rendered="{!NOT(ISBLANK(p.Product2.Product_Image__c))}"/> I do not want to have to go in and refactor every instance of this line to refer to attachment records when the solution I have works just fine. It's a simple text field. I have also already built a custom upload widget that solves this problem without touching rich text fields or attachments here: salesforce.stackexchange.com/a/194687/49567 – ag415 Oct 6 '17 at 19:35

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.