locked
[Forum FAQ] A content management tool with dashboard based on SharePoint List RRS feed

  • General discussion


  • 1.  Scenario:

    The SharePoint OOTB List has saved us a lot of time on managing mess data. It provides three forms to create/view/edit items, the ability to save the views we want with some specific filtering and sorting condition, versioning for easy restoring, and we can make it advanced with workflow contains the specific business logic.

    However, if there is a need for better user experience, interacting with the public APIs and a bit of script to customize the web page would be required.

    Suppose there is a requirement like this:

    1.        We need a content collection tool which collects ideas from contributors, the newly ideas will be reviewed by reviewers.
    2.        We may need to filter the list in a convenient way, get the wanted result with the data from the list and display in a chart or rank list. 

    We can add some buttons in Metro style to display the counting result of the data from the list dynamically. When we click them, the list will be filtered and sorted to display a friendly set of items. Also, we need to display a trend of the mess data graphically in some beautiful charts.  If we want to find out some outstanding contributors, top contributor board would be more comfortable than the top N items in the OOTB list view.

    The page would look like this:

    2.  Introduction:

    Engineers will come up with some ideas in the daily job and write a content to enlighten others. Reviewers will help to review ideas or contents and publish the contents if qualified.

    The complete process looks like this:


    As we can see, only the approved idea can be written as a content and only the approved content can be published.

    2.1 How it works

    We build the whole tool in one page. All ideas and contents will be saved in a custom list. This is how it looks like:

    There are three parts in this page:

    1       

    2       

    2.1       

    2.1.1        Top menu

    The top menu contains three elements:

    A Drop Down menu for filtering data by team, it will refresh the other two parts with the filtered data:


    A hyperlink “STATISTIC” links to a PowerBI report whose data source is the custom list.

    A hyperlink “FEEDBACK” for collecting feedbacks:


    The feedbacks will be saved in another list:


    2.1.2        Information menu

    This part will display the calculated data retrieved from the list within tiles, chart and ranking list.

    The tiles can be clicked to filter and refresh the list view.


    2.1.3        List view

    A list stores all ideas and contents with the properties needed. It can be filtered by the Top menu and Information menu.

    The customization on the OOTB custom list template makes it more powerful and more suit for this scenario:


    1. An item leveled comment feature (based on OOTB Tags & Notes feature) for other users make comments to an idea or content:


    2. Title column: When there is no attachment in the current item, it redirects to the default DisplayForm page. If there is, it will open the attachment (usually a .docx file) in Word Online in a new tab.

    3. ECB menu: Add some custom shortcuts for popular actions:


    4. A hyperlink column stores the hyperlink points to the website where the content is published to.

    3.    How to achieve it

    This solution will be hosted in SharePoint Online environment, so we do all the job using JavaScript, REST API and Client Object Model.

    The Drop Down menu, tiles, rank list are generated with some HTML+CSS.

    The Trend Chart, we take advantage of the Combo chart in the Google chart library.  

    The list view is hosted in a <iframe> which can be easily filtered and refreshed by just passing a generated URL with query string.

    For the customization on the list view and the ECB menu, JSLink with Client Object Model would be OK.

    3.1 Specific to every part

    3.1.1        Top menu

    3.1.1.1  Drop Down menu for retrieving filtered data and refreshing the display of the related controls

    When user selects a team here, there will be a request sent out for retrieving items of the list. By default, the limit is 100 when using REST API to get list items, so we can append a “$top=1000” to require more items from server.

    Code snippet like this:

    $.ajax({
        url: _spPageContextInfo.webAbsoluteUrl + "/_api/web/lists/getbytitle('" + listName + "')/items?$top=1000",
        method: "GET",
        headers: { "Accept": "application/json; odata=verbose" },
        success: function (data) {
        	console.log("getListItems succ");
            console.log(data);        
        },
        error: function (data) {
        alert("getListItems error");
            //failure(data);
        }
    });



    Then we will get the “data” as a JSON format string, it contains all the values we need from the list:


    We can get the values we want like this:

    //get item Count
    var arr = [], len;
    for(key in data.d.results) 
    {
        arr.push(key);
    }
    len = arr.length;
    
    for(var ii=0; ii<len; ii++)
    {
    	var team = data.d.results[ii].Team;
    	var month = data.d.results[ii].Month;
    }



    As we need to know the counts of each type of ideas or contents, we use an array for saving the counters:

    //ary to store all counters for tiles: all/pendingIdea/pendingContent/my/approvedIdea/approvedContent
    var aryAllCounters = [0,0,0,0,0,0];
    for(var ii=0; ii<len; ii++)
    {
    	//get pendingIdeaCount
        if(data.d.results[ii].Statuss === 'Pending')
        {
            aryAllCounters[1]++;
        }
    }



    Once all the numbers are ready, we can do the refreshing.

    As the list view page is hosted in a <iframe>, all we need to do is passing a constructed URL with query string:

    url_team = URL + "?FilterField1="+FIELD_MYTEAM+"&FilterValue1=" + sel_val;
    $iframe.attr('src', url_team);



    3.1.1.2  Hyperlink for popping up a dialog to collect feedbacks

    The feedback dialog hosts another page which contains two buttons and one text area.

    The HTML code of the FEEDBACK button:

    <a id="feedback" href="#" onclick="javascript:openDialogBox('../SitePages/Feedback.aspx');">FEEDBACK</a>


    The openDialogBox() function:

    function openDialogBox(url){     
    	var options = SP.UI.$create_DialogOptions();
    	options.url = url;
    	options.height = 130;
    	options.width = 425;
    	options.title = "Feedback";
    	SP.UI.ModalDialog.showModalDialog(options);
    }


    In the Feedback.aspx page, when user click submit button, we will save the content of the text area into the feedback list:

    function addListItem() 
    {
    	this.clientContext = new SP.ClientContext.get_current();
        this.oList = clientContext.get_web().get_lists().getByTitle('Feedback');
    
        var itemCreateInfo = new SP.ListItemCreationInformation();
        this.oListItem = this.oList.addItem(itemCreateInfo);
    
        //set person field
        var userValue = new SP.FieldUserValue();
        //userValue.set_lookupId(this.currentUser.get_id());
        userValue.set_lookupId(_spPageContextInfo.userId);
        oListItem.set_item('Provider', userValue);
    
        //Sets the specified field value
        oListItem.set_item('Title', str);
    
        //datetime field
        var currDate = new Date();
        oListItem.set_item('Submit_Time',currDate);
        
        oListItem.update();
        clientContext.executeQueryAsync(Function.createDelegate(this, this.onQuerySucceeded_add), Function.createDelegate(this, this.onQueryFailed));
    }



    3.1.2        Information menu

    3.1.2.1  Tile shortcut

    In the click event of the tiles, the code will pass a generated URL with query string to the <iframe>:

    //filter list only
    $tile.click(function(){
    
    	//distinguish tiles by id
        var v = $(this).attr('id');      
        switch(v)
        {   
        case S_MY_CONTENT:
        url_team1 = URL + "?FilterField1="+FIELD_COMPOSER+"&FilterValue1=" + currentUsername;
        break;
    
        //...
    
        case S_PENDING_IDEA:
        url_team1 = url_team + "&FilterField2="+FIELD_STATUS+"&FilterValue2=Pending&FilterField3="+FIELD_IDEATYPE+"&FilterValue3=Idea";    
        }
        break;   
        $iframe.attr('src', url_team1);    
    });



    3.1.2.2  Trend chart

    The chart will be initialized with the numbers by month stored in a 3D array:

    google.load("visualization", "1", {packages:["corechart"]});
    google.setOnLoadCallback(drawVisualization);
    function drawVisualization(ary) 
    {
    	// Some raw data (not necessarily accurate)
    	var data = google.visualization.arrayToDataTable(ary);
    
    	var view = new google.visualization.DataView(data);
    	view.setColumns([0, 1,
    	               { calc: "stringify",
    	                 sourceColumn: 1,
    	                 type: "string",
    	                 role: "annotation" 
    	                 
    	                 },
    	               2]);
    
    	// Create and draw the visualization.
    	var ac = new google.visualization.ComboChart(document.getElementById('chart1'));
    	ac.draw(view, {
    		//legend: 'top',
    		legend: {     
    		title : '',
    		//width: 0,
    		//height: 285,
    		vAxis: {title: "", format:'#',viewWindowMode:'explicit',
    	          viewWindow:{
    	            min:0
    	          },ticks: ticks
    		},
    		//hAxis: {title: ""},
    		lineWidth: 4,
    		bar: {groupWidth: "60%"},
    		seriesType: "bars",
    		series: {1: {type: "line"}},
    		chartArea:{
    		colors: ['#A4C400', '#F9A13B']
    	});
    }



                   

    3.1.2.3  Top contributors rank list

    When retrieving list items, we can get the “AuthorId” which represents the id of the user in the siteUserInfoList. We run another request to retrieve all items in the siteUserInfoList which stores the username with the URL of profile.

    Then we can use a hash table(provided by jshashtable.js) to store the user id, username and profile URL:

    $.ajax({
        url: _spPageContextInfo.webAbsoluteUrl + "/_api/web/siteUserInfoList/Items",
        method: "GET",
        headers: { "Accept": "application/json; odata=verbose" },
        success: function (data) {
            console.log(data);
    
            //get item Count
            var arr = [], len;
            for(key in data.d.results) 
            {
                arr.push(key);
            }
            len = arr.length;
    
            var ht_authors = new Hashtable();
            for(var ii=0; ii<len; ii++)
            {       
                if(authorSet.contains(data.d.results[ii].Id))
                {
                    if(data.d.results[ii].Picture != null)
                    {
                    	ht_authors.put(data.d.results[ii].Id, data.d.results[ii].Title+'|'+data.d.results[ii].Picture.Url);
                    }
                    else
                    {
                    	ht_authors.put(data.d.results[ii].Id, data.d.results[ii].Title+'|');
                    }                
                }
            }
            console.log("ht_authors.keys(): "+ht_authors.keys());
            console.log("ht_authors.values(): "+ht_authors.values());
        },
        error: function (data) {
        alert("error");
            //failure(data);
        }
    });



    3.1.3        List view

    For the Comment button, custom title link and the custom published link of each item, we can use JSLink to achieve.

    Comment button: It is supposed to be the OOTB “Type” column, I change the icon and modify the click event of it to pop up a comment dialog which take advantage of the OOTB “Tags&Notes” feature;

    Custom Title link: As there will be two situations of an item: has attachment or not. We will need to run a request to get the URL of attachment and change the hyperlink of the Title field accordingly:


    (function () {
    
        // Create object that have the context information about the field that we want to change it output render  
        var linkFiledContext = {};
        linkFiledContext.Templates = {};
        linkFiledContext.Templates.Fields = {        
            //"Attachments": { "View": AttachmentsFiledTemplate }
            "LinkTitle": { "View": TitleFieldTemplate }, 
            "Published_x0020_Link": { "View": PublishedLinkFieldTemplate },
            "DocIcon": { "View": DocIconFieldTemplate },
            "MyTeam": { "View": MyTeamFieldTemplate }
        };
        SPClientTemplates.TemplateManager.RegisterTemplateOverrides(linkFiledContext);
    })();
    
    function DocIconFieldTemplate(ctx)
    {
        var htmlStr = "";
        var listId = ctx.listName;
        var itemId = ctx.CurrentItem.ID;
        var s = listId + "," + itemId;
        htmlStr += "<img width='16' height='16' class=' ms-draggable' alt='Comment' src='"+_spPageContextInfo.webAbsoluteUrl+"/Shared%20Documents/img/comment-icon.png' border='0' ms-draggableragId='0' onclick='CommentIcon(\""+ s +"\")'></img>";
        return htmlStr;
    }
    
    function CommentIcon(s)
    {
        var listId = s.split(',')[0];
        var itemId = s.split(',')[1];
        var url=_spPageContextInfo.webAbsoluteUrl+"/_layouts/15/socialdataframe.aspx?listid="+listId+"&id="+itemId+"&mode=1";
        console.log(url);
        openCustomDialog(url,"Comment",650,520);
    }
    
    function openCustomDialog(pageUrl,title,width,height) 
    {  
       SP.UI.ModalDialog.showModalDialog({  
           url: pageUrl,
           width: width,
           height: height,
           title: title,
           dialogReturnValueCallback: function (result){
                if(result== SP.UI.DialogResult.OK)
                { 
                    parent.window.location.href=parent.window.location.href;         
                }   
           }       
        });  
    }
    
    function PublishedLinkFieldTemplate(ctx)
    {
        //console.log(ctx);
        var htmlStr = "";
        var itemPublishedLink = "";
        var itemPublishedLinkDesc = "";
        if((ctx.CurrentItem.Published_x0020_Link != ''))
        {
            itemPublishedLink = ctx.CurrentItem.Published_x0020_Link;
            itemPublishedLinkDesc = ctx.CurrentItem["Published_x0020_Link.desc"];
            htmlStr = "<a href='" + itemPublishedLink + "' target='_blank'>" + itemPublishedLinkDesc + "</a>";
        }
        return htmlStr;
    }
    
    
    function MyTeamFieldTemplate(ctx)
    {
        var htmlStr = "";
        var itemMyTeam = "";
        if((ctx.CurrentItem.MyTeam[0] != undefined) && (ctx.CurrentItem.MyTeam[0] != null))
        {
            itemMyTeam = ctx.CurrentItem.MyTeam[0].lookupValue;
            htmlStr = itemMyTeam;
        }
        return htmlStr;
    }
    
    function TitleFieldTemplate(ctx) {
        console.log(ctx.CurrentItem);
        var itemId = ctx.CurrentItem.ID;
        var itemTitle = ctx.CurrentItem.Title;
        var listName = ctx.ListTitle;
        var siteUrl = _spPageContextInfo.webAbsoluteUrl;
        var listUrl = _spPageContextInfo.webAbsoluteUrl + "/Lists/" +listName;
        
        var fileNames = getAttachmentsNames(listName, itemId); 
        console.log(fileNames);   
        var fileNameAry = fileNames.split("|");
        var htmlStr = "";
    
        //check the attachment existence
        if(fileNameAry[0] != '')
        {   
            for(var j = 0; j < fileNameAry.length; j++)
            {
                var fileName = fileNameAry[j];
                
                var s1 = "<a class=\"ms-listlink ms-draggable\" onmousedown=\"return VerifyHref(this, event, '1', 'SharePoint.OpenDocuments.3', '1";
                //1``https://microsoft.sharepoint.com/teams/spfrmcs
                var s2 = "/_layouts/15/WopiFrame.aspx?sourcedoc=";
                //2``/teams/spfrmcs/Lists/Content%20Pool
                var s3 = "/Attachments/";
                //3``137
                var s4 = "/";
                //4``[Forum FAQ] Highlight the list tab in Quick Launch when the list view changes.docx
                var s5 = "&action=default'); return false;\" href=\"";
                //5``https://microsoft.sharepoint.com/teams/spfrmcs/Lists/Content Pool
                var s6 = "/Attachments/";
                //6``137
                var s7 = "/";
                //7``[Forum FAQ] Highlight the list tab in Quick Launch when the list view changes.docx
                var s8 = "\" target=\"_blank\" DragId=\"1\">";
                //8``Highlight the list tab in Quick Launch when the list view changes
                var s9 = "</a>";
                var s = s1+siteUrl+s2+listUrl+s3+itemId+s4+fileName+s5+listUrl+s6+itemId+s7+fileName+s8+itemTitle+s9;
                htmlStr += s; 
    
                //console.log(htmlStr);
                if (j != fileNameAry.length - 1) 
                {
                   htmlStr += "<br/>";
                }
            }
        }
        //if no attachments, set the <a> point to displayForm
        else
        {
            htmlStr += "<a class='ms-listlink ms-draggable' onclick='EditLink2(this,28);return false;' onfocus='OnLink(this)' href='" + siteUrl + "/_layouts/15/listform.aspx?PageType=4&ListId=%7BE54A4FBB%2DDDC2%2D4F7E%2D8343%2D8A1C78757CF4%7D&ID=" + itemId + "&ContentTypeID=0x010079A1D928FF77984C80BFEF1D65C3809F' target='_blank' DragId='0'>" + itemTitle + "</a>";
        }
    
        return htmlStr;
    }
    
    function getAttachmentsNames(listName,itemId) {
      
        var url = _spPageContextInfo.webAbsoluteUrl;
        var requestUri = url + "/_api/web/lists/getbytitle('" + listName + "')/items(" + itemId + ")/AttachmentFiles";
        var str = "";
        // execute AJAX request
        $.ajax({
            url: requestUri,
            type: "GET",
            headers: { "ACCEPT": "application/json;odata=verbose" },
            async: false,
            success: function (data) {
                for (var i = 0; i < data.d.results.length; i++) 
                { 
                    if(i != 0)
                    {
                        str += "|";
                    }
                    str += data.d.results[i].FileName;              
                }          
            },
            error: function (err) {
                //alert(err);
            }
        });
        return str;
    }



     

    3.2 How to make them work together

    When selecting an option in the Drop Down menu, the Information menu and the List view will be refreshed separately.

    When clicking the tiles, only the list view will be filtered and refreshed, the other parts will not be influenced.

    When items created/modified, the whole page will be refreshed to keep all the numbers in each part updated.  A workflow will also be triggered to inform engineers or reviewers the progress of an item or content.

     

    3.3 Other customizations

    3.3.1        ECB menu and permission control

    As we need to refresh the page when new item or modify item, we put all the form pages in a custom modal dialog and execute the refresh in the success callback function.

    There are three roles: Site owner, reviewer and engineer. They have limited privileges according to the roles they are:

    Site owner: Full control on the list, can see all the buttons in the ECB menu;


    Reviewer: There is another list which stores the names of each team and reviewers’ names of each team. The reviewer has limited full control only on the team they belong to. To other teams, the role can be seen as a visitor;

    Composer (create owner): The one who contribute an idea. For the ideas\contents from other teams, this role can be seen as visitor.

    The ECB menu they can see is:


    For the visitor, the ECB menu will only display a few buttons:


     

    The code:

    (function () {
        var viewContext = {};
        viewContext.Templates = {};
        viewContext.OnPostRender = OnViewPostRender;
        SPClientTemplates.TemplateManager.RegisterTemplateOverrides(viewContext);
    })();
    
    function OnViewPostRender(ctx) {        
        $("a[title='More options']").removeAttr("onclick");
        $(".ms-list-itemLink").removeAttr("onclick");
        $("a[title='More options']").attr("onclick", "showMenuList(this);return false;");
    }
    
    function showMenuList(obj) {
    
        var itemId = $(obj).parents("tr").attr("id").split(",")[1];
        //show ECB menu                  	        
        CoreInvoke('ShowECBMenuForTr', obj, event);
    
        var teamId = getCurrentTeamId("Content Pool", itemId);
        var styles = "";
        if (isSiteOwner("Technet SharePoint Team Owners")) {
    
            styles = "li[text='Delete Item ']{display:block;} li.ms-core-menu-separator:last-child{display:block;} ul.ms-core-menu-list > li:nth-last-child(5){display:block;} li[text='Edit Item ']{display:block;} li[text='Upload Document']{display:block;} li[text='Approve']{display:block;} li[text='Reject']{display:block;} li[text='Add Publish Link']{display:block;}";
    
        } else if (isReviewer("List1_FAQ_team", teamId, "Reviewers")) {
    
            styles = "li[text='Delete Item ']{display:block;} li.ms-core-menu-separator:last-child{display:block;} ul.ms-core-menu-list > li:nth-last-child(5){display:block;} li[text='Edit Item ']{display:block;} li[text='Upload Document']{display:block;} li[text='Approve']{display:block;} li[text='Reject']{display:block;} li[text='Add Publish Link']{display:block;}";
    
        } else if (isComposer(obj)) {
    
            styles = "li[text='Delete Item ']{display:block;} li.ms-core-menu-separator:last-child{display:block;} ul.ms-core-menu-list > li:nth-last-child(5){display:block;} li[text='Edit Item ']{display:block;} li[text='Upload Document']{display:block;} li[text='Approve']{display:none;} li[text='Reject']{display:none;} li[text='Add Publish Link']{display:none;}";
    
        } else {
    
            styles = "li[text='Delete Item ']{display:none;} li.ms-core-menu-separator:last-child{display:none;} ul.ms-core-menu-list > li:nth-last-child(5){display:none;} li[text='Edit Item ']{display:none;} li[text='Upload Document']{display:none;} li[text='Approve']{display:none;} li[text='Reject']{display:none;} li[text='Add Publish Link']{display:none;}";
    
        }
    
        includeStyleElement(styles);
    }
    
    //get current team id
    function getCurrentTeamId(listName,itemId){
    	var teamId="";
    	 var requestUri = _spPageContextInfo.webAbsoluteUrl +
                      "/_api/Web/Lists/getByTitle('"+listName+"')/items("+itemId+")?$select=MyTeamId";
        
        // execute AJAX request
        $.ajax({
            url: requestUri,
            type: "GET",
            headers: { "ACCEPT": "application/json;odata=verbose" },
            async: false,
            success: function (data) {
               if(data.d.MyTeamId!=null){
               		teamId=data.d.MyTeamId;
               }else{
               		teamId="0";
               }              
            },
            error: function () {
                //alert("Failed to get details");
            }
        });
    
    	return teamId;	
    }
    
    //check whether is owner
    //Technet SharePoint Team Owners
    function isSiteOwner(groupName) {
        var flag = false;
        var requestUri = _spPageContextInfo.webAbsoluteUrl + "/_api/Web/effectiveBasePermissions";
    
        // execute AJAX request
        $.ajax({
            url: requestUri,
            type: "GET",
            headers: { "ACCEPT": "application/json;odata=verbose" },
            async: false,
            success: function (data) {
                var permissions = new SP.BasePermissions();
                permissions.fromJson(data.d.EffectiveBasePermissions);
                flag = permissions.has(SP.PermissionKind.managePermissions);           
            },
            error: function () {
                //alert("Failed to get details");
            }
        });
    
        return flag;
    }
    
    function isComposer(obj) {
        var flag = false;
        var userId = _spPageContextInfo.userId;
        var composerId = $(obj).parents("tr").find("a[href*='userdisp.aspx']").attr("href").split("ID=")[1];
        if (composerId == userId) {
            flag = true;
        }
        return flag;
    }
    
    //check whether is reviewer
    function isReviewer(listName,teamId,peopleColumn){
    	var flag=false;   		
    	var userId=_spPageContextInfo.userId;
    	  		   		
    	// begin work to call across network
        var requestUri = _spPageContextInfo.webAbsoluteUrl +
                      "/_api/Web/Lists/getByTitle('"+listName+"')/items?$select=ID&$filter=(ID eq '"+teamId+"' and "+peopleColumn+"Id eq '"+userId+"')";
        
        // execute AJAX request
        $.ajax({
            url: requestUri,
            type: "GET",
            headers: { "ACCEPT": "application/json;odata=verbose" },
            async: false,
            success: function (data) {
               if(data.d.results.length>0){
               		flag=true;
               }           
            },
            error: function () {
                //alert("Failed to get details");
            }
        });
           		
    	return flag;
    }
    
    //insert style into page 
    function includeStyleElement(styles) {	
    	var style = document.createElement("style");	
    	style.type = "text/css";
    	(document.getElementsByTagName("head")[0] || document.body).appendChild(style);
    	if (style.styleSheet) { 
    	    //for ie
    		style.styleSheet.cssText = styles;
    	} else {
    	    //for w3c
    		style.appendChild(document.createTextNode(styles));
    	}
    }


    3.3.2        Workflow email customization

    The email will only be sent to engineer or team reviewer in the three scenarios:

    When engineer uploads an idea or content, reviewer will receive an email;

    When engineer uploads a content to an existing idea, reviewer will receive an email;

    When reviewer approve/reject an idea or content, engineer will receive an email;

    The design of the workflow process  :


     The email design like this:

    Email to engineer


    Email to reviewer


    Let us know if you are interested in it. Happy coding!



    Please click to vote if the post helps you. This can be beneficial to other community members reading the thread.


    • Edited by ForumFAQ Friday, August 8, 2014 6:25 AM
    Friday, August 8, 2014 6:20 AM

All replies

  • A good solution. Liked it very much. Can you please make it a technet blog for others.

    sudipmisra@hotmail.com

    Wednesday, December 3, 2014 8:40 PM