Nintex Forms

W swoim ostatnim projekcie musiałem stworzyć dynamiczną listę osób zatwierdzających w procesie, wybranych na podstawie lokalizacji i kwoty oraz kilku innych parametrach, jednak to nie ma znaczenia teraz. Na początku pomyślałem naturalnie, by stworzyć listę SharePoint, która będzie przechowywać odpowiednie mapowania. Następnie pomyślałem o akcji w przepływie pracy, która po prostu odpyta tę listę i korzystając z filtrowania uzyska listę tylko tych rekordów, które faktycznie reprezentować będą zatwierdzających dla danego procesu, którym następnie proces przypisze zadania. 

Ale był haczyk 🙂 Klient oczekiwał również, że formularz będzie pokazywać listę tych dynamicznie zebranych zatwierdzających i w miarę postępów w procesie, będzie zaznaczać jak każdy z nich zatwierdzał. I również z możliwością ręcznego dodania lub usunięcia osoby z tak wygenerowanej listy!

Pomyślałem więc, by użyć kontrolkę o nazwie Repeating Section, ale nie miałem pojęcia, w jaki sposób zasilić ją danymi dynamicznie. W końcu jednak to osiągnąłem i efekt wygląda jak poniżej:

Nintex Forms dynamic repeating section

Zaś lista źródłowa, z której pochodzą dane, wygląda następująco:

Jak zrobić taką dynamiczną repeating section?

Krok po kroku. Zaczynamy!

Struktura danych

Opisywana w tym poście aplikacja używa poniższej struktury danych (listy SharePoint):

  1. Locations (prosta lista, tylko z polem Tytuł)
  2. ApprovalThresholds – a lista zbudowana z poniższych pól:
    1. Title
    2. Approver (pole typu osoba, możliwy wielokrotny wybór)
    3. Location (lookup do listy Locations)
    4. Threshold (numer)
    5. OrderNo (numer, w moim wypadku chodzi o to, by umieć zidentyfikować do jakiej grupy zatwierdzających należy dany użytkownik)
  3. WorkingList (główna lista, na której tworzony będzie formularz)
    1. Title
    2. Location (lookup do listy Locations)
    3. Volume (numer)
    4. Approvers (pole tekstowe, wielo-linijkowe. Ważne! Musi to być typ  „plain text”!)

Formularz

Stworzyłem prosty formularz dla listy WorkingList. Z pewnymi zmianami oczywiście:

  1. Volume – pole otrzymało zmienną JavaScript o nazwie: var_Volume;
  2. Location pole otrzymało zmienną JavaScript o nazwie: var_Location;
  3. Potem usunąłem domyślne pole „Approvers” i zastąpiłem je kontrolką repeating section:

NIntex Form repeating section layout

Polu dodałem klasę CSS „approvers”:

To jest tutaj kluczowe. Nazwa klasy jest jedynym sposobem dla skryptu, by odnaleźć tę kontrolkę w kodzie formularza i następnie dostać się do jej zawartości.

Repeating section Nintex Form settings

Kontrolka jest zbudowana z takich pól:

  1. Approver name – pole otrzymało klasę CSS: approversApprover
  2. Approver email – pole otrzymało klasę CSS: approversApproverEmail 
  3. Approval group – pole otrzymało klasę CSS: approversOrderNo 

Dodałem także dwa pola typu checkbox, jedno posiada zmienną JavaScript o nazwie var_IsEditMode, drugie var_IsNewMode. Są używane, by przekazać do skryptu informację, w jakim trybie otwarty jest formularz. Oba mają ustawiony parametr „Default value” na „Expression”:

Checkboxes settings

I w zasadzie to wszystko, jeśli chodzi o formularz. Całą magię robi kod jQuery.

Skrypt

Skrypt odpowiada za wykonanie poniższych rzeczy:

  1. Wiąże listenery dla zdarzeń „change” i „blur” na polach Volume i Location;
  2. Definiuje funkcję, która jest uruchamiana w przypadku wystąpienia zdarzeń;
  3. Dodaje obsługę repeating section w trybie edycji formularza;
  4. Pozwala także na ukrywanie (lub pozostawianie nietkniętych) kontrolek dla obsługi repeating section – usuwanie wierszy, dodawanie wierszy (zmienna hideNativeRepeatingSectionControlls)

AD. 1 – wiązanie listenerów

var clientContext = new SP.ClientContext();
var siteurl = _spPageContextInfo.webAbsoluteUrl;
var hideNativeRepeatingSectionControlls = 0;


NWF$(document).ready(function () {

    //hide "add row" link in repeating section
    if (hideNativeRepeatingSectionControlls) NWF$(".approvers").find('.nf-repeater-addrow').css("visibility", "hidden");

    //trigger if location, CapitalExp or Volume is changed - recalculate list of Approvers
    NWF$("#" + var_Location).change(function () { retrieveApprovers(); });
    NWF$("#" + var_Volume).blur(function () { retrieveApprovers(); });

    if (NWF$("#" + var_IsEditMode).prop("checked")) redrawRepeatingTableEditMode();
});

