Umbraco backoffice listview + infinite editing - part 2

jemayn

Jesper Mayntzhusen

Posted on April 14, 2021

Umbraco backoffice listview + infinite editing - part 2

Hi my name is Jesper 👋

I've recently started a new job as an Umbraco developer at a company called Limbo. I recently had to learn how to create a custom listview that uses infinite editing and thought I would share.

In part 1 went through how to create a dashboard and pull some data into it, which means the next step starting in this blogpost is presenting that data.

Step 4 - Presenting the data nicely in a listview!

We haven't yet gotten all the data we want to present, but it's all more of the same - so before we do that let's present the data in a better way than a massive json blob!

Our starting point for the view is this:

dashboard.html

<div ng-controller="MyDashboardController as vm">
    <h1>Welcome to my dashboard!</h1>

    <umb-box ng-if="vm.weHaveRecords">
        <umb-box-content>
            <!-- The following is a super nice way to just get an overview of the data you get and make sure the data is there! -->
            {{vm.records | json}}
        </umb-box-content>
    </umb-box>

    <umb-box ng-if="!vm.weHaveRecords">
        <umb-box-content>
            No records at this point!
        </umb-box-content>
    </umb-box>

    <umb-load-indicator ng-if="vm.loading">
    </umb-load-indicator>
</div>
Enter fullscreen mode Exit fullscreen mode

So far we have managed to get by with using pretty much only built in directives - unfortunately that is not possible for a listview - but don't worry, we can write markup and use classes already included with the backoffice so we don't need to do any css!

We will leave the fallback umb-box that appears if we have no records, as well as the umb-load-indicator as that is just general good practice, and instead focus on replacing the top umb-box with a listview - first we will build the scaffold:

The basic structure of a table in the backoffice is this:

<div class="umb-table">
    <div class="umb-table-head">
        ...
    </div>
    <div class="umb-table-body">
        ...
    </div>
</div>
Enter fullscreen mode Exit fullscreen mode

The table head has this structure:

<div class="umb-table-head">
    <div class="umb-table-row">
        <div class="umb-table-cell">Normal header</div>
        <div class="umb-table-cell umb-table__name">Name header</div>            
    </div>
</div>
Enter fullscreen mode Exit fullscreen mode

The name header is slightly wider, but is otherwise the same.

The table body has this structure:

<div class="umb-table-body">
    <div class="umb-table-row"> 
        <div class="umb-table-cell">Regular row</div>
        <div class="umb-table-cell umb-table__name">Name row</div>
    </div>
    <div class="umb-table-row -selectable">
      ...
    </div>
</div>
Enter fullscreen mode Exit fullscreen mode

For the body each row has the optional -selectable class that gives all the visual cues to make it look clickable.
Additionally the name rows are still wider but also in bold.

The final thing to know is that the first column in a table is by default a lot narrower than the others, as it's intended for icons.

If we fill out the structure with some dummy data we get something like this:

<div class="umb-table" ng-show="!vm.loading && vm.weHaveRecords">
    <div class="umb-table-head">
        <div class="umb-table-row">
            <div class="umb-table-cell">This is a table header</div>
            <div class="umb-table-cell umb-table__name">This is a highlighted table header</div>
            <div class="umb-table-cell">This is a table header</div>
            <div class="umb-table-cell">This is a table header</div>
        </div>
    </div>

    <div class="umb-table-body">
        <div class="umb-table-row -selectable">
            <div class="umb-table-cell">Table row data</div>
            <div class="umb-table-cell umb-table__name">Highlighted table row data</div>
            <div class="umb-table-cell">Table row data</div>
            <div class="umb-table-cell">Table row data</div>
        </div>

        <div class="umb-table-row -selectable">
            <div class="umb-table-cell">Table row data</div>
            <div class="umb-table-cell umb-table__name">Highlighted table row data</div>
            <div class="umb-table-cell">Table row data</div>
            <div class="umb-table-cell">Table row data</div>
        </div>
    </div>
</div>
Enter fullscreen mode Exit fullscreen mode

Which gives this result:

image

It is very clear the first icon column shouldn't have text, but it's all fine for now - let's move on to populating our table with our content awaiting a scheduled publishing!

First we will rewrite our table head a little bit to have the relevant data:

<div class="umb-table-head">
    <div class="umb-table-row">
        <!-- We leave this first on empty in the header & show icons in the body -->
        <div class="umb-table-cell"></div>
        <div class="umb-table-cell umb-table__name">Name</div>
        <div class="umb-table-cell">Publish time</div>
        <div class="umb-table-cell">Culture</div>
    </div>
</div>
Enter fullscreen mode Exit fullscreen mode

Then we move onto the body where we will loop through and generate rows of content:

<div class="umb-table-body">
    <div class="umb-table-row -selectable" ng-repeat="item in vm.records">
        <div class="umb-table-cell">
            <i class="umb-table-body__icon umb-table-body__fileicon icon-document" ng-if="item.ScheduleInfo.FullSchedule[0].Action == 0"></i>
            <i class="umb-table-body__icon umb-table-body__fileicon icon-document-dashed-line color-grey" ng-if="item.ScheduleInfo.FullSchedule[0].Action == 1"></i>
        </div>
        <div class="umb-table-cell umb-table__name">{{item.Content.Name}}</div>
        <div class="umb-table-cell">{{item.ScheduleInfo.FullSchedule[0].Date | date : medium}}</div>
        <div class="umb-table-cell">{{item.ScheduleInfo.FullSchedule[0].Culture}}</div>
    </div>
