{"id":97,"date":"2025-02-03T15:53:08","date_gmt":"2025-02-03T15:53:08","guid":{"rendered":"https:\/\/kerner.digital\/?p=97"},"modified":"2025-11-21T22:23:59","modified_gmt":"2025-11-21T22:23:59","slug":"timeline-connector-part-2-implementing-record-filtering","status":"publish","type":"post","link":"https:\/\/kerner.digital\/?p=97","title":{"rendered":"Timeline Connector Part 2: Implementing Record Filtering"},"content":{"rendered":"\n<p>In <a href=\"https:\/\/kerner.digital\/?p=77\" data-type=\"link\" data-id=\"https:\/\/kerner.digital\/?p=77\">Part 1<\/a> we built the basic shape of a custom timeline connector: defining the interfaces, wiring up the core methods, loading data, and making the control happy enough to render your records.<\/p>\n\n\n\n<p>Now it\u2019s time for something users will actually notice:&nbsp;<strong>filtering<\/strong>.<\/p>\n\n\n\n<p>Filtering is the magic that lets them stop scrolling like maniacs and see only the subset of records they care about.<\/p>\n\n\n\n<p>This part walks through the concepts, the UI, and the backend logic you\u2019ll need to support filtering in a clean, reliable way.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><br><strong>What filtering means in a custom timeline connector<\/strong><\/h2>\n\n\n\n<p>Filtering in the timeline connector isn\u2019t fundamentally different from filtering list views or subgrids. The twist is that the timeline expects the filtering logic to live&nbsp;<strong>inside your connector<\/strong>, not the platform.<\/p>\n\n\n\n<p>Your connector decides:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>what filters exist<\/li>\n\n\n\n<li>how they\u2019re displayed<\/li>\n\n\n\n<li>how they get translated into queries<\/li>\n\n\n\n<li>how results are refreshed<\/li>\n<\/ul>\n\n\n\n<p>Typical use cases:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>show only records of a specific type<\/li>\n\n\n\n<li>filter by status<\/li>\n\n\n\n<li>filter by date range<\/li>\n\n\n\n<li>narrow the timeline to only assigned-to-me records<\/li>\n<\/ul>\n\n\n\n<p>The timeline doesn\u2019t enforce a specific model. That freedom is great right up until you have to design it,&nbsp;<strong>because the platform itself has no idea how your data source works<\/strong>. It doesn\u2019t know:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>what backend you\u2019re querying<\/li>\n\n\n\n<li>how your records relate to each other<\/li>\n\n\n\n<li>what your entity schema looks like<\/li>\n\n\n\n<li>whether your data even supports server-side filtering or sorting<\/li>\n\n\n\n<li>or if the source is something exotic like an external API, a custom index, or a flat stream of events<\/li>\n<\/ul>\n\n\n\n<p>So the platform pushes that responsibility onto the connector: you expose filters, you translate them, you execute the query.<\/p>\n\n\n\n<p>This avoids assumptions, but it also means&nbsp;<strong>the entire UX, query model, and edge-case handling lands on the connector developer<\/strong>.<\/p>\n\n\n\n<p>There&nbsp;<em>is<\/em>&nbsp;an argument to be made that the platform could improve this. For example:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>If connectors could optionally implement a\u00a0<strong>standardised interface describing their schema and sortable\/filterable fields<\/strong>, the timeline could handle some logic itself.<\/li>\n\n\n\n<li>The platform could do client-side sorting when appropriate.<\/li>\n\n\n\n<li>The control could validate filters before they hit your backend.<\/li>\n\n\n\n<li>Or at minimum, the connector could declare which operations (sorting, grouping, filtering) are supported by the backend and let the timeline enforce those rules.<\/li>\n<\/ul>\n\n\n\n<p>But for now, the connector is fully responsible. You define the filtering surface, and the platform stays agnostic.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><br><strong>Extending the connector interfaces<\/strong><\/h2>\n\n\n\n<p>In Part 1 we introduced the basic interfaces. Now we expand them with fields responsible for filtering.<\/p>\n\n\n\n<p>At minimum, you will need to enrich:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>getFilterDetails\u00a0\u2013 tells the timeline what filters your connector supports<\/li>\n\n\n\n<li>getRecordsData\u00a0\u2013 actually respects those filters when loading items<\/li>\n\n\n\n<li>internal state objects \u2013 store current filter selections from the UI<\/li>\n<\/ul>\n\n\n\n<p>I like to keep things clean so I&#8217;ll implement a separate class for doing the filtering:<\/p>\n\n\n\n<script src=\"https:\/\/gist.github.com\/piboke\/11f0f006541c3e1005dff87a1307844b.js\"><\/script>\n\n\n\n<p>After adding that to the project and removing unused code we can start filling in the blanks.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><br><strong>Designing the filtering UI<\/strong><\/h2>\n\n\n\n<p>Your connector controls its own UI. This means you decide how the user picks filters:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>dropdowns<\/li>\n\n\n\n<li>date pickers<\/li>\n\n\n\n<li>text fields<\/li>\n\n\n\n<li>toggles<\/li>\n<\/ul>\n\n\n\n<p>But the timeline expects your UI to expose:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>what filters exist<\/li>\n\n\n\n<li>when they change<\/li>\n\n\n\n<li>how to reset them<\/li>\n<\/ul>\n\n\n\n<p>All filtering UI should feed into one internal state object, something along the lines of:<\/p>\n\n\n\n<script src=\"https:\/\/gist.github.com\/piboke\/9cb7aa5167d3069ffb76d996a3c73ac0.js\"><\/script>\n\n\n\n<p>I decided to use single- and multi-select options in the getFilterDetails filtering status, modified date and quote amount. In each case you need to provide all possible filter options and then handle each one in your connector.<\/p>\n\n\n\n<p>After you build and publish you should see the filters in your app:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1012\" height=\"619\" src=\"https:\/\/kerner.digital\/wp-content\/uploads\/2025\/11\/image.png\" alt=\"\" class=\"wp-image-100\" srcset=\"https:\/\/kerner.digital\/wp-content\/uploads\/2025\/11\/image.png 1012w, https:\/\/kerner.digital\/wp-content\/uploads\/2025\/11\/image-300x183.png 300w, https:\/\/kerner.digital\/wp-content\/uploads\/2025\/11\/image-768x470.png 768w, https:\/\/kerner.digital\/wp-content\/uploads\/2025\/11\/image-570x350.png 570w\" sizes=\"auto, (max-width: 1012px) 100vw, 1012px\" \/><\/figure>\n\n\n\n<p>The only problem is, those will work only for your connector, so good luck explaining that to your users \ud83d\ude09<\/p>\n\n\n\n<p><strong><mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-vivid-red-color\">Also, there is a bug in Power Platform code that fails to render the timeline items if you try to filter the records and you have both regular activities and custom records. <\/mark><\/strong> It&#8217;s still a thing in November 2025 \u00af\\_(\u30c4)_\/\u00af <\/p>\n\n\n\n<p>But leaving that for a moment, we can also run a text filter by tapping into the search timeline textbox. It&#8217;s in the filter.searchKey (IFilterRequest interface).  You can see it in action here:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1023\" height=\"375\" src=\"https:\/\/kerner.digital\/wp-content\/uploads\/2025\/11\/image-3.png\" alt=\"\" class=\"wp-image-103\" srcset=\"https:\/\/kerner.digital\/wp-content\/uploads\/2025\/11\/image-3.png 1023w, https:\/\/kerner.digital\/wp-content\/uploads\/2025\/11\/image-3-300x110.png 300w, https:\/\/kerner.digital\/wp-content\/uploads\/2025\/11\/image-3-768x282.png 768w\" sizes=\"auto, (max-width: 1023px) 100vw, 1023px\" \/><\/figure>\n\n\n\n<p>The code behind is in a applySearchFilter method which is called on fetched records. We can also implement this as a filter criteria in a similar way to the above filters and include in the fetchXml, but I thought a little variety might be in order:<\/p>\n\n\n\n<script src=\"https:\/\/gist.github.com\/piboke\/2227a4a68a343c01ebdb17b5970d4bf5.js\"><\/script>\n\n\n\n<p>Whenever the user changes something, you simply:<\/p>\n\n\n\n<ol start=\"1\" class=\"wp-block-list\">\n<li>update the filter state<\/li>\n\n\n\n<li>call a refresh<\/li>\n\n\n\n<li>timeline requests data using updated filter<\/li>\n\n\n\n<li>you return a filtered dataset<\/li>\n<\/ol>\n\n\n\n<p>Nothing magical; just plumbing.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><br><strong>Translating filter UI into FetchXML or OData<\/strong><\/h2>\n\n\n\n<p>This is where the real work happens.<\/p>\n\n\n\n<p>Your connector receives filter parameters. Now you must convert them into a real FetchXML or OData query.<\/p>\n\n\n\n<p>Keep it simple:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>If status filter exists, add a\u00a0&lt;condition>.<\/li>\n\n\n\n<li>If date filter exists, apply\u00a0ge\u00a0or\u00a0le\u00a0operators.<\/li>\n\n\n\n<li>If type filter exists, change the entity queried or add another condition.<\/li>\n<\/ul>\n\n\n\n<p>Example skeleton (intentionally incomplete):<\/p>\n\n\n\n<script src=\"https:\/\/gist.github.com\/piboke\/6cd1846af6e8d0780b1c8f3fc13e92c9.js\"><\/script>\n\n\n\n<h2 class=\"wp-block-heading\"><br><strong>Sorting &amp; pagination interaction<\/strong><\/h2>\n\n\n\n<p>Filtering resets the timeline\u2019s paging context.<\/p>\n\n\n\n<p>If the user applies a filter, your connector must:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>reset continuation tokens<\/li>\n\n\n\n<li>run the query from page 1<\/li>\n\n\n\n<li>regenerate sorting logic (because filtered data may require a new order)<\/li>\n<\/ul>\n\n\n\n<p>The timeline control expects consistency. If you sort by&nbsp;createdon&nbsp;descending once, all further pages must follow the same logic or the control starts misbehaving.<\/p>\n\n\n\n<p>Let&#8217;s sort this out (pun intended) by expanding getFilteredQuotes by this little snippet:<\/p>\n\n\n\n<script src=\"https:\/\/gist.github.com\/piboke\/c709b02046a0f007f66d834ffe4ae703.js\"><\/script>\n\n\n\n<p>Also, remember to handle any exceptions \ud83d\ude42<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Performance considerations<\/strong><\/h2>\n\n\n\n<p>Filtering can make queries cheaper or stupidly expensive. Your call.<\/p>\n\n\n\n<p>Guidelines:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>only fetch attributes you display<\/li>\n\n\n\n<li>avoid huge OR filters<\/li>\n\n\n\n<li>limit date windows if possible<\/li>\n\n\n\n<li>do not fetch 1000 records and filter client-side<\/li>\n\n\n\n<li>sanitize inputs before building XML<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\"><br><strong>Summary and what comes next<\/strong><\/h2>\n\n\n\n<p>By the end of this part, your connector:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>exposes available filters<\/li>\n\n\n\n<li>stores current filter settings<\/li>\n\n\n\n<li>generates queries dynamically<\/li>\n\n\n\n<li>returns only relevant records<\/li>\n\n\n\n<li>plays nicely with the timeline control\u2019s paging and sorting<\/li>\n<\/ul>\n\n\n\n<p>Part 3 will focus on rendering the perfect card. Stay tuned and happy coding!<\/p>\n","protected":false},"excerpt":{"rendered":"<p>In Part 1 we built the basic shape of a custom timeline connector: defining the interfaces, wiring up the core methods, loading data, and making the control happy enough to render your records. Now it\u2019s time for something users will actually notice:&nbsp;filtering. Filtering is the magic that lets them stop scrolling like maniacs and see [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[7,6],"tags":[],"class_list":{"0":"post-97","1":"post","2":"type-post","3":"status-publish","4":"format-standard","6":"category-javascript","7":"category-power-platform","8":"czr-hentry"},"_links":{"self":[{"href":"https:\/\/kerner.digital\/index.php?rest_route=\/wp\/v2\/posts\/97","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/kerner.digital\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/kerner.digital\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/kerner.digital\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/kerner.digital\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=97"}],"version-history":[{"count":4,"href":"https:\/\/kerner.digital\/index.php?rest_route=\/wp\/v2\/posts\/97\/revisions"}],"predecessor-version":[{"id":105,"href":"https:\/\/kerner.digital\/index.php?rest_route=\/wp\/v2\/posts\/97\/revisions\/105"}],"wp:attachment":[{"href":"https:\/\/kerner.digital\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=97"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/kerner.digital\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=97"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/kerner.digital\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=97"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}