AD. 2- funkcja obsługująca zmiany na polach

function retrieveApprovers() {

    var oList = clientContext.get_web().get_lists().getByTitle('ApprovalThresholds');

    var camlQuery = new SP.CamlQuery();
    var locationArr = NWF$("#" + var_Location).val().split(";#");
    var location = locationArr[1];
    var locationCaml = '<Eq><FieldRef Name="Location" /><Value Type="LookupMulti">' + location + '</Value></Eq>';
    var volume = NWF$("#" + var_Volume).val().replace(/[\,]/gi, "");

    if (!volume) volume= 0;

    camlQuery.set_viewXml('<View><Query><Where><And>' + locationCaml + 
        '<Leq><FieldRef Name="Threshold" /><Value Type="Number">' + volume+ '</Value></Leq>' +
        '</And></Where>' +
        '<OrderBy><FieldRef Name="Title"/><FieldRef Name="GroupOrderNo"/></OrderBy></Query></View>');
    
    this.collListItem = oList.getItems(camlQuery);

    clientContext.load(collListItem);

    clientContext.executeQueryAsync(
        Function.createDelegate(this, this.onQuerySucceeded),
        Function.createDelegate(this, this.onQueryFailed)
    );
}

Funkcja zbiera informacje z pól Location i Volume, a następnie konstruuje z nich zapytanie CAML, które następnie wysyła do SharePoint w celu uzyskania listy zatwierdzających. W przypadku sukcesu, wykonuje funkcję „onQuerySucceeded”, która iteruje po znalezionych wierszach.

Następnie, dla każdego zatwierdzającego, w bieżącym wierszu (może być ich więcej niż jeden) woła endpoint SharePoint w celu pozyskania dodatkowych informacji o nim (email, login):

function onQuerySucceeded(sender, args) {
    // Redraw the existing table, remove everything what exists, leave fresh instance
    redrawRepeatingTable();

    var listItemEnumerator = collListItem.getEnumerator();

    while (listItemEnumerator.moveNext()) {
        var oListItem = listItemEnumerator.get_current();

        var approvers = oListItem.get_item('Approvers');
        var approvalOrder = oListItem.get_item('GroupOrderNo');

        NWF$(approvers).each(function (idx, obj) {
            var person = JSON.stringify(obj);
            person = JSON.parse(person);
            // get user's display name and ID
            var approverId = person.$1T_1;
            var approverName = person.$4K_1;
            var userData = "";

            // ask for users additional data
            NWF$.ajax({
                url: siteurl + "/_api/web/getuserbyid(" + approverId + ")",
                method: "GET",
                async: false,
                headers: { "Accept": "application/json; odata=verbose" },
                error: function (data) {
                    console.log("Error: " + data);
                }

Po zebraniu komplety danych zapisuje je do pól w ostatnim, znalezionym wierszu Repeating Section (.nf-repeater-row:last) używając klasy ‚.approvers’ (pamiętaj, to bardzo ważne!) jako selektor.

 Jeśli zmienna „hideNativeRepeatingSectionControlls” funkcja usuwa także ikonkę „X” z przetwarzanego wiersza, dzięki czemu użytkownik nie będzie w stanie samodzielnie usunąć wygenerowanego wiersza.

Pamiętaj też, że nie możesz ustawić pól jako „disabled” lub ukryte korzystając z CSS lub reguł Nintex Forms. Z jakiegoś powodu elementy mające atrybut disabled lub styl „display:none” nie są brane pod uwagę podczas zapisu i wartości z nich nie trafiają do SharePoint. 

By tego uniknąć używaj „visibility:hidden” (zamiast „display:none”) i pól obliczeniowych lub overlay  by uczynić pole nieaktywnym.

 Po tym, gdy funkcja wypełni wszystkie pola, symuluje zdarzenie „click” na linku „add row” poniżej wiersza, w celu wygenerowania kolejnego, pustego wiersza, który w następnym kroku zostanie wypełniony danymi:
}).done(function (userData) {
            NWF$(".approvers .nf-repeater-row:last").find('.approversOrderNo input').val(approvalOrder);
            NWF$(".approvers .nf-repeater-row:last").find('.approversOrderNo input.nf-associated-control').attr("style", NWF$(".approvers .nf-repeater-row:last").find('.approversOrderNo input.nf-associated-control').attr("style") + "background-color: transparent !important;");
            
            NWF$(".approvers .nf-repeater-row:last").find('.approversApproverEmail input').val(userData.d.Email);
            NWF$(".approvers .nf-repeater-row:last").find('.approversApprover input').val(approverName);
            // remove image for row deletion
            if (hideNativeRepeatingSectionControlls) NWF$(".approvers .nf-repeater-row:last").find('.nf-repeater-deleterow-image').css("visibility", "hidden");

            // append overlay to avoid editting 😉 fields must be enabled to allow proper save
            if (hideNativeRepeatingSectionControlls) NWF$(".approvers .nf-repeater-row:last").append('<div class="approverOverlay"></div>');

            //add next row
            NWF$(".approvers").find('a').click();
        });
    });
}

Na koniec pętli usuwa ostatni, pusty wiersz, który zawsze pozostaje. Dodaje także, do każdego wygenerowanego wiersza, klasę „toRemoveOnReload”, dzięki czemu funkcja „redrawRepeatingTable()” będzie wiedzieć, które wiersze powinny zostać usunięte w przypadku konieczności przegenerowania zawartości kontrolki (np. z powodu zmiany wartości parametrów wejściowych).

    // remove last, empty row, as it is always empty
    NWF$(".approvers .nf-repeater-row:last").find('.nf-repeater-deleterow-image').click();

    // mark all additional rows as to be removed once control requires redraw:
    var addedRowsSuffixes = NWF$("input[name$='InternalRepeaterAddedRowSuffixes']").val().split(",");
    NWF$(addedRowsSuffixes).each(function (key, val) {
        NWF$("div[name='" + val + "undefined'").addClass("toRemoveOnReload");
    });

    return true;
}

