I got really tired of how ugly and inflexible Salesforce Reports are. So I made this Apex/Visualforce combo that searches long text fields, makes adding and removing fields a snap, has a customizable view, and is generally easy(er) to work with.This basically does everything by shuttling JSON strings back and forth between the view and the controller. If your browser doesn't have Javascript enabled, you're going to have a bad time.Other cool things:Works for any custom record type, and any custom field. You specify which in the view; theoretically, there's no need to touch any Apex, just good ol' HTML/Visualforce.Detects whether or not one of the fields by which you want to search is a picklist. If it is, it gets all the values for the picklist and prints them out in the DOM as a multi-select picklist. Ditto for checkboxes.It's super-easy to reference fields and make them appear in the view however you want.You can sort results just by clicking on column headers.Salesforce has two methods of searching for records: SOSL and SOQL. One's great for searching for text strings; the other one's great for everything else. This Apex class figures out automatically which one to use.<apex:page    controller  =SFReportsPlus    sidebar     =false    showHeader  =true    standardStyleSheets=true><!--Contact hello@robertakarobin.com with any questions!-->    <apex:form>        <apex:actionFunction            name            =  lookUpFields            action          ={!lookUpFields}            oncomplete      =renderFields(),                             $('#loading').slideUp(),                             $('#wrapper').slideDown()            reRender        =fieldsBlock, messageBlock        ><!--Specify the Salesforce Name of the record type you want to search. I use Leads here, but custom record types (and custom fields) are peachy.-->            <apex:param                name        =  recordName                assignTo    ={!recordName}                value       =Lead            /><!--Specify all the fields you want to involve here -- the fields by which you want people to search, and the fields you want to otherwise show in the view.--><!--This is clearly just a JSON map. The values are the actual Salesforce Names of the fields. The keys are easier-to-read headers that you create, and that you use to refer to the fields from now on.--><!--If you're getting a ton of null pointer exceptions, it's because you spelled field's Name wrong, or referenced a field that doesn't exist. Pardon my crappy error handling.-->            <apex:param                name        =  fieldsHeadersWithNamesJSONIn                assignTo    ={!fieldsHeadersWithNamesJSONIn}                value       ={                    'id'                        :'Id',                    'name'                        :'Name',                    'description'                        :'Description',                    'where_it_came_from'                        :'LeadSource'                }            /><!--Using the headers you specified above, specify the fields by which you want people to be able to search.-->            <apex:param                name        =  fieldsToSearchJSONIn                assignTo    ={!fieldsToSearchJSONIn}                value       =[                    'name',                    'where_it_came_from'                ]            /><!--Using the headers you specified above, specify the fields you want to show in the view.-->            <apex:param                name        =  fieldsToShowJSONIn                assignTo    ={!fieldsToShowJSONIn}                value       =[                    'id',                    'name',                    'where_it_came_from',                    'description'                ]            />        </apex:actionFunction>    </apex:form><!--You'll need to tweak this to reference jQuery properly.-->      <apex:includeScript value={!URLFOR($Resource.SLCA2__jQuery, 'jquery.min.js')} />    <script type=text/javascript>        window.onload = new function(){            lookUpFields();        }        var fieldsToShow;        var fieldsToSearch;        var fieldsWithOptions;        function prettify(input){            var output = ;                output += input.charAt(0).toUpperCase();                output += input.substr(1).replace(/_/g, );            return output;        }//This takes the values from multi-select picklists and concats them into a comma-separated string in hidden text inputs.        function selectedToCsv(select){            var values = [];            $(select).find(:selected).each(function(index, element){                values.push($(element).val());            });            $(select).prev(input).val(values.join(,));        }        function toggleFields(){            if($(#and).val().trim() == ){                $(#or, #not).prop(disabled,disabled);            }else{                $(#or, #not).prop(disabled,);            }        }               function renderFields(){            window.fieldsWithOptions                    = $.parseJSON($(#fieldsWithOptionsJSONOut).val());            window.fieldsToSearch                    = $.parseJSON($(#fieldsToSearchJSONOut).val());            window.fieldsToShow                    = $.parseJSON($(#fieldsToShowJSONOut).val());            window.results;            var fieldsInputs        = [],                fieldsHeaders       = [],                fieldsInputsHTML    = ,                fieldsHeadersHTML   = ,                fieldsToSearchHTML  = ,                numCols             = 5;            for(var x in fieldsToSearch){                var fieldHeader = fieldsToSearch[x],                    field       = fieldsWithOptions[fieldHeader],                    output      = ;                switch(field.type){                    case checkbox:                        output  = <input type=\checkbox\ id=\ + field.name                                + \ name=\ + field.name + \/>;                        break;                    case picklist:                        output  = <input type=\hidden\ name=\ + field.name + \>                                + <select multiple=\true\ id=\ + field.name                                + \ onchange=\selectedToCsv(this)\>;                        for(var optionValue in field.options){                            var optionLabel = field.options[optionValue];                            output  += <option value=\ + optionValue + \>                                    + optionLabel + </option>;                        }                        output  += </select>;                        break;                    default:                        output  =                            <input type=\text\ name=\ + field.name + \ />;                        break;                }                fieldsInputs.push(<td> + output + </td>);                fieldsHeaders.push(<th> + prettify(fieldHeader) + </th>);            }            for(var x in fieldsHeaders){                var counter = Number(x) + 1;                fieldsHeadersHTML   += fieldsHeaders[x];                fieldsInputsHTML    += fieldsInputs[x];                if(counter % numCols == 0 || counter >= fieldsHeaders.length){                    fieldsToSearchHTML  +=                         <tr> + fieldsHeadersHTML + </tr> +                        <tr> + fieldsInputsHTML + </tr>;                    fieldsHeadersHTML   = ;                    fieldsInputsHTML    = ;                }            }            $(#fieldsToSearch).html(fieldsToSearchHTML);        }        function getNewResults(){            window.results = $.parseJSON($(#resultsJSON).val());            renderResults();        }        function renderResults(){            var output = ;            $(#results).html();            for(x in window.results){                var result  = window.results[x],                    template = $(#resultTemplate).html();                output += template.replace(/\{([a-zA-Z_])+\}/g, function(substring){                    var varName = substring.substring(1, substring.length - 1);                    if(varName.indexOf(date) > -1){                        return result[varName].substring(0, 10);                    }else if(result[varName] == null){                        return ;                    }else{                        return result[varName];                    }                });            }            $(#results).html(output);            $('#loading').slideUp();            $('#resultsWrapper').slideDown();        }        var ascOrDesc = asc;        function orderBy(orderByThis){            window.results.sort(function(a,b){                if(a[orderByThis] > b[orderByThis]){                    return (window.ascOrDesc == asc? 1 : -1);                }else{                    return (window.ascOrDesc == asc? -1 : 1);                }            });            if(window.ascOrDesc == asc){                window.ascOrDesc = desc;            }else{                window.ascOrDesc = asc;            }            renderResults();        }    </script>    <style type=text/css>        .errors        {            color:red;          }        #wrapper *        {            box-sizing:border-box;          }        #wrapper table        {        width:100%;        }        #wrapper tr>*        {            vertical-align:top;        }        #wrapper th        {            background-color:#fda;            white-space:normal;        }        #fields select,        #fields input,        #fields textarea        {            width:100%;         }        #fields *[colspan]        {            max-width:none;         }        #fields td        {            padding-bottom:30px;        }        #resultsWrapper th[onclick]        {            cursor:pointer;            text-decoration:underline;        }        .hide{            display:none;        }        #loading span        {            display:inline-block;            vertical-align:bottom;            overflow:hidden;            animation: loading 1s ease-in-out 0s infinite alternate;            -webkit-animation: loading 1s ease-in-out 0s infinite alternate;        }        @keyframes loading        {            0% { width:5px;}            100% { width:30px; }        }        @-webkit-keyframes loading        {            0% { width:5px; }            100% { width:30px; }        }    </style>    <apex:sectionHeader title=SFReportsPlus />    <apex:outputPanel id=messageBlock>            <p class=errors>{!messageOut}</p>    </apex:outputPanel>    <div id=wrapper class=hide>        <apex:pageBlock id=fieldsBlock>            <apex:form>            <input                id      =  fieldsToSearchJSONOut                value   ={!fieldsToSearchJSONOut}                type    =hidden            />            <input                id      =  fieldsToShowJSONOut                value   ={!fieldsToShowJSONOut}                type    =hidden            />            <input                id      =  fieldsWithOptionsJSONOut                value   ={!fieldsWithOptionsJSONOut}                type    =hidden            />            <table id=fields>            <tbody id=fieldsToSearch></tbody>            <tr>                <th colspan=2>Search for records that contain...</th>                <th>ANY of these words:</th>                <th>OR any of these:</th>                <th>but NOT any of these:</th>            </tr>            <tr>                <td colspan=2><em>(Separate terms with commas, e.g. <kbd>John Smith, Graphic design, Graduate</kbd>)</em></td>                <td>                    <input                        name        =and                        id          =and                        type        =text                        onkeyup     =toggleFields()                    />                </td>                <td>                    <input                        name        =or                        id          =or                        type        =text                        disabled    =disabled                    />                </td>                <td>                    <input                        name        =not                        id          =not                        type        =text                        disabled    =disabled                    />                </td>            </tr>            <tr>                <td>                    <apex:commandButton                        value       =Search                        action      ={!doTheSearch}                        onclick     =$('#loading').slideDown()                        oncomplete  =getNewResults()                        reRender    =messageBlock, resultsJSONBlock                    />                </td>            </tr>            </table>            </apex:form>        </apex:pageBlock>        <apex:outputPanel id=resultsJSONBlock>            <input                id      =  resultsJSON                value   ={!resultsJSON}                type    =hidden            />        </apex:outputPanel>        <div id=resultsWrapper class=hide>            <apex:pageBlock>                <table>                    <thead>                        <tr>                            <th></th>                            <th onclick=orderBy('name')>Name</th>                            <th onclick=orderBy('where_it_came_from')>Lead source</th>                        </tr>                    </thead><!--This is where the results actually show up. This <tbody> is a template. For each result, Javascript repeats this template in the DOM. It expects everything inside curly brackets {blah} to correspond to one of the headers of the record you're looking up.--><!--Stuff in curly brackets with an exclamation mark {!blah} still refers to an actual Salesforce variable.-->                    <tbody id=resultTemplate class=hide>                        <tr>                            <td>                                <button onclick=$('#longText{id}').toggle() type=button>View</button>                            </td>                            <td><a href={!$Site.BaseUrl}/{id} target=_blank>{name}</a></td>                            <td>{where_it_came_from}</td>                        </tr>                        <tr id=longText{id} class=hide>                            <td colspan=8>{description}</td>                        </tr>                    </tbody>                    <tbody id=results>                    </tbody>                </table>            </apex:pageBlock>        </div>    </div>    <p id=loading>Loading<span>..........</span></p></apex:page>...and the Apex class:public with sharing class SFReportsPlus{    public String        messageOut      = '';    public String        recordName {get; set;}    public Schema.SObjectType        recordType;    public Map<String, Schema.SObjectField>        recordFields;    public SObject         record;    public String        fieldsHeadersWithNamesJSONIn {get; set;}    public Map<String, String>        fieldsHeadersWithNames;    public Map<String, Map<String, Object>>         fieldsWithOptions   = new Map<String, Map<String, Object>>();    public String        fieldsToSearchJSONIn {get; set;}    public List<String>        fieldsToSearch;    public String        fieldsToShowJSONIn {get; set;}    public List<String>        fieldsToShow;    public String getMessageOut(){        return      messageOut;    }    public String getFieldsHeadersWithNamesJSONOut(){        return      JSON.serializePretty(fieldsHeadersWithNames);    }    public String getFieldsToSearchJSONOut(){        return      JSON.serializePretty(fieldsToSearch);    }    public String getFieldsToShowJSONOut(){        return      JSON.serializePretty(fieldsToShow);    }    public String getFieldsWithOptionsJSONOut(){        return      JSON.serializePretty(fieldsWithOptions);    }    public PageReference lookUpFields(){        try{            recordType  = Schema.getGlobalDescribe().get(recordName);            record      = recordType.newSObject();            recordFields                        = recordType.getDescribe().fields.getMap();            fieldsHeadersWithNames  = (Map<String, String>)                    JSON.deserialize(                        fieldsHeadersWithNamesJSONIn.replaceAll('\'', ''),                        Map<String, String>.class                    );            fieldsToSearch = (List<String>)                    JSON.deserialize(                        fieldsToSearchJSONIn.replaceAll('\'', ''),                        List<String>.class                    );            fieldsToShow = (List<String>)                    JSON.deserialize(                        fieldsToShowJSONIn.replaceAll('\'', ''),                        List<String>.class                    );            for(String fieldLabel : fieldsHeadersWithNames.keySet()){                fieldsWithOptions.put(fieldLabel, returnFieldOptions(fieldLabel));            }        }catch(Exception e){            messageOut += 'Something went wrong getting the search fields: ' + e + '; ';        }        return null;    }    public Map<String, Object> returnFieldOptions(String fieldLabel){        String            fieldName   = fieldsHeadersWithNames.get(fieldLabel);        Schema.DescribeFieldResult            field       = recordFields.get(fieldName).getDescribe();        String            fieldType   = String.valueOf(field.getType());        Map<String, String>            fieldOptions                        = new Map<String, String>();        Map<String, Object>            output      = new Map<String, Object>();        if(            fieldType == 'PICKLIST'          ||fieldType == 'MULTIPICKIST'        ){                fieldType = 'picklist';                for(Schema.PicklistEntry option : field.getPicklistValues()){                    fieldOptions.put(option.getValue(), option.getLabel());                }        }else if(            fieldType == 'DATE'          ||fieldType == 'TIME'        ){                fieldType = 'time';        }else if(            fieldType == 'BOOLEAN'        ){                fieldType = 'checkbox';        }else{                fieldType = 'string';        }        output.put('name', fieldName);        output.put('type', fieldType);        output.put('options', fieldOptions);        return output;    }    public Map<String, String>        fieldsWithInputs {get; set;}    public String        logicString = '';    public String        conditionalString   = '';    public String        fieldsToShowString  = '';    public String        queryString;    public List<SObject>        results = new List<SObject>();    public List<Object>        resultsOut;    public String getResultsJSON(){        return JSON.serializePretty(resultsOut);        }    public String getQueryString(){        return queryString;    }    public PageReference doTheSearch(){        messageOut  = '';        resultsOut  = new List<Object>();        try{            fieldsWithInputs = ApexPages.currentPage().getParameters();            buildLogicString();            buildFieldsToShowString();            buildConditionalString();            if(fieldsWithInputs.get('and') == ''){                results = doSOQLSearch();            }else{                results = doSOSLSearch();            }            for(Integer x = 0; x < results.size(); x++){                SObject                    result      = results.get(x);                Map<String, Object>                    resultOut   = new Map<String, Object>();                for(String fieldHeader : fieldsToShow){                    String fieldName = fieldsHeadersWithNames.get(fieldHeader);                    resultOut.put(fieldHeader, result.get(fieldName));                }                resultsOut.add(resultOut);            }        }catch(Exception e){            messageOut += 'Something went wrong looking up records: ' + e + '; ';           }        return null;    }    public List<SObject> doSOQLSearch(){        queryString = 'SELECT ' + fieldsToShowString                    + ' FROM ' + recordName;        if(conditionalString != ''){            queryString                    +=' WHERE (' + conditionalString + ')';        }        List<SObject> SOQL = Database.query(queryString);        return SOQL;    }    public List<SObject> doSOSLSearch(){        queryString = 'FIND \'(' + logicString + ')\''                    + ' RETURNING ' + recordName                    + ' (' + fieldsToShowString;        if(conditionalString != ''){            queryString                    +=' WHERE (' + conditionalString + ')';        }            queryString                    +=')';        List<List<SObject>> SOSL = Search.query(queryString);        return SOSL[0];    }    public void buildLogicString(){        logicString = '(';        logicString += '(' + fieldsWithInputs.get('and').replaceAll(',',' AND ') + ')';        if(String.isNotBlank(fieldsWithInputs.get('or'))){            logicString += ' OR (' + fieldsWithInputs.get('or').replaceAll(',',' OR ') + ')';        }        logicString += ')';        if(String.isNotBlank(fieldsWithInputs.get('not'))){            logicString += ' AND NOT (' + fieldsWithInputs.get('not').replaceAll(',',' OR ') + ')';        }    }    public void buildFieldsToShowString(){        List<String> output = new List<String>();        for(String fieldHeader : fieldsToShow){            output.add(fieldsHeadersWithNames.get(fieldHeader));            }        fieldsToShowString = join(output, ', ');    }    public void buildConditionalString(){        List<String> outputsList    = new List<String>();        for(String fieldHeader : fieldsToSearch){            String fieldName = fieldsHeadersWithNames.get(fieldHeader);            if(fieldsWithInputs.get(fieldName) == null             ||fieldsWithInputs.get(fieldName) == ''            ){                continue;            }            String  outputString;            String  inputString = fieldsWithInputs.get(fieldName);            List<String>                    inputList   = inputString.split(',', 0);            if(inputList.size() == 1){                outputString =                     fieldName + ' = \'' + inputString + '\'';            }else{                outputString =                    fieldName + ' IN (\'' + join(inputList,'\',\'') + '\')';            }            outputsList.add('(' + outputString + ')');        }        conditionalString = join(outputsList, ' AND ');    }    public String join(List<String> values, String delimeter){        String output = '';        for(Integer x = 0; x < values.size(); x++){            output += values.get(x);            if(x < values.size() - 1){                output += delimeter;            }        }        return output;    }}On GitHub
Salesforce Report that searches long text fields, has customizable view, easy to update, etc
salesforce apex