Zoeken in Umbraco-lijstweergaven aanpassen

Als je te maken hebt met grote archieven met inhoud, zoals een blogsectie, productcatalogus of iets dergelijks, is het gebruik van lijstweergaven een geweldige manier om de inhoudsstructuur in Umbraco op te ruimen.

Simpel gezegd, door de lijstweergave van het archiefdocumenttype in te schakelen, worden alle onderliggende items van dat documenttype verborgen voor de boomstructuur en in plaats daarvan weergegeven in een "inhoudsapp", in de vorm van een sorteerbare lijst.

Dit maakt het een stuk makkelijker om met grote archieven te werken, zoals de eerder genoemde blogsectie, productcatalogus etc.

Maar ze helpen niet als je je speld in de hooiberg van productnodes moet vinden. Standaard wordt de Umbracos-lijstweergave geleverd met een zoekfunctie, maar deze is helaas beperkt tot alleen zoeken in de knooppuntnamen.

Voor zoiets als een productcatalogus met duizenden producten zou het fijn zijn om productnodes te kunnen zoeken op basis van hun SKU's. Maar hiervoor moet u de SKU in de naam van het knooppunt plaatsen. Ik zou meestal de productnaam als de naam van het knooppunt plaatsen en de SKU op zichzelf op een eigenschap zetten.

Hieronder ziet u een voorbeeld van de productcatalogus in de standaard starterkit van Umbraco, waar ik heb gezocht naar een product-SKU. Er is niets gevonden.

Gelukkig kun je de standaardzoekopdracht vrij eenvoudig vervangen door die van jezelf.

Met behulp van de magie van $http-interceptors in AngularJS luister je eenvoudig naar verzoeken naar het standaard API-eindpunt voor het doorzoeken van onderliggende knooppunten en wissel je het uit met je eigen eindpunt.

Bouw uw eigen zoeklogica door de standaard over te nemen

Hiervoor heb ik een controller gemaakt, geërfd van Umbracos eigen ContentController .

using System.Linq;
using Examine;
using Umbraco.Core;
using Umbraco.Core.Cache;
using Umbraco.Core.Configuration;
using Umbraco.Core.Logging;
using Umbraco.Core.Models;
using Umbraco.Core.Persistence;
using Umbraco.Core.Persistence.DatabaseModelDefinitions;
using Umbraco.Core.PropertyEditors;
using Umbraco.Core.Services;
using Umbraco.Web;
using Umbraco.Web.Editors;
using Umbraco.Web.Models.ContentEditing;

namespace skttl
{
    public class CustomListViewSearchController : ContentController
    {
        public CustomListViewSearchController(PropertyEditorCollection propertyEditors, IGlobalSettings globalSettings, IUmbracoContextAccessor umbracoContextAccessor, ISqlContext sqlContext, ServiceContext services, AppCaches appCaches, IProfilingLogger logger, IRuntimeState runtimeState, UmbracoHelper umbracoHelper)
            : base(propertyEditors, globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, umbracoHelper)
        {
        }