</div>
Enter fullscreen mode Exit fullscreen mode

So we loop through all the records in vm.records with the ng-repeat statement in the row div, then for each table cell we output a value based on our content.

First either a full document icon if the event is a publish event, otherwise a dashed line grey version if it's an unpublish event.

Then we get the Name for the 2nd column, the publish date using the AngularJS built in date filter, and finally the culture for the specific content event:

image

I've added 2 new events - an unpublish of the Contact node (notice the icon changed), and a publish of the Afrikaans culture of the Blog node.

Step 5 - Supporting paged listviews!

At this point the listview pretty much lists what we want. One thing is not optimal - if fx several cultures of the same node has a scheduled publish then we only show the first one since we in the view use FullSchedule[0] - could do that in a smarter way, but will leave it like this for now.

The final step missing for the listview now is to add the pagination component - luckily this part is very easy to use as an Umbraco component:

<div class="umb-table" ng-show="!vm.loading && vm.weHaveRecords">
    <div class="umb-table-head">
        ...
    </div>

    <div class="umb-table-body">
        ...
    </div>
</div>

<div class="flex justify-center" ng-show="!vm.loading && vm.weHaveRecords">
    <umb-pagination page-number="vm.pagination.pageNumber"
                    total-pages="vm.pagination.totalPages"
                    on-next="vm.nextPage"
                    on-prev="vm.prevPage"
                    on-change="vm.changePage"
                    on-go-to-page="vm.goToPage">
    </umb-pagination>
</div>
Enter fullscreen mode Exit fullscreen mode

For this to work we need to make a few other changes. First of all let's make the API controller paged instead of just returning everything - it is fine for now with only a few results, but could be hundreds potentially.

We will add an offset and limit as parameters, and then we will add a skip and take on the result set:

public List<ContentWithScheduledInfo> GetContentForReleaseNextWeek (int offset = 0, int limit = 10)
{
    ...

    var pagedContent = new PagedContent
    {
        // filter the result set with offset and limit
        ScheduledNodes = contentWithScheduledInfo.Skip(offset).Take(limit).ToList(),
        TotalRecords = contentWithScheduledInfo.Count
    };
}

// add new model to return
public class PagedContent
{
    public List<ContentWithScheduledInfo> ScheduledNodes { get; set; }
    public int TotalRecords { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

At this point we've changed the output so now the JS response will need to be edited - this is the new model output:

image

Quite a few changes to the JS controller, in short we are doing the following:

  • Add offset and limit to our getContentForRelease function
  • Create the nextPage, prevPage, changePage, goToPage functions needed for the pagination
  • A bit of logic for calculating the total pages and figuring out the pagination
function DashboardController($http) {

    var vm = this;

    function init() {
        getContentForRelease(0, vm.recordsPerPage);            
    }

    vm.nextPage = nextPage;
    vm.prevPage = prevPage;
    vm.changePage = changePage;
    vm.goToPage = goToPage;
    vm.recordsPerPage = 2;

    function nextPage(pageNumber) {
        vm.loading = true;
        var offset = (pageNumber - 1) * vm.recordsPerPage;
        getContentForRelease(offset, vm.recordsPerPage)
    }

    function prevPage(pageNumber) {
        vm.loading = true;
        var offset = (pageNumber - 1) * vm.recordsPerPage;
        getContentForRelease(offset, vm.recordsPerPage)
    }

    function changePage(pageNumber) {
        vm.loading = true;
        var offset = (pageNumber - 1) * vm.recordsPerPage;
        getContentForRelease(offset, vm.recordsPerPage)
    }

    function goToPage(pageNumber) {
        vm.loading = true;
        var offset = (pageNumber - 1) * vm.recordsPerPage;
        getContentForRelease(offset, vm.recordsPerPage)
    }

    function getContentForRelease(offset, limit) {
        vm.loading = true;
        $http({
            method: 'GET',
            url: '/Umbraco/backoffice/api/Dashboard/GetContentForReleaseNextWeek?offset=' + offset + '&limit=' + limit,
            headers: {
                'Content-Type': 'application/json'
            }
        }).then(function (response) {
            console.log(response); // logging the response so we know what to do next!

            if (response.data.ScheduledNodes.length > 0) {
                vm.weHaveRecords = true;
                vm.records = response.data.ScheduledNodes;
            } else {
                vm.weHaveRecords = false;
            }    

            var totalPages = Math.ceil(response.data.TotalRecords / limit);

            vm.pagination = {
                pageNumber: (offset / vm.recordsPerPage) + 1,
                totalPages: totalPages
            };

            vm.loading = false;
        });
    }

    init();
}
Enter fullscreen mode Exit fullscreen mode

The pagination component will be hidden when the records can fit on 1 page, and be shown otherwise. I've set it to only fit 2 on each page just so it shows:

image

Outro

Thanks for reading so far! Part 2 will end here with a fully functioning listview listing out pending scheduled publishing nodes including paging.

In the next part we will look at adding functionality to open each node in an "infinite editor".

If you like this post, have any feedback, suggestions, improvements to my hacky code or anything else please let me know on Twitter - @jespermayn

💖 💪 🙅 🚩
jemayn
Jesper Mayntzhusen

Posted on April 14, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related