Funkcja redrawRepeatingTable() wygląda nastepująco:

//function used to delete existing rows in repeating table leaving it as new
function redrawRepeatingTable() {
    //delete all existing repeating table rows, then build them again
    NWF$(".approvers .toRemoveOnReload").each(function () {
        NWF$(this).find('.nf-repeater-deleterow-image').click();
    });

    NWF$(".approvers .nf-repeater-row:last").find('.approversOrderNo input').val("").css("background-color", "rgb(248, 248, 248) !important");
    NWF$(".approvers .nf-repeater-row:last").find('.approversApprover input').val("");
    NWF$(".approvers .nf-repeater-row:last").find('.approversApproverEmail input').val("");
}

Ostatnią funkcją jaka jest używana w skrypcie, jest redrawRepeatingTableEditMode(). Istnieje w celu wstrzyknięcia sufiksów i klas „toRemoveOnReload” dla wierszy wygenerowanych w przypadku zbudowania zawartości repeating section w trybie edycji formularza:

//function used to enchance table of approvers created in the edit mode:
function redrawRepeatingTableEditMode() {
    var suffix = 1;
    var suffixes = "";
    NWF$(".approvers .nf-repeater-row").each(function () {
        NWF$(this).find('.nf-repeater-deleterow-image').css("visibility", "hidden");
        // inject prefix into div's ID'
        if (suffix > 1) {
            NWF$(this).attr("id", suffix + "_" + NWF$(this).attr("id"));
            NWF$(this).attr("name", suffix + "_undefined");
            suffixes += suffix + "_,";
        }

        //increment prefix value
        suffix += 1;
    });

    // inject suffixes into the dedicated field
    NWF$(".approvers").find('.nf-repeater-addeddrow-suffixes').val(suffixes.substr(0, suffixes.length - 1));
    var addedRowsSuffixes = suffixes.split(",");
    NWF$(addedRowsSuffixes).each(function (key, val) {
        NWF$("div[name='" + val + "undefined'").addClass("toRemoveOnReload");
    });
}
  Zauważ, że Repeating Section trzyma suffiksy każdego, wygenerowanego wiersza w ukrytym polu o naywie „InternalRepeaterAddedRowSuffixes”. Wartość pola zbudowana jest wg wzoru: #_;#_;#_ gdzie # to kolejna cyfra naturalna, licząc od 1. Zauważ rónież, że każdy wiersz posiada nazwę, która zaczyna się właśnie od tegoż sufiksu i słowa „undefined”, toteż możliwe jest również użycie tych informacji w celu iteracji po wygenerowanych wierszach.

Co ciekawe, w trybie edycji formularza ukryte pole jest puste, a sufiksów nie ma, dlatego skrypt je wstrzykuje.

Podsumowanie

Ten sposób tworzenia dynamicznych repeating section nie jest jedynie dedykowany dla zbierania informacji o zatwierdzających w procesie. Może byc użyty dla tworzenia dowolnych, dynamicznych zestawów danych. Na przykład dla pokazywania produktów dla określonej kategorii, czy części dla wybranych produktów. Ilość przypadków użycia zależy tylko od Ciebie 😉 

Poniżej załączyłem wyeksportowany formularz (Nintex Forms 2016) oraz JavaScript. Powodzenia!

[wpdm_package id=’713′]