        public PagedResult<ContentItemBasic<ContentPropertyBasic>> GetChildrenCustom(int id, string includeProperties, int pageNumber = 0, int pageSize = 0, string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = "", string cultureName = "")
        {
            // get the parent node, and its doctype alias from the content cache
            var parentNode = Services.ContentService.GetById(id);
            var parentNodeDocTypeAlias = parentNode != null ? parentNode.ContentType.Alias : null;

            // if the parent node is not "products", redirect to the core GetChildren() method
            if (parentNode?.ContentType.Alias != "products")
            {
                return GetChildren(id, includeProperties, pageNumber, pageSize, orderBy, orderDirection, orderBySystemField, filter);
            }

            // if we can't get the InternalIndex, redirect to the core GetChildren() method, but log an error
            if (!ExamineManager.Instance.TryGetIndex("InternalIndex", out IIndex index))
            {
                Logger.Error<CustomListViewSearchController>("Couldn't get InternalIndex for searching products in list view");
                return GetChildren(id, includeProperties, pageNumber, pageSize, orderBy, orderDirection, orderBySystemField, filter);
            }

            // find children using Examine

            // create search criteria
            var searcher = index.GetSearcher();
            var searchCriteria = searcher.CreateQuery();
            var searchQuery = searchCriteria.Field("parentID", id);

            if (!filter.IsNullOrWhiteSpace())
            {
                searchQuery = searchQuery.And().GroupedOr(new [] { "nodeName", "sku" }, filter);
            }

            // do the search, but limit the results to the current page 👉 https://shazwazza.com/post/paging-with-examine/
            // pageNumber is not zero indexed in this, so just multiply pageSize by pageNumber
            var searchResults = searchQuery.Execute(pageSize * pageNumber);

            // get the results on the current page
            // pageNumber is not zero indexed in this, so subtract 1 from the pageNumber
            var totalChildren = searchResults.TotalItemCount;
            var pagedResultIds = searchResults.Skip((pageNumber > 0 ? pageNumber - 1 : 0) * pageSize).Select(x => x.Id).Select(x => int.Parse(x)).ToList();
            var children = Services.ContentService.GetByIds(pagedResultIds).ToList();

            if (totalChildren == 0)
            {
                return new PagedResult<ContentItemBasic<ContentPropertyBasic>>(0, 0, 0);
            }

            var pagedResult = new PagedResult<ContentItemBasic<ContentPropertyBasic>>(totalChildren, pageNumber, pageSize);
            pagedResult.Items = children.Select(content =>
                Mapper.Map<IContent, ContentItemBasic<ContentPropertyBasic>>(content))
                .ToList(); // evaluate now

            return pagedResult;

        }
    }
}

Door te erven van ContentController , ik kan de standaardfunctionaliteit gemakkelijk herstellen als ik niets aangepast heb.

Ik heb een replicatie toegevoegd van de standaard GetChildren methode van de ContentController , genaamd GetChildrenCustom . Het heeft dezelfde parameters nodig, waardoor ik de url gewoon kan verwisselen wanneer Umbraco de API aanroept. Maar daarover later meer.

// get the parent node, and its doctype alias from the content cache
var parentNode = Services.ContentService.GetById(id);
var parentNodeDocTypeAlias = parentNode != null ? parentNode.ContentType.Alias : null;

// if the parent node is not "products", redirect to the core GetChildren() method
if (parentNode?.ContentType.Alias != "products")
{
    return GetChildren(id, includeProperties, pageNumber, pageSize, orderBy, orderDirection, orderBySystemField, filter);
}

In eerste instantie krijg ik het bovenliggende knooppunt van de ContentService en controleert of het bovenliggende knooppunt de productcatalogus is. Zo niet, dan retourneer ik gewoon de GetChildren methode uit de ContentController , de standaardfunctionaliteit herstellen.

Als ik me in een context van een productcatalogusknooppunt bevind, kan ik mijn eigen logica gaan doen.

// if we can't get the InternalIndex, redirect to the core GetChildren() method, but log an error
if (!ExamineManager.Instance.TryGetIndex("InternalIndex", out IIndex index))
{
    Logger.Error<CustomListViewSearchController>("Couldn't get InternalIndex for searching products in list view");
    return GetChildren(id, includeProperties, pageNumber, pageSize, orderBy, orderDirection, orderBySystemField, filter);
}

Eerst controleer ik of ik de InternalIndex van Examine krijg - als dit niet lukt, ga ik weer terug naar de standaard GetChildren.

// find children using Examine

// create search criteria
var searcher = index.GetSearcher();
var searchCriteria = searcher.CreateQuery();
var searchQuery = searchCriteria.Field("parentID", id);

if (!filter.IsNullOrWhiteSpace())
{
    searchQuery = searchQuery.And().GroupedOr(new [] { "nodeName", "sku" }, filter);
}

Maar in de meeste gevallen is de InternalIndex werkt (ik moet nog een Umbraco-installatie zien zonder de InternalIndex ). Ik kan dan doorgaan met zoeken.

Ik gebruik hier Onderzoeken, omdat het sneller is dan het doorlopen van de ContentService , als het gaat om vastgoedwaarden. Zoek in dit voorbeeld naar knooppunten, waarbij de parentId veld komt overeen met mijn bovenliggende node-ID.

En als de filter parameter een waarde heeft (dit is het zoekveld in de interface), daar voeg ik een zoekopdracht voor toe, kijkend in zowel de nodeName , en de sku velden.

