SharePoint Online

Niedługi czas temu, w trakcie rozmowy z klientem pojawił się temat budowy pulpitu, który miał służyć do monitorowania wykonania zadań w przepływie pracy. Chodziło o to, by w zależności od faktu wykonania zadania lub przekroczenia deadline’u wiersze odpowiadające zadaniom były wyróżniane innym kolorem. W pierwszej chwili pomyślałem o użyciu formatowania kolumn w liście z włączonym Modern Experience. Jednak wówczas zostało dodane kolejne wymaganie, by nie tylko formatować wiersze odpowiadające zadaniom, lecz również wiersze je grupujące, z uwagi na dużą ilość poszczególnych zadań i potrzebę monitorowania wykonania całej grupy zadań, bez konieczności każdorazowego przeglądania każdego pojedynczo.

Znamienne jest również to, że rozwiązanie, które jako pierwsze przyszło mi do głowy nie jest możliwe do zrealizowania z użyciem standardowej listy zadań przepływu pracy (Workflow Tasks), ponieważ nie posiada ona jeszcze swojej wersji dla Modern UI. Wówczas pomyślałem o tym, by przechowywać kopie informacji o zadaniach na standardowej liście i używać przepływu pracy, by je tam aktualizował.

Jednak takie rozwiązanie w ogóle nie jest możliwe w sytuacji, gdy użyje się opcji „Wait for tasks completion” w ustawieniach akcji przydzielania zadania w Nintex Workflow. Nie wchodząc za bardzo w szczegóły – ta koncepcja również upadła z uwagi na długie opóźnienia między faktycznym zakończeniem zadania, a odnotowaniem tego faktu na opisywanej liście.

Rozwiązanie

Finalnie zdecydowałem o użyciu listy zadań przepływu pracy, jednak z zaimplementowanymi, dodatkowymi skryptami JavaScript.

Celem skryptu było przejrzenie wszystkich zadań, we wszystkich grupach zadań i w sytuacji, gdy co najmniej jedno zadanie było opóźnione, wyróżnienie nie tylko wiersza go prezentującego, ale także grupującego nagłówka.

Pomyślałem więc o skrypcie w JSLink. Poświęciłem trochę czasu na poszukiwanie podobnego rozwiązania w Internecie, jednak byłem w stanie znaleźć jedynie takie, w których autor używał skryptów do wyróżniania poszczególnych wierszy, nie zaś nagłówka. Zrozumiałem, że JSLink nie będzie w tym wypadku dobrym rozwiązaniem i zdecydowałem się na użycie zwykłego jQuery.

Wyzwania

Podczas pisania kodu zrozumiałem, że to wcale nie jest tak trywialne zadanie, jak sądziłem na początku, a to za sprawą następujących czynników:

  1. Domyślnie wszystkie grupy są zwinięte. To oznacza, że elementy nie są fizycznie obecne w kodzie DOM strony – nie są załadowane.
  2. W przypadku zmiany ustawień widoku i wyboru opcji, by domyślnie wszystkie grupy były rozwinięte, załadowanie wszystkich elementów zabiera niemało czasu.
  3. Co więcej, pracując z listą posiadającą widok pogrupowany, SharePoint zapamiętuje stan zwinięcia/ rozwinięcia poszczególnych grup i podczas przechodzenia po stronach/ odświeżania listy niektóre grupy pozostają zwinięte, a inne rozwinięte.
  4. Rozwinięcie grupy powoduje ładowanie elementów, jednak w sposób asynchroniczny. Nie udało mi się znaleźć żadnych informacji na temat zdarzeń wyzwalanych w momencie zakończenia ładowania elementów poszczególnej grupy lub na całej stronie.

Jak to zrobiłem?

By być w stanie wyróżniać nagłówki, musiałem przejrzeć wszystkie wiersze zadań dla każdej z nich. By to zrobić, zadania w każdej grupie musiały być załadowane do struktury DOM. Uznałem więc, że najlepszym rozwiązaniem będzie wykonanie „kliknięcia” w każdy element „a”, który zawiera w sobie element „span” z klasą „ms-commentexpand-iconouter”:

Collapsed SharePoint list DOM element

Co więcej, analizując structure kodu zauważyłem, że elementy „tbody” odpowiadające za grupowanie wierszy zawierają w sobie parametr „selectablerows”, który przechowuje informację na temat liczby wierszy zgrupowanych pod nim (i informacja ta jest dodawania po załadowaniu wszystkich wierszy danej grupy):

SharePoint grouped list view - selectable rows property

Zdecydowałem się więc napisać skrypt, który najpierw klika wszystkie elementy „a”, co powoduje rozwinięcie grup i załadowanie wierszy. Następnie, w pętli, sprawdza, czy liczba załadowanych wierszy (elementy „tr”) jest równa sumie wartości pobranych z obecnych na stronie parametrów „selectablerows”. Jeśli tak, funkcja zwraca obiekt „promise” i następnie skrypt przechodzi do wyróżniania wierszy zadań oraz grup zadań. Na koniec skrypt zamyka wszystkie grupy.

Kod

Najpierw, skrypt przetwarza nagłówek tabeli w celu pobrania nazw wewnętrznych każdej z kolumn, dostępnych w widoku. Następnie dzięki temu możliwe jest wykonywanie wewnątrz kodu referencji do konkretnej kolumny, nie jest indeksu w widoku, co sprawia, że kod jest bardziej elastyczny i odporny na zmiany, np. w przypadku modyfikacji widoku.

$('tr[class*="viewheadertr"] > th').each(
        function () {
            fieldsArr[$(this).children('div[class="ms-vh-div"]').attr("name")] = counter++;
        }
);

Następnie kod wykonuje funkcję, której zadaniem jest wykonanie opisanego powyżej scenariusza: rozwinięcie wszystkich grup, przetworzenie zadań i zwinięcie grup:

$.manageTasks = function () {
    var tasksHideShow = $.Deferred();

    function checkRowsLoaded() {

        var rows = 0;
        $("tbody[id^='tbod']").each(function () {
            rows += ($(this).attr("selectablerows") !== undefined ? parseInt($(this).attr("selectablerows")) : 0);
        });
        var actualRows = $("tbody[id^='tbod'] > tr").length;

        if (actualRows == rows && actualRows !== 0) {
            $.colorizeRows();
            $("span[class='ms-commentcollapse-iconouter']").parent("a").click();
            return tasksHideShow.resolve();
        }

        $("span[class='ms-commentexpand-iconouter']").parent("a").click();
        setTimeout(checkRowsLoaded, 1000);
    }
    setTimeout(checkRowsLoaded, 500);

    return tasksHideShow.promise();
}

Zauważ, że funkcja tworzy obiekt Deferred i rozwiązuje go, w chwili gdy liczba wygenerowanych wierszy jest równa sumie wartości z parametrów „selectablerows”. Woła również funkcję „colorizeRows()” w chwili, gdy wszystkie grupy są rozwinięte. Funkcja przechodzi przez wszystkie wiersze każdej z grup i odpowiednio je wyróżnia, zaś w sytuacji, gdy napotka choć jeden wiersz z opóźnionym zadaniem – wyróżnia również wiersz nagłówka:

$.colorizeRows = function () {
    var DueDateCollIdx = fieldsArr["DueDate"];
    var StatusCollIdx = fieldsArr["Status"];

    $('tbody[id^="tbod"]').each(function () {
        var isAnyTaskDelayed = false;
        $(this).children('tr[class*="itmhover"]').each(
            function () {
                var cellsInRow = [];
                cellsInRow = $(this).has("td").children("td");
                var dueDate = $(cellsInRow[DueDateCollIdx]).children("span").attr("title");
                var taskStatus = $(cellsInRow[StatusCollIdx]).text();
                
                // Task overdue
                if (new Date() > new Date(dueDate) && taskStatus !== "Completed") {
                    $(this).attr("style", "background-color:#e85050");
                    isAnyTaskDelayed = true;
                }

                // Task on track - in progress
                else if (new Date() <= new Date(dueDate) && taskStatus !== "Completed") {
                    $(this).attr("style", "background-color:#9dae11");
                }

                // Task completed
                else {
                    $(this).attr("style", "background-color:#00b0ff");
                }
            }
        );

        // If any task is delayed - highlight the grouping header row
        if (isAnyTaskDelayed) {
            var groupId = $(this).attr("id");
            var parentGroupId = (groupId.substring(0, groupId.length - 1)).replace("tbod", "titl");
            $("tbody[id='" + parentGroupId + "'] > tr").attr("style", "background-color:#e85050");
        }
    });
}

Na koniec zwraca “promise”, dzięki czemu główny skrypt wznawia swoje działanie i kontynuuje swoje akcje. Na koniec, po przetworzeniu wszystkich zadań i grup, animacja ładowania zostaje ukryta:

$.manageTasks().done(function () {
        $.hideSpinner();
});

Funkcja Init(), która odpowiada za wykonanie pozostałych funkcji, jest wyzwalana po zakończeniu ładowania skryptów „sp.js”:

SP.SOD.executeOrDelayUntilScriptLoaded(function () {
    $.init();
}, "sp.js");

I tyle!

Wynik

Poniżej możesz zobaczyć, jak działa rozwiązanie. Zauważ, że im więcej danych: więcej wierszy obecnych jest w grupach, tym czas ładowania ich i tym samym długość czasu wyświetlania animacji są dłuższe.

Loading SharePoint tasks listExpanding formatted SharePoint tasks list

Następne kroki

Na obecnym etapie rozwiązanie nie jest przygotowane do obsługi błędów, np. spowodowanych długim czasem ładowania zadań, brakiem połączenia internetowego, utratą sesji – w chwili obecnej skrypt będzie działać na zawsze.

Zauważyłem także, iż w przypadku ustawienia widoku, by dzielił grupy na strony, po wykonaniu się skryptu na jednej stronie i próbie przejścia na następną, SharePoint w ogóle nie wykonuje skryptu ładowania danych. Na chwilę obecną nie wiem, czym jest to spowodowane.

Ostatnią rzeczą, którą można poprawić, jest warstwa wizualna, by prezentowanie informacji nt. statusu zadań było atrakcyjniejsze wizualnie.

 

Proszę, zostaw swój komentarz i pytania poniżej!

Cześć! Nazywam się Tomasz. Jestem wielkim fanem automatyzacji procesów i analizy biznesowej. Skupiam się na rozwijaniu moich umiejętności w pracy z produktami Nintex i Microsoft: w szczególności Office 365, SharePoint, Flow, PowerApps. Posiadam ponad 8 lat doświadczenia w pracy z SharePoint.