// do the search, but limit the results to the current page 👉 https://shazwazza.com/post/paging-with-examine/
// pageNumber is not zero indexed in this, so just multiply pageSize by pageNumber
var searchResults = searchQuery.Execute(pageSize * pageNumber);

// get the results on the current page
// pageNumber is not zero indexed in this, so subtract 1 from the pageNumber
var totalChildren = searchResults.TotalItemCount;
var pagedResultIds = searchResults.Skip((pageNumber > 0 ? pageNumber - 1 : 0) * pageSize).Select(x => x.Id).Select(x => int.Parse(x)).ToList();
var children = Services.ContentService.GetByIds(pagedResultIds).ToList();

if (totalChildren == 0)
{
    return new PagedResult<ContentItemBasic<ContentPropertyBasic>>(0, 0, 0);
}

var pagedResult = new PagedResult<ContentItemBasic<ContentPropertyBasic>>(totalChildren, pageNumber, pageSize);
pagedResult.Items = children.Select(content =>
    Mapper.Map<IContent, ContentItemBasic<ContentPropertyBasic>>(content))
    .ToList(); // evaluate now

return pagedResult;

Dan op zoek. Ik wil niet meer knooppunten retourneren dan geconfigureerd in de lijstweergave, dus implementeer ik paging bij de zoekopdracht, zoals geadviseerd door Shannon in zijn blogpost.

Eindelijk repliceer ik een deel van de code van de standaard GetChildren methode, met vergelijkbare resultaten, maar gebaseerd op mijn onderzoek naar onderzoeken.

De backoffice mijn zoeklogica laten gebruiken

Zoals ik eerder al zei, wordt AngularJS geleverd met een concept genaamd $http interceptors. Hierin kun je luisteren en reageren op verschillende dingen, wanneer AngularJS http-verzoeken afhandelt.

Om deze truc te laten werken, moet ik verzoeken voor /umbraco/backoffice/UmbracoApi/Content/GetChildren . wijzigen (het standaard eindpunt voor onderliggende knooppunten), en wijzig dit in mijn nieuw gemaakte, die zich bevindt op /umbraco/backoffice/api/CustomListViewSearch/GetChildrenCustom .

Dit kan eenvoudig worden gedaan door een js-bestand toe te voegen dat een interceptor als deze bevat.

angular.module('umbraco.services').config([
   '$httpProvider',
   function ($httpProvider) {

       $httpProvider.interceptors.push(function ($q) {
           return {
               'request': function (request) {

                   // Redirect any requests for the listview to our custom list view UI
                   if (request.url.indexOf("backoffice/UmbracoApi/Content/GetChildren?id=") > -1)
                       request.url = request.url.replace("backoffice/UmbracoApi/Content/GetChildren", "backoffice/api/CustomListViewSearch/GetChildrenCustom");

                   return request || $q.when(request);
               }
           };
       });

   }]);

Merk op hoe ik /umbraco heb weggelaten van de URL's waarnaar wordt gezocht. Sommige mensen willen de naam van de backoffice-map wijzigen van umbraco naar iets anders - beveiliging door onduidelijkheid en dergelijke. Door alleen naar het laatste deel van de url te kijken, kan ik beide ondersteunen.

Ten slotte moet ik ervoor zorgen dat Umbraco mijn interceptor vindt en opneemt. Dit wordt gedaan met een package.manifest bestand in mijn App_Plugins map.

{
  "javascript": [
    "/App_Plugins/CustomListViewSearch/CustomListViewSearch.js"
  ]
}

Start uw site opnieuw, ga naar uw productcatalogus en voila. U kunt nu zoeken naar productknooppunten door SKU's te typen.

Voor de volledigheid zou je ook een ISearchableTree kunnen implementeren. Dit maakt de wereldwijde backoffice-zoekfunctie mogelijk en stelt uw redacteuren in staat om eenvoudig op Ctrl+Space op hun toetsenbord te drukken en te beginnen met zoeken.

Ik hoop dat je dit artikel leuk vond, en als er iets is waar je meer over wilt weten, reageer dan gerust of tweet me :)

Foto door Peter Kleinau op Unsplash