<?xml version="1.0" encoding="utf-8"?>
<rss xmlns:media="http://search.yahoo.com/mrss/" version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Dancing with CRM</title>
        <link>https://www.dancingwithcrm.com/</link>
        <description>Oleksandr's Olashyn personal blog about Power Platform, Dynamics 365 and many more</description>
        <lastBuildDate>Fri, 09 Oct 2026 04:02:11 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>en</language>
        <copyright>All rights reserved 2026, Oleksandr Olashyn</copyright>
        <atom:link href="https://www.dancingwithcrm.com/feed.xml" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Supported isn't the same as working - Astro in Power Pages]]></title>
            <link>https://www.dancingwithcrm.com/astro-in-power-page/</link>
            <guid isPermaLink="false">https://www.dancingwithcrm.com/astro-in-power-page/</guid>
            <pubDate>Wed, 07 Oct 2026 02:45:20 GMT</pubDate>
            <description><![CDATA[The addition of Astro as a supported framework for Power Pages code sites sounded like a perfect match for content-driven projects. However, after putting it to the test, I quickly learned that "supported" is not the same as "working." In this post, I break down the fundamental architectural clash between Astro and Power Pages, the heavy workarounds required to deploy it, and why I currently can't recommend it. Check it out to find out more.]]></description>
            <content:encoded><![CDATA[<p>I like Astro. It is an awesome web framework that lets you embrace a zero-JS-by-default approach while still using your favourite JS frameworks for different interactive parts of the site. Heck, even my blog, where you're currently reading this post, is written in <a href=https://www.dancingwithcrm.com/switching-from-jekyll-to-astro/>Astro</a>. So you can imagine how excited I was when Microsoft mentioned that Astro will be supported for the new SPA sites (as part of the skills). And now that I've tried it, there is only one thing to say - <strong>"supported"</strong> is not the same as <strong>"working"</strong>.</p><h2>Why Astro?</h2><p>This section is focused on explaining the basics of Astro and what makes it great. Feel free to skip it; however, it gives useful context for future issues and frustrations.</p><p>Astro is a web framework for building content-driven websites like blogs (surprise, right?), marketing sites, and e-commerce sites. It is best-known for pioneering a new frontend architecture (<a href=https://docs.astro.build/en/concepts/islands/>the Island Architecture</a>) to reduce JavaScript overhead and complexity compared to other frameworks. If you need a website that loads fast, is content-driven and has great SEO, then Astro is for you.</p><p>Another great feature of Astro is that it is UI-agnostic - bring React, Vue, Angular, htmx or anything in between.</p><p>Being content-driven and server-first does have limitations, meaning it isn't the right choice for highly complex client-side apps. However, it is ideal for knowledge-based websites, FAQ and similar portals, which makes it ideal for those Power Pages scenarios.</p><h2>Creating a site</h2><p>I decided to give it a try using official Microsoft Skills for Power Pages with Claude Code. I asked Claude to create a simple FAQ site with an About page using Astro.</p><p><img alt="Claude initial prompt" src=https://www.dancingwithcrm.com/assets/img/2026-10-06-claude-code-create-site-command.jpg></p><p>The site was created quickly and ran well locally. (Btw, did you know that when you use official MS AI skills, you don’t actually create a site from scratch? Instead, the AI copies a provided sample from Microsoft and expands on it. This helps MS better control the shape of the initial site and makes validation easier.)</p><p><img alt="Site running locally" src=https://www.dancingwithcrm.com/assets/img/2026-10-06-site-running-locally.jpg></p><p>Next steps were deployment. And again, everything went well. The site was deployed and provisioned without any errors.</p><p>When I opened the site, the Home page was exactly as expected. However, as soon as I navigated to the About page, everything fell apart - the page was empty. URL was valid; there was no 404 Not Found error. The page was there - it just didn’t have any content.</p><p><strong>Working Home Page</strong></p><p><img alt="Working Home page" src=https://www.dancingwithcrm.com/assets/img/2026-10-06-working-home-page.jpg></p><p><strong>Empty About Page</strong></p><p><img alt="Empty About page" src=https://www.dancingwithcrm.com/assets/img/2026-10-06-empty-about-page.jpg></p><p>I checked the page locally - it worked. However, the deployed site told a different story. So what went wrong?</p><h2>Investigation</h2><p>The first thing to check was the page record itself in Dataverse. Since the URL was valid, I checked if the page was provisioned. And it was indeed there. However, the Localized Content page was empty as well.</p><p><img alt="Page record in Power Pages management" src=https://www.dancingwithcrm.com/assets/img/2026-10-06-page-record-power-pages-management.jpg></p><p>Next, I checked locally in the <strong>.powerpages-site</strong> folder - the generated folder where all the actual metadata lived. The webpage was generated there; however, the HTML was empty.</p><p><img alt="Empty local page" src=https://www.dancingwithcrm.com/assets/img/2026-10-06-empty-local-page.jpg></p><p>So PAC CLI correctly interpreted the page URL and created it, based on Astro folder structure (Astro uses file-based routing). However, instead of taking the actual HTML of the page, it left it empty.</p><p>After a deeper dive, I noticed something else - there were a few index.html as Web Files in the generated folder. That was odd - we shouldn’t have any. When I checked where those files belonged, I finally understood - each of them was under their respective pages: About and FAQ.</p><p><img alt="Web files in the generated folder" src=https://www.dancingwithcrm.com/assets/img/2026-10-06-web-files-generated-folder.jpg></p><p>This meant that PAC CLI found those pages in the dist folder, understood that it needed to create a new page based on the folder; however, instead of treating the HTML inside the folder as the source for the page, it created it as a child Web File.</p><p>Technically, this means we can open both our About and FAQ pages as web files by going to their URLs directly.</p><p><img alt="Page for web file" src=https://www.dancingwithcrm.com/assets/img/2026-10-06-page-for-web-file.jpg></p><h2>Workaround (not really)</h2><p>So, now that we know what PAC CLI did and why our pages are empty, can we do something about it?</p><p>Unfortunately, no.</p><p>We cannot force users to use web file URLs instead of actual pages. While it opens a file that looks like what we built, it runs outside the portal runtime - it won’t have any shared styles, header/footer wrappers or context objects (like signed-in user details) to make it actually functional. <strong>(see above)</strong></p><p>Updating the Astro config to use direct files in the output instead of directories (ie generating about.html instead of about/index.html) doesn’t help either. PAC CLI just ignores them.</p><p>The only <strong>“option”</strong> is to treat the Astro project as a pure SPA - ie writing a custom router definition and combining everything into one HTML file. That way PAC CLI will pick it up, and we can resolve routes with a bit of JS.</p><p>However, this isn't a real solution. If you do this, the Home page will grow insanely large very quickly as all page content is literally crammed into the initial load. It completely defeats the purpose of using Astro, and it creates a nightmare for accessibility standards.</p><h2>Deploying directly from sample</h2><p>Ok, <em>I thought to myself</em>, maybe AI messed up somehow. Let’s start from the source directly.</p><p>I copied the Microsoft sample directly (which contains, beyond the Home page, the About page as well), then manually ran the PAC CLI command to upload the code site.</p><p>And guess what? Results were exactly the same - the About page was generated as a separate page without any HTML inside, and the actual page was just added to the Web Files.</p><h2>Conclusion</h2><p>I just cannot recommend using Astro with Power Pages as it is right now.</p><p>Honestly, I don’t know if this is some issue with the recent PAC CLI updates or if Microsoft genuinely missed this one, but Astro just doesn’t work out of the box.</p><p>Power Pages Code Sites were always targeting pure SPAs first (React, Angular, Vue). Having only one route page and loading everything else as additional JS files works really well for that architecture.</p><p>Astro is different. It is a multi-page setup first. Ironically, this MPA approach would align perfectly with how traditional Power Pages actually work under the hood, making it a great solution for content-driven scenarios like FAQs and knowledge bases.</p><p>Unfortunately, until PAC CLI understands how to handle multi-page output, I cannot recommend Astro. I really hope Microsoft addresses this soon.</p><h2>Credits</h2><p>Cover image by <a href=https://pixabay.com/photos/light-bulb-incandescent-smoke-crack-503881/>PixaBay</a></p>]]></content:encoded>
            <author>oleksandr@dancingwithcrm.com (Oleksandr Olashyn)</author>
            <enclosure url="https://www.dancingwithcrm.com/assets/img/2026-10-06-cover-rss.jpg" length="0" type="image/jpg"/>
        <media:content url="https://www.dancingwithcrm.com/assets/img/2026-10-06-cover-rss.jpg" medium="image"/>
        </item>
        <item>
            <title><![CDATA[Automating Power Pages Administration with PAC CLI]]></title>
            <link>https://www.dancingwithcrm.com/automating-power-page-administration-with-pac-cli/</link>
            <guid isPermaLink="false">https://www.dancingwithcrm.com/automating-power-page-administration-with-pac-cli/</guid>
            <pubDate>Mon, 21 Sep 2026 02:45:20 GMT</pubDate>
            <description><![CDATA[The arrival of the new pac power-pages CLI command group marks a major step forward for Power Pages administration and DevOps. In this post, I review some of these commands that solve issues commonly faced by everyday administrators, such as managing SSL certificates, controlling site lifecycles, and handling break-glass governance exceptions, as well as how these new commands help to automate these tasks. Check it out to find out more.]]></description>
            <content:encoded><![CDATA[<p>Administering a Power Pages site was always a chore. Historically, it meant juggling multiple apps, interfaces, and websites to manage different aspects of a site, without a truly unified platform. A lot of this was addressed when Microsoft added Power Pages to the Power Platform Admin APIs. However, you still need to know the proper endpoints and get proper auth tokens.</p><p>Now, we finally have an easy way to automate our admin tasks - with the latest PAC CLI 2.12.X update, all those Admin APIs are exposed as dedicated commands.</p><p>My only pet peeve with this release is the command name <strong>pac power-pages</strong>, which is confusing since we already have pac pages to work with site content and ALM. Furthermore, power-pages on its own doesn’t articulate that these are admin commands. Since these commands are currently in preview, I really hope that MS will reconsider the naming convention before GA.</p><blockquote><p><strong>Important</strong></p><p>As of writing this article, pac power-pages commands are in Preview.</p></blockquote><p>With this release, Microsoft introduced 40 new commands under <strong>pac power-pages</strong>. For the whole list of commands, see <a href=https://learn.microsoft.com/en-us/power-platform/developer/cli/reference/power-pages>the official documentation</a>.</p><p>In this article, we will review the ones I find the most interesting and how they fit into real-world admin scenarios.</p><h2>Automated Certificate Management</h2><p>One of the most annoying tasks in Power Pages is managing custom SSL certificates. As standard security practice dictates, these certificates need to be rotated often, based on your company's security policy. The typical process was as follows: set up a calendar reminder for when the certificate is about to expire, obtain the new one and manually upload the certificate through the UI. I kid you not - I still have meeting invites in my calendar from some of my previous clients specifically to update their certificates.</p><p>Finally, all of this can become a thing of the past with the introduction of 2 new commands:</p><ul><li><strong>pac power-pages upload-certificate</strong> - allows you to upload a certificate for the specified website</li><li><strong>pac power-pages add-ssl-binding-by-portal</strong> - allows you to bind a certificate to the specified site</li></ul><p>Now, instead of doing everything manually, you can set up a fully automated workflow, for example, using Azure Key Vault automated renewals paired with a deployment pipeline. When a certificate is renewed, an automated pipeline can get the new certificate as a .pfx file, upload it, and bind it using PAC CLI.</p><p>And no more “renewal” meetings in your calendar or calls about an “unsecure” website.</p><h2>Site Lifecycle Controls</h2><p>How often, after deploying changes to your website, do you need to go to the admin center to manually hit that sweet “Restart Website” button to fully “nuke” server-side cache and allow your changes to take effect? And how many times have you wished to have a short command to do it or even make it part of the deployment?</p><p>Well, now your wish has been granted- the new <strong>pac power-pages restart-website</strong> command can do just that - hard restart your website.</p><p>In addition, two new commands, <strong>start-website</strong> and <strong>stop-website</strong>, can help you manage your site state much more easily. However, be careful - <strong>stop-website</strong> will actually stop your website completely, instead of putting it into maintenance mode. This means that your users will be greeted by generic 503 or error pages, so make sure you are using this command intentionally.</p><p>This is actually my only grievance with this command group, as having a simple command to put the website into maintenance mode would be an ideal addition for the proper ALM pipeline.</p><h2>Exception Governance</h2><p>Power Pages as a platform constantly evolves, and Microsoft is constantly introducing new changes. This also means that some functionality is being deprecated (either by being legacy/outdated or because of security implications). The most recent example of this is Web API wildcard deprecation. This setting led to the recent data leaks and hacker attacks. Starting last week (September 14, 2026), your website can no longer use wildcards as a Web API option, making it much more secure.</p><p>However, not all of the deprecations are equal. Sometimes, your company needs more time to adjust its website. Or legacy code is just too complex and process-dependent, and the deprecation timeline just isn’t feasible. What should you do in that case?</p><p>Well, that's where the exception functionality comes into play.</p><p>Exception functionality allows you to grant a break-glass admin exception for a specific exception type for a specific website. Admins need to explicitly acknowledge the risk of granting the exception.</p><p>Microsoft provides the following commands to manage exceptions:</p><ul><li><strong>pac power-pages get-affected-sites</strong> - get the list of sites that are affected by a specific exception type</li><li><strong>pac power-pages get-exceptions</strong> - get the list of exceptions applied at the tenant level</li><li><strong>pac power-pages grant-exception</strong> - grants an exception for a specific exception type to a specific site</li><li><strong>pac power-pages revoke-exception</strong> - revokes a previously granted exception</li></ul><p>Currently, the only exception supported is “WebApiStarRetirement”, which is directly related to the recent wildcard deprecation. However, I would definitely not recommend granting an exception for this one if you can avoid it - it is indeed too permissive, and you should definitely limit its scope.</p><p>In the future, when new deprecations arrive, using the commands above will allow you to better prepare and remediate deprecations without full-blown production downtime.</p><h2>Conclusion</h2><p>The introduction of the pac power-pages commands is a massive step forward for Power Pages administration. No longer do we need to rely on manual steps or complex REST API calls. Having a unified set of commands that are easily scriptable opens the door for a much more maintainable and structured DevOps and management story. I still hope that MS will adjust the name to something more appropriate before GA to avoid confusing users, but having these capabilities natively in the CLI is a major milestone for Power Pages governance.</p><h2>Credits</h2><p>Image by <a href="https://pixabay.com/users/dimitri_k-30935610/?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=7925618">Dimitrios K</a> from <a href="https://pixabay.com//?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=7925618">Pixabay</a></p>]]></content:encoded>
            <author>oleksandr@dancingwithcrm.com (Oleksandr Olashyn)</author>
            <enclosure url="https://www.dancingwithcrm.com/assets/img/2026-09-20-cover-rss.jpg" length="0" type="image/jpg"/>
        <media:content url="https://www.dancingwithcrm.com/assets/img/2026-09-20-cover-rss.jpg" medium="image"/>
        </item>
        <item>
            <title><![CDATA[Power Pages Backend Trifecta]]></title>
            <link>https://www.dancingwithcrm.com/power-pages-backend-trifecta/</link>
            <guid isPermaLink="false">https://www.dancingwithcrm.com/power-pages-backend-trifecta/</guid>
            <pubDate>Mon, 27 Apr 2026 03:45:20 GMT</pubDate>
            <description><![CDATA[The arrival of Server Logic marks a major shift in how we handle logic in Power Pages. In this post, I explore what I call the "Power Pages Backend Trifecta" (Companion Apps, Cloud Flows, and Server Logic) and how these three distinct approaches shape the way we build the sites in 2026. Check it out to find out more.]]></description>
            <content:encoded><![CDATA[<p>For years, if you needed to do anything beyond basic CRUD in Power Pages, like calling a private API or running a secure calculation, you had to build a Companion App (an external service, usually an Azure Function or something similar, that would serve as a custom backend).</p><p>A couple of years ago, a new option emerged - Cloud Flows with a native Power Pages trigger. While a massive step forward, it wasn't always the right fit for high-performance, synchronous tasks.</p><p>Now, with the full GA of Server Logic and the recent addition of agentic skills to automate its creation, the question arises: where should your Power Pages backend live?</p><p>Let's start by understanding what each option brings to the table.</p><h2>Companion App</h2><p><img alt="Companion App Diagram" src=https://www.dancingwithcrm.com/assets/img/2026-04-26-companion-app-diagram.jpg></p><p>The Companion App approach emerged almost immediately with the introduction of Dynamics Portals (yep, it's that old). It refers to any external service built to act as a dedicated backend for your site. Usually built with Azure Functions or Azure Web Apps to simplify the Dataverse connection, this approach was the only way to handle complex scenarios for a decade.</p><p>However, it suffers from two primary issues:</p><ul><li>Security: You had to build custom middleware to handle requests. While the OAuth 2.0 implicit grant flow became a de facto default, it required significant "handshake" code on both the client and the server (even with Microsoft providing code samples to simplify it).</li><li>Maintainability: Your code lives in a completely separate ecosystem. You need separate Git repositories and deployment pipelines, which can lead to a "split-brain" architecture where the frontend and backend can easily fall out of sync.</li></ul><h2>Cloud Flows</h2><p><img alt="Sample Cloud Flow" src=https://www.dancingwithcrm.com/assets/img/2026-04-26-cloud-flow.jpg></p><p>The introduction of native Power Pages support in Power Automate Cloud Flows, with dedicated trigger and response actions, was a huge win several years ago. It immediately made the Power Pages sites much more powerful by allowing the usage of 1000 built-in connectors, without the need to manage complex authentication or exposing any secrets. This makes it ideal for sending messages in Teams or emails via Outlook, or performing any action with the existing connector for external systems.</p><p>A cherry on top was native support for the Web Roles, which ensured that users had the correct role before calling the flow. However, Cloud Flow doesn't run in the user's context, meaning it doesn't apply Table Permissions in any capacity. It also doesn't know which Contact triggered the execution, unless you pass the id parameter explicitly and then perform additional validations inside the flow.</p><p>Unfortunately, despite all the benefits, Cloud Flow were not ideal for synchronous, high-performance operations.</p><h2>Server Logic</h2><p><img alt="Server Logic Flow" src=https://www.dancingwithcrm.com/assets/img/2026-04-26-server-logic.jpg></p><p>Server Logic is the latest addition to the Power Pages and, in my opinion, one of the most important. Although long overdue, it finally gives developers a “native” backend to work with.</p><p>It allows you to write JavaScript (ECMAScript 2023 compliant) that runs directly on the Power Pages server. However, it is important to understand the boundaries: this is a sandboxed server environment. You cannot use browser-specific APIs (like window or document), and standard client-side networking tools like fetch or XMLHttpRequest are replaced by the native Connector.HttpClient object. Additionally, for security reasons, you cannot currently import external npm packages or libraries.</p><p>Unlike Cloud Flows, which are asynchronous and better suited for "workflow" tasks, Server Logic is designed for synchronous, high-performance operations. It is ideal for:</p><ul><li>Calling a service like Stripe or OpenAI without exposing your API keys to the browser.</li><li>Running multi-table logic before allowing a Dataverse write.</li><li>Performing secure server-side filtering or returning aggregated data structures.</li></ul><h3>Security Context</h3><p>By design, Server Logic runs in the context of the current user, with all table permissions applied. In 90% of scenarios, this is exactly what we need - it ensures that your backend logic respects the existing security hierarchy. You don't have to manually re-validate if a user "should" see a record; the platform does it for you.</p><p>However, this "User Context" can be a double-edged sword. If you need to perform a "System Context" operation, for example, validating a promo code against a "Marketing Campaigns" table that is hidden from regular users, Server Logic will return nothing because it respects the user's lack of permissions.</p><p>Fingers crossed that Microsoft will update this in the future and allow us to perform System-level queries natively, empowering an already capable option.</p><h2>Agentic development workflow</h2><p>The recent release of the Power Pages Agentic Skills for Claude Code and GitHub Copilot has turned the complex task of backend configuration and creation into a more streamlined and straightforward process.</p><blockquote><p><strong>Important</strong></p><p><strong>Compatibility Note:</strong> These agentic skills are specifically designed for the newest SPA-based website creation and management workflows. They will not function properly with traditional Power Pages sites.</p></blockquote><p>Here is how these skills reshape the build process:</p><ul><li><strong>/add-server-logic</strong>: generates a Server Logic endpoint, including not just the code, but also the endpoint configuration, security role assignment, etc.</li><li><strong>/add-cloud-flow</strong>: acts as the bridge to your automated workflows. It identifies an existing Power Pages-enabled Cloud Flow and generates the necessary code to invoke it and process its response. <ul><li><strong>IMPORTANT</strong> This skill does not create a new Cloud Flow. You still need to design your business logic within the Power Automate designer first; the agent just handles connecting it to your SPA.</li></ul></li><li><strong>/integrate-backend</strong>: this is the most interesting one. Acting as a Power Pages Architect, the agent analyzes your project's requirements and determines the optimal approach for each feature - whether that's a simple Web API call, a synchronous Server Logic endpoint, or an asynchronous Cloud Flow. It then orchestrates the entire build sequence across your project files.</li></ul><h2>How to choose</h2><p>My advice is to follow a "Native-First" approach: only use external services when the platform's sandbox limits your requirements.</p><p><strong>Start with Server Logic for almost everything.</strong> If your task involves securing an API key, performing a synchronous calculation, or validating data before it hits Dataverse, Server Logic is your best friend. It offers the lowest latency and tightest integration.</p><p><strong>Switch to Cloud Flows when the ecosystem is the priority.</strong> If your requirement ends with an action in another service, like sending an approval email via Outlook, posting a notification to a Teams channel, or updating a file in SharePoint, don't reinvent the wheel. Use the native Power Pages trigger to invoke a flow and let prebuilt connectors do the work.</p><p><strong>Reserve the Companion App for the "heavy tasks".</strong> If you find yourself hitting the limits of the JavaScript sandbox and task is too complex or code-depndant for Flow, this is a choice for you. Use an Azure Function or Web App if you need to:</p><ul><li>Use a proprietary C# library or a specific NuGet/NPM package.</li><li>Perform high-intensity compute tasks (like image/video processing).</li><li>Access on-premises data via VNet integration.</li></ul><p>Remember that you don't have to choose just one. You might use Server Logic to validate a form synchronously, a Cloud Flow to notify the team afterward, and a Companion App to generate a complex PDF report in the background; all inside the same site.</p><p>The goal isn't to find the "perfect" tool for the whole project, but to choose the most efficient tool for the specific task at hand.</p><h2>Conclusion</h2><p>In my opinion, Server Logic is the new default for the Power Pages backend. It offers a great combination of capabilities to create secure and high-performance endpoints, internal implementations of which shouldn't be exposed to the client.</p><p>This doesn't mean that Cloud Flows or Companion Apps are obsolete. They still serve a purpose by solving specific problems and being ideal for their own scenarios.</p><p>You don't have to choose one - just select the tool that is the best for the job.</p><h2>Credits</h2><p>Cover Image by <a href="https://pixabay.com/users/wilhei-883152/?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=705666">Willi Heidelbach</a> from <a href="https://pixabay.com//?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=705666">Pixabay</a></p><p>Server Logic Image by Microsoft</p>]]></content:encoded>
            <author>oleksandr@dancingwithcrm.com (Oleksandr Olashyn)</author>
            <enclosure url="https://www.dancingwithcrm.com/assets/img/2026-04-26-cover-rss.jpg" length="0" type="image/jpg"/>
        <media:content url="https://www.dancingwithcrm.com/assets/img/2026-04-26-cover-rss.jpg" medium="image"/>
        </item>
        <item>
            <title><![CDATA[Switching from Jekyll to Astro]]></title>
            <link>https://www.dancingwithcrm.com/switching-from-jekyll-to-astro/</link>
            <guid isPermaLink="false">https://www.dancingwithcrm.com/switching-from-jekyll-to-astro/</guid>
            <pubDate>Tue, 14 Apr 2026 03:45:20 GMT</pubDate>
            <description><![CDATA[After 7 years of Liquid and Jekyll, it was time for a new playground. I’m sharing the full breakdown of my move to Astro—including how I preserved legacy URLs, switched to Bun for speed, and used AI to refactor years of code in a fraction of the time.]]></description>
            <content:encoded><![CDATA[<h2>Why I Started to Look Around</h2><p>My blog has been running on Jekyll and GitHub Pages since 2019. When I first started writing, I didn't just want a place to share ideas; I wanted a playground where I could experiment.</p><p>Back in 2019, Dynamics Portals (way before they became Power Pages) were gradually becoming my main focus. I decided to go with Jekyll as I wanted a blog-focused ecosystem that ran on Liquid, since I wanted to sharpen my skills with it. The main idea was that building my personal website from scratch would help me to upgrade my web development knowledge.</p><p>However, now things are different. Power Pages is evolving as a platform, allowing for hosting SPA and being a much more development-focused environment. Meanwhile, Liquid isn't part of this transformation. And to be honest, there aren't many things that I haven't tried with Liquid for the last 7 years.</p><p>While Jekyll remains a fantastic, reliable choice for static sites, I realized that to keep improving my craft, it was time to try something new.</p><h2>Why Astro?</h2><p>I first found Astro two years ago, when I was creating a documentation website for my <a href=https://www.dancingwithcrm.com/power-pages-lookup-to-select/>Lookup To Select project</a>. Although I could have built it with Jekyll, I wanted to try something new.</p><p>After some searching and comparison, Astro was looking like an ideal candidate:</p><ul><li>support for MD and MDX out of the box,</li><li>static output, so I can still use it with GitHub Pages,</li><li>a wide variety of existing templates,</li><li>framework flexibility, allowing me to use any frontend library or stay HTML-first,</li><li>native support for TypeScript and modern JavaScript features,</li><li>a growing community of passionate developers.</li></ul><p>Overall, Astro felt like the perfect upgrade, offering a modern development experience while still allowing me to maintain the static site benefits I loved about Jekyll.</p><p>Creating a new website felt great, and even back then, I was thinking of moving from Jekyll to Astro. However, there was no “one-click” solution - my existing posts contained a lot of custom liquid code that would require significant refactoring. I tried to use AI for the job - it seemed like an ideal approach; however, technology wasn't there yet, and the initial migration attempt failed. So I decided to postpone the idea.</p><p>Now, with Astro just releasing version 6 and Power Pages supporting Astro out of the box, I decided that the time is right and I should try again.</p><h2>Migration</h2><p>To move to Astro, I needed to:</p><ul><li>rebuilt all the liquid templates and custom components as Astro components,</li><li>migrate all of my posts,</li><li>maintain consistent styling and branding,</li><li>preserve all existing URLs and RSS feeds,</li><li>ensure that the search and subscribe functionality still works.</li></ul><p>I decided to go with Bun as the package manager. I know that it is technically not 100% supported, but I was using Bun a lot recently and really like how snappy and all-in-one it is.</p><h3>From Liquid to .astro Components</h3><p>The primary task was to replace my liquid templates with pure .astro components. In the future, I might add some React or other frameworks, but for the start, pure HTML inside Astro components felt like the best option.</p><p>While Jekyll uses a "top-down" approach where a layout wraps around content, Astro treats everything as a component - even the pages themselves.</p><h3>Understanding Astro's Routing & Page Handling</h3><p>If you're coming from Jekyll, the biggest mental shift is how Astro handles the <code>pages/</code> directory.</p><ul><li><p><strong>File-Based Routing</strong>: In Jekyll, your structure is often dictated by <code>_posts</code> and <code>permalinks</code> in frontmatter. In Astro, any file inside <code>src/pages/</code> automatically becomes a route.</p></li><li><p><strong>The .astro File Structure</strong>: Unlike Liquid, which intersperses logic and HTML, an Astro component uses a "Code Fence" (the --- block) to handle all your logic at build time.</p></li></ul><p>For my migration, I utilized <strong>Dynamic Routes</strong> to handle my legacy post structure. Instead of having hundreds of individual files in my pages folder, I used a single <code>[...slug].astro</code> file that fetches content from my collections and generates the pages dynamically at build time.</p><h3>Managing Content with Collections</h3><p>In Jekyll, your posts are just files in a directory. There's no built-in way to enforce that every post has a title or a date, and if you make a typo in the frontmatter, the build might just fail silently. Astro's <strong>Content Collections API</strong> solves this by treating your Markdown files like a database with a schema.</p><p>By defining a schema using <strong>Zod</strong> in <code>content.config.ts</code>, I was able to:</p><ul><li><p><strong>Enforce Data Integrity</strong>: I made fields like title and date required. Beyond just checking for missing fields, Astro validates the provided data types, for example: ensuring that dates are actual Date objects and tags are arrays of strings. If a legacy post had a malformed date or an incorrect data type, Astro caught it at build time before it could break the site.</p></li><li><p><strong>Enable Type Safety</strong>: My IDE now provides autocomplete for post properties, so I don't have to remember if a field was named thumbnail or image.</p></li></ul><p>My post collection schema looks like this (note the use of <code>.optional()</code> to define optional fields and types like <code>z.coerce.date()</code> to ensure that date strings are properly parsed into Date objects):</p><pre><code class=language-javascript>const posts = defineCollection({
  loader: glob({ pattern: '**/*.{md,mdx}', base: './src/content/posts' }),
  schema: z.object({
    layout: z.string().optional(),
    title: z.string(),
    date: z.coerce.date(),
    description: z.string().optional().default(''),
    excerpt: z.string().optional().default(''),
    shortDescription: z.string().optional().default(''),
    img: z.string().optional().default(''),
    image: z.string().optional().default(''),
    tags: z.array(z.string()).optional().default([]),
  }),
});
</code></pre><p>Below is an example of a frontmatter error that Astro would catch during the build process, which would have been much harder to debug in Jekyll.</p><p><img alt="Content Collections Frontmatter Error Example" src=https://www.dancingwithcrm.com/assets/img/2026-04-13-frontmatter-error.jpg></p><h3>Project Structure Mapping</h3><p>To make the transition smoother, I used the GitHub Copilot CLI with Opus 4.6 to create a migration plan, then used Sonnet 4.6 to execute it.</p><p>Here is how my Jekyll structure maps over to Astro:</p><ul><li><code>_layouts</code> → <code>src/layouts</code> (directly mapped, with some adjustments to fit Astro's component model)</li><li><code>_includes/</code> → <code>src/components/</code> (with additional customization specific components, like Video and UpdateBlock living inside the mdx folder)</li><li><code>_data</code> → <code>src/data</code> (with not just a list of projects, but an additional feeds configuration for RSS, more about it later)</li><li><code>assets</code> → <code>public/assets</code> (structure updated to match the typical Astro structure)</li><li><code>_pages</code> → <code>src/pages</code></li><li><code>_posts</code> → <code>src/content/posts</code></li></ul><p>AI performed significantly better than it did two years ago, aligning the projects with much higher accuracy.</p><h3>Content Migration</h3><p>Honestly, this part was scaring me the most, as there would be a lot of manual work with different liquid-specific tags, but AI was able to resolve it pretty quickly (by writing some temporary Python scripts for replacement and update logic).</p><p>One specific challenge was URL preservation. Jekyll uses a <code>_posts/YYYY-MM-DD-title.md</code> naming convention but omits the date in the final URL. Since Astro uses file paths for slugs by default, I updated the <em><strong>getStaticPaths</strong></em> function inside <code>[...slug].astro</code> component to strip the date prefix, ensuring my legacy links didn't break.</p><pre><code class=language-javascript>export const getStaticPaths: GetStaticPaths = async () => {
  const posts = await getCollection('posts');
  return posts.map((post) => ({
    params: { slug: post.id.replace(/\.mdx?$/, '').replace(/^\d{4}-\d{2}-\d{2}-/, '') },
    props: { post },
  }));
};
</code></pre><p>Other key adjustments included:</p><ul><li><strong>Frontmatter</strong>: Updated the schema in content.config.ts for strict type safety.</li><li><strong>Liquid Tags</strong>: Replaced <code>{% highlight %}</code> with fenced code blocks and <code>{% include %}</code> with MDX component imports. All .md files were renamed to .mdx to support these components. All of these changes were done via AI.</li></ul><h3>Styling and RSS</h3><p>I refactored my SCSS to follow a "one partial per component" structure, switching from the older <code>@import</code> syntax to the modern <code>@use</code> rule.</p><p>For RSS, Astro's default package produces RSS 2.0, but I wanted <strong>Atom</strong> format. I used <a href=https://gsong.dev/articles/astro-feed-unified/>the following article</a> as a reference and built a custom RSS generation pipeline inside <code>feeds.ts</code> using feed and unify packages.</p><p>I also took the opportunity to refresh the site's look and feel a bit. This included a new hero post layout to make the latest updates pop, plus a much simpler header. I purposely kept the same general feel for this first pass, but I plan to experiment with it more in the future.</p><h3>New Features</h3><p>The migration allowed me to implement several upgrades:</p><ul><li><strong>Pagefind Search</strong>: I replaced my jQuery-based search with Pagefind. It runs post-build to generate a static index, resulting in blazing-fast, low-bandwidth searches.</li><li><strong>View Transitions</strong>: This gives the site an SPA-like feel with smooth animations between pages. Currently, it keeps the header persistent during navigation, and I plan to experiment with more dynamic transitions soon.</li><li><strong>Font Optimization</strong>: I'm using Astro's new Font API to handle web font optimization automatically.</li></ul><p>View Transition demo is available below, and you can also check it out live on the site by clicking around the posts (notice that the header stays persistent while the content transitions in and out).</p><h2>Verdict</h2><p>I'm thrilled with this upgrade. The site feels modern, fast, and most importantly, it is once again a playground for me to experiment. AI helped me a lot this time around, making the daunting task of refactoring years of Liquid code manageable.</p><h2>Credits</h2><p>Cover image by <a href="https://pixabay.com/users/nasa-imagery-10/?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=970">NASA-Imagery</a> from <a href="https://pixabay.com//?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=970">Pixabay</a></p>]]></content:encoded>
            <author>oleksandr@dancingwithcrm.com (Oleksandr Olashyn)</author>
            <enclosure url="https://www.dancingwithcrm.com/assets/img/2026-04-13-cover-rss.jpg" length="0" type="image/jpg"/>
        <media:content url="https://www.dancingwithcrm.com/assets/img/2026-04-13-cover-rss.jpg" medium="image"/>
        </item>
        <item>
            <title><![CDATA[Customizing Copilot Agent appearance in Power Pages - Christmas edition]]></title>
            <link>https://www.dancingwithcrm.com/cutomize-copilot-agent-in-powerpages/</link>
            <guid isPermaLink="false">https://www.dancingwithcrm.com/cutomize-copilot-agent-in-powerpages/</guid>
            <pubDate>Wed, 17 Dec 2025 01:45:20 GMT</pubDate>
            <description><![CDATA[With the holidays upon us, I wanted the last article of the year to be light and on theme for Christmas. And what better way to do that than by making your Power Pages chatbot look more festive? Check this post to find out more.]]></description>
            <content:encoded><![CDATA[<p>With the holidays upon us, I wanted the last article of the year to be light and on theme for Christmas. And what better way to do that than by making your Power Pages chatbot look more festive? You can see the final result below.</p><h2>Available styling</h2><p>First, let’s discuss the prerequisites: you have a Power Pages site on the Enhanced Data Model with a Copilot agent provisioned. We will be customizing the “modern” agent, so if you still have Power Virtual Agent, this guide will not apply.</p><p>We will be using <a href="https://learn.microsoft.com/en-us/power-pages/getting-started/customize-your-agent?tabs=modern#customize-the-agent-appearance">this official Microsoft article</a> as a reference point. Spoilers - MS needs to update it, cause some things just don’t work as expected.</p><p>To make the standard Copilot agent look better, we want to update the following elements:</p><ul><li>replace the widget icon with something more Christmas-themed</li><li>update the Header of the chat interface with festive colors</li><li>update chat bubbles for the user and Copilot with holiday colors</li></ul><h2>New icon</h2><p>According to an article on MS Learn, we need to run the following JS code to replace the icon.</p><pre><code class=language-html>&lt;script>
document.addEventListener('DOMContentLoaded', function() {
   var buttons = document.getElementsByClassName("pva-embedded-web-chat-widget");
    buttons[0].innerHTML = '&lt;img src="&lt;image URL>" height= "70px" width = "70px" />';
}, false);
&lt;/script>
</code></pre><p>The problem with this code is that it should trigger when the initial DOM tree is loaded. However, a “modern” agent is added via - dromroll please - React, so the actual render will occur after the DOM tree finishes loading, and this code won’t work.</p><p><img alt="Chat button rendered via React" src=https://www.dancingwithcrm.com/assets/img/2025-12-16-agent-react.png></p><p>Instead, we should use <a href=https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver>MutationObserver</a>. The MutationObserver interface provides the ability to watch for changes being made to the DOM tree. If you never worked with Mutation Observer before, it is pretty simple - you create an observer object that will watch the target element and call the provided callback function when the changes occur. Those changes can be an addition of a new child element to the DOM tree, an attribute change, such as classes update, etc. Inside the callback function, we can validate what the change is and react accordingly.</p><p>We can add the observer to the document body itself; however, there are multiple changes constantly happening to the document body, which will lead to multiple triggers of the callback function, that will be completely unrelated to what we need. So ideally, we want the observer to be as close to the part that we need. To do so, we need to understand what actually renders our agent.</p><p>Microsoft has a web template called Power Virtual Agents. This template is automatically injected at the end of the page. After reviewing this template, we can see that the element that will be there from the start is the div with the name webChat.</p><p><img alt="Original Div for Web Chat" src=https://www.dancingwithcrm.com/assets/img/2025-12-16-agent-original-div.jpg></p><p>So let’s attach the observer to the <strong>“webChat”</strong> div element and wait until the div with the class <strong>“pva-embedded-web-chat”</strong> is rendered. Since it is rendered with our button as a subchild, this is exactly what we need.</p><p><img alt="Agent Div Wrapper" src=https://www.dancingwithcrm.com/assets/img/2025-12-16-agent-div-wrapper.jpg></p><pre><code class=language-javascript>const targetClass = 'pva-embedded-web-chat';

// Select the node that will be observed for mutations (e.g., the body)
const targetNode = document.getElementsByName("webChat")[0];

// Options for the observer: watch for child additions/removals and descendent changes
const config = {
    childList: true,
    subtree: true // Monitor all descendants
};

// Callback function to execute when mutations are observed
const callback = (mutationList, observer) => {
    for (const mutation of mutationList) {
        // Check if the mutation type is 'childList' (nodes added or removed)
        if (mutation.type === 'childList') {
            // Iterate over the added nodes
            for (const node of mutation.addedNodes) {
                // Check if the added node is an Element and has the target class
                if (node.nodeType === Node.ELEMENT_NODE && node.classList.contains(targetClass)) {
                    let button = document.querySelector(".pva-embedded-web-chat-widget > button");
                    // &lt;a href="https://www.flaticon.com/free-icons/chat" title="chat icons">Chat icons created by Marz Gallery - Flaticon&lt;/a>
                    button.innerHTML = '&lt;img src="/christmas-chatbot-icon.png" height="48px" width="48px" />';
                    // Stop observing once the element is found
                    observer.disconnect();
                }
            }
        }
    }
};

// Create a new MutationObserver instance with the callback function
const observer = new MutationObserver(callback);

// Start observing the target node for the configured mutations
observer.observe(targetNode, config);
</code></pre><p>Let’s apply the code, add some styling to make the widget a bit bigger, and see what we got.</p><p>Looks nice during the first load. However, as soon as we open the chat window and then close it again, we are back to square one (except for some styling changes). Why is that? Well, the answer is still React. It dynamically rerenders parts that are needed and removes our chat button with the changes. So what should we do to prevent it? The simplest way is to use the second observer.</p><p>We will add the second observer to the “pva-embedded-web-chat” div, as it stays the same between rerenders. Now we will wait until the div with the class “pva-embedded-web-chat-widget” is rendered. And we will re-add our image then.</p><pre><code class=language-javascript>function initialRenderObservation() {
    const targetClass = 'pva-embedded-web-chat';

    // Select the node that will be observed for mutations (e.g., the body)
    const targetNode = document.getElementsByName("webChat")[0];

    // Options for the observer: watch for child additions/removals and descendent changes
    const config = {
        childList: true,
        subtree: true // Monitor all descendants
    };

    // Callback function to execute when mutations are observed
    const callback = (mutationList, observer) => {
        for (const mutation of mutationList) {
            // Check if the mutation type is 'childList' (nodes added or removed)
            if (mutation.type === 'childList') {
                // Iterate over the added nodes
                for (const node of mutation.addedNodes) {
                    // Check if the added node is an Element and has the target class
                    if (node.nodeType === Node.ELEMENT_NODE && node.classList.contains(targetClass)) {
                        let button = document.querySelector(".pva-embedded-web-chat-widget > button");
                        // &lt;a href="https://www.flaticon.com/free-icons/chat" title="chat icons">Chat icons created by Marz Gallery - Flaticon&lt;/a>
                        button.innerHTML = '&lt;img src="/christmas-chatbot-icon.png" height="48px" width="48px" />';
                        // Stop observing once the element is found
                        observer.disconnect();
                        // Adding second observer
                        followUpRenderObservation();
                    }
                }
            }
        }
    };

    // Create a new MutationObserver instance with the callback function
    const observer = new MutationObserver(callback);

    // Start observing the target node for the configured mutations
    observer.observe(targetNode, config);
}

function followUpRenderObservation() {
    const targetClass = 'pva-embedded-web-chat-widget';

    // Select the node that will be observed for mutations (e.g., the body)
    const targetNode = document.getElementsByClassName("pva-embedded-web-chat")[0];

    // Options for the observer: watch for child additions/removals and descendent changes
    const config = {
        childList: true,
        subtree: true // Monitor all descendants
    };

    // Callback function to execute when mutations are observed
    const callback = (mutationList, observer) => {
        for (const mutation of mutationList) {
            // Check if the mutation type is 'childList' (nodes added or removed)
            if (mutation.type === 'childList') {
                // Iterate over the added nodes
                for (const node of mutation.addedNodes) {
                    // Check if the added node is an Element and has the target class
                    if (node.nodeType === Node.ELEMENT_NODE && node.classList.contains(targetClass)) {
                        let button = document.querySelector(".pva-embedded-web-chat-widget > button");
                        // &lt;a href="https://www.flaticon.com/free-icons/chat" title="chat icons">Chat icons created by Marz Gallery - Flaticon&lt;/a>
                        button.innerHTML = '&lt;img src="/christmas-chatbot-icon.png" height="48px" width="48px" />';
                    }
                }
            }
        }
    };

    // Create a new MutationObserver instance with the callback function
    const observer = new MutationObserver(callback);

    // Start observing the target node for the configured mutations
    observer.observe(targetNode, config);
}

$(document).ready(function () {
    initialRenderObservation();
});
</code></pre><p>Let’s apply the updated code and see what we get.</p><p>Much better now - exactly what we wanted.</p><h2>Header customization</h2><p>Next, we want to update the header - both background and text color. This is actually the same as in the documentation - just update the proper CSS class.</p><p>As for the colors, we will go with the classic Christmas combo of Red background and Gold text.</p><pre><code class=language-css>/* open chat header */
.pages-chatbot-header {
    background: #b11c2c;
    color: #ffe05c;
}
</code></pre><p>Looks great - moving to the final step now.</p><h2>Chat bubble customization</h2><p>To make chat bubble feel more festive, we will update Copilot responses to be Green with White text, and user responses to be Red with White text.</p><p>For the background we need to modify CSS a bit, comparing to what is specified in the docs - the background needs to be applied one level deeper to the “webchat__text-content” class.</p><p>The text styling works as documented.</p><pre><code class=language-css>/* Copilot text bubble */
.webchat__bubble:not(.webchat__bubble--from-user) .webchat__bubble__content,
.webchat__bubble:not(.webchat__bubble--from-user) .webchat__bubble__content .webchat__text-content {
    background-color: #075600 !important;
    border-radius: 5px !important;
}

.webchat__bubble:not(.webchat__bubble--from-user) p {
    font-weight: 400;
    color: #ffffff;
}

/* User text bubble */
.webchat__bubble.webchat__bubble--from-user .webchat__bubble__content,
.webchat__bubble.webchat__bubble--from-user .webchat__bubble__content .webchat__text-content {
    background-color: #b11c2c !important;
    border-radius: 5px !important;
}

.webchat__bubble.webchat__bubble--from-user p {
    font-weight: 400;
    color: #ffffff;
}
</code></pre><p>And the final result looks great!</p><p>Final code can be found below:</p><pre><code class=language-html>&lt;style>
    /* Widget */
    .pva-embedded-web-chat-widget {
        background-color: #208b3a;
        border: 1px solid #000000;
        width: 60px !important;
        max-width: 60px !important;
        height: 60px !important;
    }

    /* open chat header */
    .pages-chatbot-header {
        background: #b11c2c;
        color: #ffe05c;
    }

    /* Copilot text bubble */
    .webchat__bubble:not(.webchat__bubble--from-user) .webchat__bubble__content,
    .webchat__bubble:not(.webchat__bubble--from-user) .webchat__bubble__content .webchat__text-content {
        background-color: #075600 !important;
        border-radius: 5px !important;
    }

    .webchat__bubble:not(.webchat__bubble--from-user) p {
        font-weight: 400;
        color: #ffffff;
    }

    /* User text bubble */
    .webchat__bubble.webchat__bubble--from-user .webchat__bubble__content,
    .webchat__bubble.webchat__bubble--from-user .webchat__bubble__content .webchat__text-content {
        background-color: #b11c2c !important;
        border-radius: 5px !important;
    }

    .webchat__bubble.webchat__bubble--from-user p {
        font-weight: 400;
        color: #ffffff;
    }
&lt;/style>

&lt;script>
    function initialRenderObservation() {
        const targetClass = 'pva-embedded-web-chat';

        // Select the node that will be observed for mutations (e.g., the body)
        const targetNode = document.getElementsByName("webChat")[0];

        // Options for the observer: watch for child additions/removals and descendent changes
        const config = {
            childList: true,
            subtree: true // Monitor all descendants
        };

        // Callback function to execute when mutations are observed
        const callback = (mutationList, observer) => {
            for (const mutation of mutationList) {
                // Check if the mutation type is 'childList' (nodes added or removed)
                if (mutation.type === 'childList') {
                    // Iterate over the added nodes
                    for (const node of mutation.addedNodes) {
                        // Check if the added node is an Element and has the target class
                        if (node.nodeType === Node.ELEMENT_NODE && node.classList.contains(targetClass)) {
                            let button = document.querySelector(".pva-embedded-web-chat-widget > button");
                            // &lt;a href="https://www.flaticon.com/free-icons/chat" title="chat icons">Chat icons created by Marz Gallery - Flaticon&lt;/a>
                            button.innerHTML = '&lt;img src="/christmas-chatbot-icon.png" height="48px" width="48px" />';
                            // Stop observing once the element is found
                            observer.disconnect();

                            followUpRenderObservation();
                        }
                    }
                }
            }
        };

        // Create a new MutationObserver instance with the callback function
        const observer = new MutationObserver(callback);

        // Start observing the target node for the configured mutations
        observer.observe(targetNode, config);
    }

    function followUpRenderObservation() {
        const targetClass = 'pva-embedded-web-chat-widget';

        // Select the node that will be observed for mutations (e.g., the body)
        const targetNode = document.getElementsByClassName("pva-embedded-web-chat")[0];

        // Options for the observer: watch for child additions/removals and descendent changes
        const config = {
            childList: true,
            subtree: true // Monitor all descendants
        };

        // Callback function to execute when mutations are observed
        const callback = (mutationList, observer) => {
            for (const mutation of mutationList) {
                // Check if the mutation type is 'childList' (nodes added or removed)
                if (mutation.type === 'childList') {
                    // Iterate over the added nodes
                    for (const node of mutation.addedNodes) {
                        // Check if the added node is an Element and has the target class
                        if (node.nodeType === Node.ELEMENT_NODE && node.classList.contains(targetClass)) {
                            let button = document.querySelector(".pva-embedded-web-chat-widget > button");
                            // &lt;a href="https://www.flaticon.com/free-icons/chat" title="chat icons">Chat icons created by Marz Gallery - Flaticon&lt;/a>
                            button.innerHTML = '&lt;img src="/christmas-chatbot-icon.png" height="48px" width="48px" />';
                        }
                    }
                }
            }
        };

        // Create a new MutationObserver instance with the callback function
        const observer = new MutationObserver(callback);

        // Start observing the target node for the configured mutations
        observer.observe(targetNode, config);
    }

    $(document).ready(function () {
        initialRenderObservation();
    });
&lt;/script>
</code></pre><h2>Conclusion</h2><p>In this article, I provided step-by-step instructions on how you can customize your Copilot agent in Power Pages, focusing on making it Christmas-themed. Hope you enjoyed this article.</p><h2>Credits</h2><p>Cover Image by <a href="https://pixabay.com/users/suttlemedia-12054259/?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=7982424">Shawn Suttle</a> from <a href="https://pixabay.com//?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=7982424">Pixabay</a></p>]]></content:encoded>
            <author>oleksandr@dancingwithcrm.com (Oleksandr Olashyn)</author>
            <enclosure url="https://www.dancingwithcrm.com/assets/img/2025-12-16-cover-rss.jpg" length="0" type="image/jpg"/>
        <media:content url="https://www.dancingwithcrm.com/assets/img/2025-12-16-cover-rss.jpg" medium="image"/>
        </item>
        <item>
            <title><![CDATA[Enhancing Power Pages interactivity with htmx]]></title>
            <link>https://www.dancingwithcrm.com/htmx-with-powerpages/</link>
            <guid isPermaLink="false">https://www.dancingwithcrm.com/htmx-with-powerpages/</guid>
            <pubDate>Tue, 09 Dec 2025 02:45:20 GMT</pubDate>
            <description><![CDATA[Learn how to use htmx to create an interactive UI without full-screen refreshes using Liquid (and no additional JS)]]></description>
            <content:encoded><![CDATA[<p>What if I told you that you can create an interactive UI without full-screen refreshes using Liquid? What if I told you that you could do it without writing a single line of JS code? All you need is a small library called htmx. Don’t believe me? Well, see for yourself - active search and even an infinite scroll scenario, all without JS. Let’s find out how I did it and how you can do it too.</p><h2>What is htmx?</h2><p>htmx is a JavaScript library that lets you write less JS by extending HTML (hence the name HTMX). htmx allows you to extend HTML so that components can react to events, send HTTP requests, perform CSS transitions, and much more. You can find more about htmx by visiting <a href=https://htmx.org/>htmx official website</a>.</p><p>The simplest example of htmx will look like this.</p><pre><code class=language-html>  &lt;button hx-post="/clicked" hx-swap="outerHTML">
    Click Me
  &lt;/button>
</code></pre><p>The <strong>hx-post</strong> and <strong>hx-swap</strong> attributes on this button tell htmx:</p><blockquote><p>“When a user clicks on this button, issue an AJAX request to <strong>/clicked</strong>, and replace the entire button with the HTML response.”</p></blockquote><h2>Liquid and htmx</h2><p>Liquid is a template language. This means that it is executed on the server. Because of this, any interactivity with liquid usually requires a full-page refresh. Which, in the modern web landscape, can feel outdated and slow for the users.</p><p>This is where htmx comes to play - we can use it to simplify getting Liquid from the server and making our pages much more dynamic.</p><p>Using Liquid with dynamic fetching is not a new idea. Way before we had Power Pages Web API, the most common way to get the data was to use a page as an API approach - a web template behind the page was using fetchxml to perform necessary queries, and print a formatted response as part of the page.</p><p>With htmx, we will use the same approach - create a new page to serve as an API. However, what makes this different from the usual way is twofold: htmx will make it easier to query and consume the page, and the page itself will return valid HTML instead of pseudo-JSON. Although we could achieve the same results while using JS, htmx allows us to write less code, keeping logic on the page cleaner and more understandable.</p><h2>Active Search</h2><p>Imagine we have the following requirement: show the list of available courses and allow the user to search by the course name. The search should happen without a full page refresh.</p><p>Usually, we wouldn’t go with the Liquid and fetchxml - since we don’t want to have the full page refresh. However, using Power Pages Web API can require a lot of JS code and additional site setting configuration. And what if we need to have an additional hidden filtering condition (like only show a certain type of courses) that should not be visible to the user? You can, of course, use the new Server Logic; however, it is still in Preview, and we also want to keep our code simple. So let’s use htmx and Liquid to fulfill the requirement.</p><p>First, we need to understand how our page will work and which components we will need.</p><p>We would need the following components:</p><ul><li>an input that allows users to search for courses</li><li>the list of courses</li><li>a loading indicator to show to the users while we are searching</li></ul><p>We can use htmx with the search input to both load the initial list and trigger a reload when the user inputs the search term.</p><pre><code class=language-html>    &lt;input class="form-control" 
       type="search"
       name="search" 
       placeholder="Begin Typing To Search Courses..."
       hx-get="/search-courses"
       hx-trigger="input changed delay:500ms, keyup[key=='Enter'], load"
       hx-target="#search-results"
       hx-indicator=".htmx-indicator">
</code></pre><p>The <strong>hx-get</strong> tells htmx to perform the GET request to the /search-courses URL. It will automatically include the value of our input as the <em>“search”</em> query parameter (because we defined the name of our control as “search”).</p><p>The <strong>hx-trigger</strong> attribute tells htmx to trigger on the input with a 500ms delay, to ensure we don’t trigger events on every key press. We pass the “changed” modifier to ensure that only when the value has actually changed, to perform a request. We added two additional comma-separated triggers: when users click Enter and on the initial page load.</p><p>The <strong>hx-target</strong> attribute tells htmx to swap the content of the element with the id <em>“search-results”</em> with the content of the response.</p><p>The <strong>hx-indicator</strong> attribute tells htmx that we are using an element with the class <em>“htmx-indicator”</em> as a loading indicator, so htmx will show it while waiting on the response, and will hide it as soon as the response is received.</p><p>Now let’s add a simple svg and <em>“Searching...”</em> text as a loading indicator.</p><pre><code class=language-html>  &lt;div class="htmx-indicator d-flex align-items-center" id="htmx_search_indicator">
    &lt;div style="width: 100px;">
      &lt;!-- SVG goes here -->
    &lt;/div>
    &lt;span style="font-size: 24px;">Searching...&lt;/span>
  &lt;/div>
</code></pre><p>Next, we will add the <em>“search-results”</em> element, where the list will be rendered. We will use the “dl” HTML - description list element.</p><pre><code class=language-html>&lt;dl id="search-results">&lt;/dl>
</code></pre><p>Last, but certainly not least, we need to implement the /search-courses URL. For that, we will create a new web page that will use fetchxml to get the list of courses and then will render “dt” and “dd” tags for each course, with the corresponding course name and description. It will also use the provided “search” query parameter as the criteria. In case we have no results, we will simply return div with “No Results Found” text.</p><pre><code class=language-html>
{% fetchxml courses %}
&lt;fetch mapping='logical'>
  &lt;entity name='cr770_course'>
    &lt;attribute name='cr770_courseid'>&lt;/attribute>
    &lt;attribute name='cr770_coursename'>&lt;/attribute>
    &lt;attribute name='cr770_description'>&lt;/attribute>
    &lt;order attribute='cr770_coursename'>&lt;/order>
    &lt;filter type='and'>
      &lt;condition attribute='cr770_coursename' operator='like' value='%{{params.search}}%'>&lt;/condition>
    &lt;/filter>
  &lt;/entity>
&lt;/fetch>
{% endfetchxml %}

{% if courses.results.entities.size > 0 %}
  {% for course in courses.results.entities %}
  &lt;dt>{{course.cr770_coursename}}&lt;/dt>
  &lt;dd>{{course.cr770_description}}&lt;/dd> 
  {% endfor %}
{% else %}
&lt;div>No Results Found&lt;/div>
{% endif %}

</code></pre><p>Combining all of this (and a bit of CSS to style our list), we are getting the dynamic and interactive UI that we needed in just 50 lines of HTML with no extra JS code.</p><h2>Infinite Scroll</h2><p>But what if we have too much data to load all at once? One of the modern ways to solve an issue like that is to implement an Infinite Scroll mechanism. It will work as follows: first, we will load a small subset of data, say 10 records. Then, as soon as we scroll to the last record, we will load the next set of 10 records. Repeat until all the data is loaded.</p><p>To make this happen, we need to adjust both our main page, the Liquid page, and the triggering logic for additional data load.</p><p>On the main page, we need to add a new loader indicator - specifically for when we load additional data. We will place it below the “search-results” container, so it will always show at the bottom, where the new results will render. We also need to update the hx-indicator attribute on the search input to point to the id of the search indicator instead of the "htmx-indicator" class, which will exist on the new indicator as well.</p><p>Input with updated hx-indicator</p><pre><code class=language-html>    &lt;input class="form-control" 
       type="search"
       name="search" 
       placeholder="Begin Typing To Search Courses..."
       hx-get="/search-courses"
       hx-trigger="input changed delay:500ms, keyup[key=='Enter'], load"
       hx-target="#search-results"
       hx-indicator="#htmx_search_indicator">
</code></pre><p>“search-results” with new loading indicator</p><pre><code class=language-html>    &lt;dl id="search-results">&lt;/dl>
    &lt;div class="htmx-indicator d-flex align-items-center justify-content-center" id="htmx_scroll_indicator">
      &lt;div style="width: 100px;">
        &lt;!-- SVG goes here -->
      &lt;/div>
      &lt;span style="font-size: 24px;">Loading...&lt;/span>
    &lt;/div>
</code></pre><p>Now we need to adjust the Liquid side.</p><p>The fetchxml query will be restricted to 10 records per page, by using the count parameter and will accept the current page from the query parameters.</p><p>The triggering event for the next set of data, will also be specifed as part of this page - when we render the last element of the results, we will check if more records exists, and if true we will add new htmx attribute to the last dd tag, which will be responsible for triggering the next data load. The following attributes will be added:</p><ul><li><strong>hx-get</strong> - to tell htmx to perform a GET request with the next page number and search term</li><li><strong>hx-trigger</strong> - to tell htmx to trigger when element becomes visible</li><li><strong>hx-swap</strong> - to tell htmx to append the respons html after this element</li><li><strong>hx-indicator</strong> - to tell htmx to use element with specified id as an indicator</li></ul><p>We will also adjust No Results logic - it will only show that message if our query returns 0 results and we are on the first page (to eliminate showing No Results if we have multiple pages).</p><pre><code class=language-html>
{% assign pageNumber = request.params['page'] | default:1 %}

{% fetchxml courses %}
&lt;fetch mapping='logical' page="{{ pageNumber }}" count="10">
  &lt;entity name='cr770_course'>
    &lt;attribute name='cr770_courseid'>&lt;/attribute>
    &lt;attribute name='cr770_coursename'>&lt;/attribute>
    &lt;attribute name='cr770_description'>&lt;/attribute>
    &lt;order attribute='cr770_coursename'>&lt;/order>
    &lt;filter type='and'>
      &lt;condition attribute='cr770_coursename' operator='like' value='%{{params.search}}%'>&lt;/condition>
    &lt;/filter>
  &lt;/entity>
&lt;/fetch>
{% endfetchxml %}

  {% for course in courses.results.entities %}
    {% if forloop.last and courses.results.more_records %}
      &lt;dt>{{course.cr770_coursename}}&lt;/dt>
      &lt;dd 
        hx-get="/search-courses/?page={{ pageNumber | integer | plus: 1}}&search={{params.search}}"
        hx-trigger="revealed"
        hx-swap="afterend"
        hx-indicator="#htmx_scroll_indicator"
        >{{course.cr770_description}}&lt;/dd> 
    {% else %}
      &lt;dt>{{course.cr770_coursename}}&lt;/dt>
      &lt;dd>{{course.cr770_description}}&lt;/dd> 
    {% endif %}
  {% endfor %}

  {% if courses.results.entities.size == 0 and pageNumber == 1 %}
    &lt;dt>No results found&lt;/dt>
  {% endif %}

</code></pre><p>And here is how our implementation of the Infinite Scroll looks.</p><h2>Conclusion</h2><p>htmx with Liquid breathes new life into the page interactivity. Obviously, this is not a replacement for Power Pages Web API and custom JS - we will always have complex scenarios and user interactions that the htmx approach cannot solve. However, it is an ideal combo for many easier, most common use cases, allowing you to create beautiful interactive experiences with less code and shorter timeframes.</p><p>You can find the full code at <a href=https://github.com/OOlashyn/htmx-with-powerpages>my GitHub repo</a>.</p><h2>Credits</h2><p>Cover image by <a href="https://pixabay.com/users/eluj-1927408/?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=3131802">Eluj</a> from <a href="https://pixabay.com//?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=3131802">Pixabay</a></p><p><a href=https://www.svgbackgrounds.com/elements/animated-svg-preloaders/>Animated SVG Preloaders by SVGBackgrounds.com</a></p>]]></content:encoded>
            <author>oleksandr@dancingwithcrm.com (Oleksandr Olashyn)</author>
            <enclosure url="https://www.dancingwithcrm.com/assets/img/2025-12-08-cover-rss.jpg" length="0" type="image/jpg"/>
        <media:content url="https://www.dancingwithcrm.com/assets/img/2025-12-08-cover-rss.jpg" medium="image"/>
        </item>
        <item>
            <title><![CDATA[Custom Lookup Filtering in 2024]]></title>
            <link>https://www.dancingwithcrm.com/lookup-filtering-in-2024/</link>
            <guid isPermaLink="false">https://www.dancingwithcrm.com/lookup-filtering-in-2024/</guid>
            <pubDate>Wed, 16 Oct 2024 01:45:20 GMT</pubDate>
            <description><![CDATA[Learn how you can perform custom lookup filtering in Power Pages]]></description>
            <content:encoded><![CDATA[<h2>Introduction</h2><p>One of the most common questions I receive is how to perform custom lookup filtering in Power Pages / Power Apps Portals. Even <a href=https://www.dancingwithcrm.com/custom-lookup-filtering-powerapps-portal/>one of my most viewed posts</a> is about this topic. However, that post was written in 2019 and a lot has changed in the last five years (although the question remains the same). So let's revisit that old question and provide an answer on how you can custom filter lookups in 2024.</p><h2>Approach</h2><p>First, let's discuss our approach. We cannot filter results in the OOB modal window, as the site returns only a set of records per page. Applying a filter there will yield empty rows or even whole pages. So, we will not use the OOB modal experience at all and instead create our control to handle lookup selection.</p><p>In my previous article, I proposed using Form Metadata to render lookup as a dropdown, then fetch the data we need, and replace available options in the dropdown with the data we received. Although this can and will work, there is a big issue with the server-side validation. See, Power Pages validate your submission by comparing the values system originally provided with your submission. If your result was not on the list sent to you originally by the server (which can happen, especially with big datasets) you will have the server-side error.</p><p>To prevent this problem, the easiest way is to keep the original lookup control (and not render it as a dropdown), hide it and create a custom control instead, that will present available options and populate the value back to the original control. In this case, the system will only validate if the submitted record belongs to the original query.</p><blockquote><p><strong>Important</strong></p><p>Even with a custom control approach you still need to make sure that the value you submit will be included in the view for the lookup control. For example, if the view shows only active records and you try to submit inactive the system will still throw an error.</p></blockquote><h2>Custom Control</h2><p>Now that we know the approach we will take let's see what can we do in terms of the control itself. We cannot use PCF Code Components, as at the time of writing, they aren't supporting Lookups. So, we need to create something on the fly. For this article, we will go with the simplest approach, which will be a basic dropdown, but in actual projects, you can create almost anything. See more complex implementation examples via external library Lookup to Select later in this article.</p><p>Before building control, we need to get the data. We have three main ways to get it:</p><ol><li>Power Pages Web API</li><li>Custom Page that acts as an API endpoint</li><li>Cloud Flows</li></ol><h3>Fetching Power Pages Web API</h3><p>Power Pages Web API provides a convenient way to get the data you need. You can easily apply filters, sort, etc. When I was writing my previous article, Web API wasn't an option. Right now though, it is the most straightforward way. For this article, we will use Web API in the following example.</p><p>However, there are some crucial things you need to consider before going with the Web API approach. The most important one is security. <strong>Depending on the type of data you need to get, there might be cases when unfiltered data shouldn't be exposed to the user in any way, shape or form.</strong> In those cases, using Web API might not be a choice, as users could just perform a query in the console and obtain information they shouldn't see. Also, when using Web API it is really important to just define the columns your users need to access.</p><p>You can learn more about using Power Pages Web API by visiting <a href=https://learn.microsoft.com/en-us/power-pages/configure/web-api-overview>Microsoft official documentation</a> and my <a href=https://www.dancingwithcrm.com/powerapps-portal-web-api-deep-dive/>Deep Dive into Web API post</a>.</p><h3>Fetching with Custom Pages</h3><p>One of the oldest ways to get data in Power Pages, before the Web API, was using liquid fetchxml requests and custom pages, which will serve as a custom API endpoint.</p><p>In short, you create a web template which contains liquid with the necessary fetchxml query. You can configure it to receive parameters and return json as a result, so it will act as our custom web API endpoint.</p><p>There are great articles from Colin Vermander (<a href=https://colinvermander.wordpress.com/2017/04/17/dynamics-365-portals-use-liquid-to-return-json-or-xml/>first</a> and <a href=https://colinvermander.wordpress.com/2018/06/26/dynamics-365-portal-use-liquid-fetchxml-with-paging-cookie/>second</a>) that explain this concept in more detail. So definitely check them out to learn more.</p><p>This approach is better from a security perspective if you need to hide additional filtering criteria and want to make sure your user cannot modify them via the request itself. For example, if you want to filter by users account and don't want them to see it.</p><h3>Fetching with Cloud Flows</h3><p>One of the most recent additions to the Power Pages is the ability to call the cloud flows natively. This allows sending and receiving data from Power Automate and performing various actions that not yet available in Power Pages.</p><p>This approach is better from a security perspective, especially, if you want to provide access or filter the data that is not available for the portal users.</p><p><strong>The downside of this approach is potential licensing cost</strong> - Cloud Flows aren't part of the Power Pages licensing and their consumptions and limits might restrict their usage.</p><p>To find more about how you can call Cloud Flows from Power Pages visit <a href=https://learn.microsoft.com/en-us/power-pages/configure/cloud-flow-integration>this official documentation page</a>.</p><h3>Creating the control</h3><p>Now that we know how to get the data let's create a control. As stated above, in this example we will be creating a simple dropdown and fetch the data using Power Pages Web API.</p><pre><code class=language-javascript>// function that will return filtered response
// using Power Pages Web API
function getSampleFilteredResponse(filterCondition) {
  // webapi.safeAjax is a wrapper provided by Microsoft
  // to call Power Pages Web API (you need to manually add it to the page)
  // for the latest wrapper and sample please visit official documentation via link below
  // https://learn.microsoft.com/en-us/power-pages/configure/write-update-delete-operations#web-api-ajax-samples
  return webapi.safeAjax({
    type: "GET",
    url: `/_api/dwctst_countries?$select=dwctst_countryid,dwctst_name&$filter=startswith(dwctst_name,'${filterCondition}')`,
    contentType: "application/json",
    });
}

$(document).ready(async function () {
    const lookupToHideId = "dwctst_country";
    const lookupTableLogicalName = "dwctst_country";
    const idFieldName = "dwctst_countryid";
    const textFieldName = "dwctst_name";
    // hide original lookup
    $(`#${lookupToHideId}`).parent().hide();

    const customLookupId = "my_custom_lookup";
    // create custom select
    const customSelect = $(`&lt;select class='form-control picklist' id='${customLookupId}'>`);
    // place custom select after existing lookup
    $(`#${lookupToHideId}`).parent().after(customSelect);

    // add default selected Select option
    // to mimic OOB select behavior
    customSelect.append(`&lt;option selected="selected" value="" label="Select" aria-label="Select">Select&lt;/option>`);

    try {
        let results = await getSampleFilteredResponse("U");

        if (results.value.length > 0) {
            // create an option element for each result
            // and append it to custom select
            results.value.forEach(el =>
                customSelect.append(`&lt;option value='${el[idFieldName]}'>${el[textFieldName]}&lt;/option>`)
            )
        }
    } catch (error) {
        // an error occured retrieving data
        // place your error processing logic here
        console.error(error);
    }

    // add onchange event to populate actual lookup
    customSelect.on("change", function () {
        // get selected id
        const selectedValue = customSelect.val();
        // get selected text
        const selectedValueLabel = $(`#${customLookupId} option:selected`).text();

        $(`#${lookupToHideId}`).val(selectedValue);
        $(`#${lookupToHideId}_entityname`).val(lookupTableLogicalName);
        $(`#${lookupToHideId}_name`).val(selectedValueLabel);

        // trigger change event on original lookup
        $(`#${lookupToHideId}`).trigger("change");
    });

});
</code></pre><p>See example below: only showing countries that starts with "U"</p><h2>External library - Lookup to Select</h2><p>To simplify, speed up implementation in complex scenarious and provide users with better experience you can use existing free (like <a href=https://select2.org/>Select2</a> or <a href=https://jqueryui.com/selectmenu/#default>jQuery UI Selectmenu</a>) or paid (like <a href=https://demos.telerik.com/kendo-ui/dropdownlist/index>Kendo UI jQuery DropDownList</a>) controls instead of simple dropdowns.</p><p>I created Power Page Lookup to Select (LtS for short) - a wrapper around the Select2 library. When you call LtS it hides OOB lookup control, creates a new instance of Select2 and an additional select field to hold possible values, and binds the custom select field to OOB control to propagate the value.</p><p>See LtS in action below:</p><p>To find all the ways you can use LtS please visit <a href=https://www.dancingwithcrm.com/power-pages-lookup-to-select/>the official documentation website</a>.</p><h2>Conclusion</h2><p>In this article I described a simle way of filtering lookup:</p><ul><li>hide original control</li><li>fetch data via Power Pages Web API/Custom Fetchxml Pages/Cloud Flows</li><li>create custom dropdown with the retrieved data</li></ul><p>You can use this approach on any type of form. Hope you find this article useful.</p><p>Cover Image by <a href="https://pixabay.com/users/pdpics-44804/?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=389900">PDPics</a> from <a href="https://pixabay.com//?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=389900">Pixabay</a></p>]]></content:encoded>
            <author>oleksandr@dancingwithcrm.com (Oleksandr Olashyn)</author>
            <enclosure url="https://www.dancingwithcrm.com/assets/img/2024-10-15-cover-rss.jpg" length="0" type="image/jpg"/>
        <media:content url="https://www.dancingwithcrm.com/assets/img/2024-10-15-cover-rss.jpg" medium="image"/>
        </item>
        <item>
            <title><![CDATA[Power Pages Lookup to Select]]></title>
            <link>https://www.dancingwithcrm.com/lookup-to-select/</link>
            <guid isPermaLink="false">https://www.dancingwithcrm.com/lookup-to-select/</guid>
            <pubDate>Mon, 27 May 2024 02:45:20 GMT</pubDate>
            <description><![CDATA[Learn how you can easily replace your lookup control with Select2 control]]></description>
            <content:encoded><![CDATA[<p>One of the most common questions I have in my Power Pages projects is regarding lookup control:</p><ul><li>can we change it to a dropdown?</li><li>can we add search/autocomplete?</li><li>can we show some additional information?</li></ul><p>And the answer is usually the same - no. The introduction of custom code components via Power Apps Component Framework gives us a glimpse of hope, however, it is not available for the Power Pages, yet.</p><p>What if I told you that you could do all of the above with just a couple of lines of code? Welcome Power Pages Lookup to Select - a jQuery library, that allows you to replace your OOB lookup control with a much more capable <a href=https://select2.org/>Select2</a> control.</p><p>See it in action below:</p><p>Visit <a href=https://www.dancingwithcrm.com/power-pages-lookup-to-select/>documentation</a> to find more information on how to get started.</p><h2>How it works</h2><p>Power Pages Lookup to Select (LtS for short) is a wrapper around the Select2 library. When you call LtS it hides OOB lookup control, creates a new instance of Select2 and an additional select field to hold possible values, and binds the custom select field to OOB control to propagate the value.</p><h3>What LtS can do</h3><p>To find all the ways you can use LtS please visit <a href=https://www.dancingwithcrm.com/power-pages-lookup-to-select/>the official documentation website</a>.</p><h3>Basic configuration</h3><p>In the most basic version, LtS provides search/autocomplete functionality</p><h3>Custom options rendering</h3><p>LtS allows you to customize how your options are rendered to the user. This can be useful if you want to show additional information to make it easier to select the correct option.</p><h3>Custom grouping</h3><p>LtS allows you to provide additional grouping for your option. This can be useful if you want to show additional information to make it easier to select the correct option.</p><h2>Open source and contribution</h2><p>This project is open source - so if you have an idea of how to make it better feel free to contribute. There are multiple ways how to contribute:</p><ul><li>open an issue to let us know about bugs or problems</li><li>look into existing issues to help to resolve existing problems</li><li>create a PR for new functionality or enhance existing one</li></ul><p>Visit <a href=https://github.com/OOlashyn/power-pages-lookup-to-select>the GitHub repository</a> for more information.</p><h2>Conclusion</h2><p>I hope you will find LtS useful. I will continue to maintain and enhance it, and hope the community will do it as well. Visit <a href=https://www.dancingwithcrm.com/power-pages-lookup-to-select/>the official documentation site</a> and <a href=https://github.com/OOlashyn/power-pages-lookup-to-select>GitHub repository</a> for the most up-to-date information on the project.</p>]]></content:encoded>
            <author>oleksandr@dancingwithcrm.com (Oleksandr Olashyn)</author>
            <enclosure url="https://www.dancingwithcrm.com/assets/img/2024-05-26-lts-hero-rss.png" length="0" type="image/png"/>
        <media:content url="https://www.dancingwithcrm.com/assets/img/2024-05-26-lts-hero-rss.png" medium="image"/>
        </item>
        <item>
            <title><![CDATA[Snowfall animation for Power Pages]]></title>
            <link>https://www.dancingwithcrm.com/snowfall-in-power-pages/</link>
            <guid isPermaLink="false">https://www.dancingwithcrm.com/snowfall-in-power-pages/</guid>
            <pubDate>Mon, 18 Dec 2023 02:45:20 GMT</pubDate>
            <description><![CDATA[Learn how to add a simple snowfall animation to your Power Pages site]]></description>
            <content:encoded><![CDATA[<p>Christmas is almost here and the festive atmosphere kicks in. Let's learn how we can add some of that winter/holiday spirit to our Power Pages site. In this article, you will find a couple of ways to add a snowfall animation to your site.</p><h2>Snowfall with CSS only</h2><p>In the first approach, we will be using <a href=https://github.com/pajasevi/CSSnowflakes>CSSnowflakes</a>, which is a combination of CSS and HTML without any JS.</p><p>This is what our final result will look like:</p><p>Let's start by creating a new Web Template called <strong>"Snowfall with CSS Only"</strong>. We will place the following code there:</p><pre><code class=language-html>&lt;style>
/* customizable snowflake styling */
.snowflake {
  color: #fff;
  font-size: 1em;
  font-family: Arial, sans-serif;
  text-shadow: 0 0 5px #000;
}

.snowflake,.snowflake .inner{animation-iteration-count:infinite;animation-play-state:running}@keyframes snowflakes-fall{0%{transform:translateY(0)}100%{transform:translateY(110vh)}}@keyframes snowflakes-shake{0%,100%{transform:translateX(0)}50%{transform:translateX(80px)}}.snowflake{position:fixed;top:-10%;z-index:9999;-webkit-user-select:none;user-select:none;cursor:default;animation-name:snowflakes-shake;animation-duration:3s;animation-timing-function:ease-in-out}.snowflake .inner{animation-duration:10s;animation-name:snowflakes-fall;animation-timing-function:linear}.snowflake:nth-of-type(0){left:1%;animation-delay:0s}.snowflake:nth-of-type(0) .inner{animation-delay:0s}.snowflake:first-of-type{left:10%;animation-delay:1s}.snowflake:first-of-type .inner,.snowflake:nth-of-type(8) .inner{animation-delay:1s}.snowflake:nth-of-type(2){left:20%;animation-delay:.5s}.snowflake:nth-of-type(2) .inner,.snowflake:nth-of-type(6) .inner{animation-delay:6s}.snowflake:nth-of-type(3){left:30%;animation-delay:2s}.snowflake:nth-of-type(11) .inner,.snowflake:nth-of-type(3) .inner{animation-delay:4s}.snowflake:nth-of-type(4){left:40%;animation-delay:2s}.snowflake:nth-of-type(10) .inner,.snowflake:nth-of-type(4) .inner{animation-delay:2s}.snowflake:nth-of-type(5){left:50%;animation-delay:3s}.snowflake:nth-of-type(5) .inner{animation-delay:8s}.snowflake:nth-of-type(6){left:60%;animation-delay:2s}.snowflake:nth-of-type(7){left:70%;animation-delay:1s}.snowflake:nth-of-type(7) .inner{animation-delay:2.5s}.snowflake:nth-of-type(8){left:80%;animation-delay:0s}.snowflake:nth-of-type(9){left:90%;animation-delay:1.5s}.snowflake:nth-of-type(9) .inner{animation-delay:3s}.snowflake:nth-of-type(10){left:25%;animation-delay:0s}.snowflake:nth-of-type(11){left:65%;animation-delay:2.5s}
&lt;/style>
&lt;div class="snowflakes" aria-hidden="true">
  &lt;div class="snowflake">
    &lt;div class="inner">❅&lt;/div>
  &lt;/div>
  &lt;div class="snowflake">
    &lt;div class="inner">❅&lt;/div>
  &lt;/div>
  &lt;div class="snowflake">
    &lt;div class="inner">❅&lt;/div>
  &lt;/div>
  &lt;div class="snowflake">
    &lt;div class="inner">❅&lt;/div>
  &lt;/div>
  &lt;div class="snowflake">
    &lt;div class="inner">❅&lt;/div>
  &lt;/div>
  &lt;div class="snowflake">
    &lt;div class="inner">❅&lt;/div>
  &lt;/div>
  &lt;div class="snowflake">
    &lt;div class="inner">❅&lt;/div>
  &lt;/div>
  &lt;div class="snowflake">
    &lt;div class="inner">❅&lt;/div>
  &lt;/div>
  &lt;div class="snowflake">
    &lt;div class="inner">❅&lt;/div>
  &lt;/div>
  &lt;div class="snowflake">
    &lt;div class="inner">❅&lt;/div>
  &lt;/div>
  &lt;div class="snowflake">
    &lt;div class="inner">❅&lt;/div>
  &lt;/div>
  &lt;div class="snowflake">
    &lt;div class="inner">❅&lt;/div>
  &lt;/div>
&lt;/div>
</code></pre><p>snowflake class allows us to change the properties of the snowflakes on our page (like size, colour, etc).</p><p>To add more snowflakes you would need to create new divs inside the snowflakes container and update the corresponding CSS (currently each snowflake has its own animation delay and position, so when adding a new one we need to create additional CSS for it).</p><p>The aria-hidden attribute is added to the snowflakes container to hide it from screen readers.</p><p>Then we will add a Site Setting called <strong>Snowfall/Enabled</strong> and set it to true. This setting will allow us to turn snow on and off without changes in our code.</p><p>Then we will add the following code to the <strong>Header</strong> web template, so it will apply to all pages with the header</p><pre><code class=language-liquid>
{% assign snowFall_enabled = settings['Snowfall/Enabled'] | boolean %}
{% if snowFall_enabled %}
  {% include "Snowfall with CSS Only" %}
{% endif %}

</code></pre><h2>Snowfall with JS</h2><p>For the second approach, we will generate our snowfall using JS (from <a href=https://codepen.io/ethancopping/pen/ExrGYGG>this CodePen</a>).</p><p>Our final result will look like this:</p><p>We will create a new Web Template called <strong>"Snowfall with JS"</strong>. We will place the following code there:</p><pre><code class=language-html>&lt;style>
.snow-container {
    position: fixed;
    top: 0;
    left: 0;
    overflow: hidden;
    width: 100vw;
    height: 100vh;
    z-index: 99999;
    pointer-events: none;
}

.snowflake {
    position: absolute;
    background-color: white;
    border-radius: 50%;
    opacity: 0.8;
    pointer-events: none;
}

@keyframes fall {
    0% {
        opacity: 0;
        transform: translateY(0);
    }
    10% {
        opacity: 1;
    }
    100% {
        opacity: 0.5;
        transform: translateY(100vh);
    }
}

@keyframes diagonal-fall {
    0% {
        opacity: 0;
        transform: translate(0, 0);
    }
    10% {
        opacity: 1;
    }
    100% {
        opacity: 0.25;
        transform: translate(10vw, 100vh);
    }
}
&lt;/style>
&lt;script>
document.addEventListener("DOMContentLoaded", function () {
    const snowContainer = document.querySelector(".snow-container");

    const particlesPerThousandPixels = 0.1;
    const fallSpeed = 1; // controls fall speed
    const pauseWhenNotActive = true;
    const maxSnowflakes = 100; // controls max number of snowflakes
    const snowflakes = [];

    let snowflakeInterval;
    let isTabActive = true;

    function resetSnowflake(snowflake) {
        const size = Math.random() * 5 + 1;
        const viewportWidth = window.innerWidth - size; // Adjust for snowflake size
        const viewportHeight = window.innerHeight;

        snowflake.style.width = `${size}px`;
        snowflake.style.height = `${size}px`;
        snowflake.style.left = `${Math.random() * viewportWidth}px`; // Constrain within viewport width
        snowflake.style.top = `-${size}px`;

        const animationDuration = (Math.random() * 3 + 2) / fallSpeed;
        snowflake.style.animationDuration = `${animationDuration}s`;
        snowflake.style.animationTimingFunction = "linear";
        snowflake.style.animationName =
            Math.random() &lt; 0.5 ? "fall" : "diagonal-fall";

        setTimeout(() => {
            if (parseInt(snowflake.style.top, 10) &lt; viewportHeight) {
                resetSnowflake(snowflake);
            } else {
                snowflake.remove(); // Remove when it goes off the bottom edge
            }
        }, animationDuration * 1000);
    }

    function createSnowflake() {
        if (snowflakes.length &lt; maxSnowflakes) {
            const snowflake = document.createElement("div");
            snowflake.classList.add("snowflake");
            snowflakes.push(snowflake);
            snowContainer.appendChild(snowflake);
            resetSnowflake(snowflake);
        }
    }

    function generateSnowflakes() {
        const numberOfParticles =
            Math.ceil((window.innerWidth * window.innerHeight) / 1000) *
            particlesPerThousandPixels;
        const interval = 5000 / numberOfParticles;

        clearInterval(snowflakeInterval);
        snowflakeInterval = setInterval(() => {
            if (isTabActive && snowflakes.length &lt; maxSnowflakes) {
                requestAnimationFrame(createSnowflake);
            }
        }, interval);
    }

    function handleVisibilityChange() {
        if (!pauseWhenNotActive) return;

        isTabActive = !document.hidden;
        if (isTabActive) {
            generateSnowflakes();
        } else {
            clearInterval(snowflakeInterval);
        }
    }

    generateSnowflakes();

    window.addEventListener("resize", () => {
        clearInterval(snowflakeInterval);
        setTimeout(generateSnowflakes, 1000);
    });

    document.addEventListener("visibilitychange", handleVisibilityChange);
});
&lt;/script>
&lt;div class="snow-container" aria-hidden="true">&lt;/div>
</code></pre><p>This version of the code allows you to customize fall speed, number of snowflakes, if snowfall pauses if there is no interaction with the page for some time etc. See the code for the full list of available options.</p><p>The aria-hidden attribute is added to the snowflakes container to hide it from screen readers.</p><p>Similarly to the CSS-only approach we need to have a site setting to control our snowfall visibility without change to the code. Add a Site Setting called <strong>Snowfall/Enabled</strong> and set it to true if you didn't do so in the previous example.</p><p>Then we will add the following code to the <em>Header</em> web template, so it will apply to all pages with the header</p><pre><code class=language-liquid>
{% assign snowFall_enabled = settings['Snowfall/Enabled'] | boolean %}
{% if snowFall_enabled %}
  {% include "Snowfall with JS" %}
{% endif %}

</code></pre><h2>Conclusion</h2><p>In this article, I showed you a couple of ways to add a winter spirit to your website by enabling snowfall animation. These code snippets can be further enhanced by adding, for example, a custom button that allows users to enable/disable animation dynamically.</p>]]></content:encoded>
            <author>oleksandr@dancingwithcrm.com (Oleksandr Olashyn)</author>
            <enclosure url="https://www.dancingwithcrm.com/assets/img/2023-12-17-cover-rss.jpg" length="0" type="image/jpg"/>
        <media:content url="https://www.dancingwithcrm.com/assets/img/2023-12-17-cover-rss.jpg" medium="image"/>
        </item>
        <item>
            <title><![CDATA[Power Pages Helper]]></title>
            <link>https://www.dancingwithcrm.com/powerpages-helper/</link>
            <guid isPermaLink="false">https://www.dancingwithcrm.com/powerpages-helper/</guid>
            <pubDate>Mon, 04 Dec 2023 02:45:20 GMT</pubDate>
            <description><![CDATA[Learn about version 1.4.0 of my Power Pages Helper extension]]></description>
            <content:encoded><![CDATA[<p>All things evolve and change. Especially, Microsoft's product naming. Power Apps Portal became Power Pages. So, my Visual Studio Code extension should evolve as well. Goodbye Power Apps Portal Helper, welcome Power Pages Helper. Beyond just the name change, the latest version packs a lot of new features like support for Enhance Data Model, per workspace settings and much more. Let's dive into what the latest release brings to the table.</p><h2>Enhanced Data Model support</h2><p>Enhanced Data Model recently went GA and brought a lot of changes with it. If you tried to download the site via PAC CLI you might encounter an error saying that a website with a specific id wasn't found. And this is because to download an EDM site you need to specify to the CLI that this is an EDM site. To do so you need to pass the modelVersion 2 parameter to the request so it looks like this</p><pre><code class=language-powershell>pac powerpages download --path "C:\portals" --webSiteId "site-id" --modelVersion 2
</code></pre><p>With the latest version of Power Pages Helper, you can now select modelVersion when using <strong>Download Portal</strong> command.</p><p>You also have a new advanced setting to specify the version for the Download Latest command, so when you are using a shortcut it will add the correct model version automatically (see below for the full list of advanced settings).</p><h2>Migrate to Bootstrap 5</h2><p>Added support for Migrate to Bootstrap 5 command that allows you to convert an existing portal to Bootstrap 5 (this action will create a new folder with the updated portal).</p><h2>Advanced Settings</h2><h3>Settings in VS Code</h3><p>Settings in VS Code have 2 scopes:</p><ul><li><strong>User Settings</strong> - Settings that apply globally to any instance of VS Code you open.</li><li><strong>Workspace Settings</strong> - Settings are stored inside your workspace and only apply when the workspace is opened.</li></ul><p>This allows you to create specific configurations for different projects. For example: if you have one standard portal and one enhanced data model portal you can configure them to use different sets of settings by specifying different Workspace settings.</p><h3>How to find and configure settings</h3><p>To get to the settings go to <strong>File > Preferences > Settings</strong> and search for Power Pages Helper.</p><p>There you will see 5 different settings: 3 related to the Download Latest command and 2 related to the Download Portal command.</p><p><img alt="List of Available setings" src=https://www.dancingwithcrm.com/assets/img/2023-12-03-settings-list.jpg></p><h3>Download Latest settings</h3><p>The <strong>Exclude Entities</strong> setting allows you to provide the list of entities' logical names to exclude during the site download using Download Latest command.</p><p>The <strong>Include Entities</strong> setting allows you to provide the list of entities' logical names to include during the site download using Download Latest command.</p><p>The <strong>Model Version</strong> setting allows you to specify the version of the model to download (1 or 2, where 1 - standard and 2 - enhanced) when using Download Latest command. If not specified, the version 1 will be used by default.</p><h3>Download Portal settings</h3><p><strong>Skip Excluded Entities</strong> settings allow you to skip the Excluded Entities step during the Download Portal command execution.</p><p><strong>Skip Included Entities</strong> settings allow you to skip the Included Entities step during the Download Portal command execution.</p><h2>Miscellaneous</h2><p>Switched to using the powerpages command instead of the paportal command, as per recommendation from Microsoft.</p><h2>Conclusion</h2><p>I will keep evolving Power Pages Helper as I am constantly using it in my projects. If you haven't downloaded this extension already - give it a try to see how it can enhance your developer experience. And if you have any feedback - feel free to hop to the <a href=https://github.com/OOlashyn/portal-helper-vscode/discussions>Discussion section</a> of the repository or open an <a href=https://github.com/OOlashyn/portal-helper-vscode/issues>Issue</a>. I would like this extension to be useful for all Power Pages developers, so your feedback matters.</p>]]></content:encoded>
            <author>oleksandr@dancingwithcrm.com (Oleksandr Olashyn)</author>
            <enclosure url="https://www.dancingwithcrm.com/assets/img/2023-12-03-cover-rss.jpg" length="0" type="image/jpg"/>
        <media:content url="https://www.dancingwithcrm.com/assets/img/2023-12-03-cover-rss.jpg" medium="image"/>
        </item>
        <item>
            <title><![CDATA[Web Roles and Enhanced Data Model]]></title>
            <link>https://www.dancingwithcrm.com/web-roles-and-enhanced-data-model/</link>
            <guid isPermaLink="false">https://www.dancingwithcrm.com/web-roles-and-enhanced-data-model/</guid>
            <pubDate>Mon, 13 Nov 2023 02:45:20 GMT</pubDate>
            <description><![CDATA[Learn how to view and dynamically assign Web Roles in the Enhanced Data Model]]></description>
            <content:encoded><![CDATA[<p>Web Roles are the core of the Power Pages security. They define who and how can access the data on our site. We are using them constantly. However, if you enabled the Enhanced Data Model you might notice that things are different: Related records missing associated Web Roles, and dynamically assigning web roles via adx relationship doesn't work anymore. In this article, I will show you what is going on with Web Roles in the new data model and how you can resolve both of the mentioned issues.</p><h2>How the Enhanced Data Model works</h2><p>Enhanced Data Model doesn't have separate tables for many things. Instead, it uses a table called Site Components that holds everything (from Web Role, and Site Settings to Web Files). Then Microsoft uses plugins and some magic to present those records as separate tables via Virtual Tables. So the Web Role that you see for the Enhanced Data Model (table name mspp_webrole) is actually just an abstraction on top of the Site Component table. Microsoft just presents it to use as separate tables in a familiar interface.</p><p><img alt="Enhanced Data Model" src=https://www.dancingwithcrm.com/assets/img/2023-11-12-enhanced-data-model.png></p><h2>How to find currently assigned roles</h2><p>Previously, when you tried to find Web Roles assigned to a contact, you would go to the Related tab on the Portal Contact form and select Web Roles. However, if you do it now, it will show you the old adx_webrole table. Unfortunately, you cannot show the virtual table in the Related section.</p><p>To view roles, currently assigned to a contact, open a contact using the <strong>Portal Contact (Enhanced Form)</strong> form, and navigate to the <strong>General</strong> tab > <strong>Web Roles</strong> section. There you will see a list of already assigned roles and can assign new ones as well.</p><p><img alt="Web Roles on the Contact form" src=https://www.dancingwithcrm.com/assets/img/2023-11-12-portal-contact-form.jpg></p><h2>How to dynamically assign roles</h2><p>Previously, with the Standard Data Model, when you wanted to dynamically assign a web role, you would use, for example, Power Automate to associate the Contact table with the Web Role table via adx_webrole_contact relationship. But, with the Enhance Data Model, you need to use the Site Component table instead. On the Contact table, you have the Relationship called <strong>powerpagecomponent_mspp_webrole_contact</strong> which links the Contact and Site Component table. Use that relationship and the odata id of the web role (<strong>mspp_webrole</strong> one) to associate the table (you can get the Odata id if you get that web role via Get Row action or in the next format <em>https://ORGNAME.crm.dynamics.com/api/data/v9.1/mspp_webroles(GUID)</em>.</p><p><img alt="Associate web role dynamically" src=https://www.dancingwithcrm.com/assets/img/2023-11-12-associate-web-role.png></p><h2>How to dynamically remove roles</h2><p>Similarly, to how we associated web roles we can dynamically remove them using Power Automate. This time, however, we need to get odataid of the Site Component behind the Web Role. To do so you can first query Web Role by name and then query Site Component by id or if you know that id query it directly. After getting odataid we need to perform an Unrelate action from the contact to the Site Component table via powerpagecomponent_mspp_webrole_contact relationship.</p><p><img alt="Remove web role dynamically" src=https://www.dancingwithcrm.com/assets/img/2024-02-26-unrelate-web-role.jpg></p><h2>Conclusion</h2><p>Enhanced Data Model is a big shift to make Power Pages more aligned with the usual ALM story for Power Platform, more performant and easier to update. And, although, from the surface, things might look the same, under the hood a lot has changed. This includes changes to the Web Roles and how we interact with them. In this article, you found how you can find current associated roles and how to dynamically assign new ones. Hope you find it useful.</p>]]></content:encoded>
            <author>oleksandr@dancingwithcrm.com (Oleksandr Olashyn)</author>
            <enclosure url="https://www.dancingwithcrm.com/assets/img/2023-11-12-cover-rss.jpg" length="0" type="image/jpg"/>
        <media:content url="https://www.dancingwithcrm.com/assets/img/2023-11-12-cover-rss.jpg" medium="image"/>
        </item>
        <item>
            <title><![CDATA[Sending files to cloud flows from Power Pages]]></title>
            <link>https://www.dancingwithcrm.com/calling-cloud-flows-in-power-pages/</link>
            <guid isPermaLink="false">https://www.dancingwithcrm.com/calling-cloud-flows-in-power-pages/</guid>
            <pubDate>Mon, 06 Nov 2023 02:45:20 GMT</pubDate>
            <description><![CDATA[Learn how to send files to cloud flows from Power Pages]]></description>
            <content:encoded><![CDATA[<p>Calling cloud flows from Power Pages was always a complicated endeavour from a security and general perspective. However, an introduction of native support for this back in June was a game changer. Now, we can even send files from Power Pages to cloud flows. Let's see how it can be done and what pitfalls you need to avoid (including some issues with flows itself - see more at the end of the post). At the end of this article you will find the full code and demo on how this works, so feel free to jump ahead.</p><h2>Action plan</h2><p>We will add a file input to a webpage. On file load, we will call a cloud flow. This flow will accept a file input (image), save that file as a note, get that note from the system back and return it to Power Page to show the image on the screen.</p><h2>Creating a flow</h2><p>First, we need to create our cloud flow. To do so open the solution you want to create your flow from (as you can only call solution-aware flows from Power Pages). Select <strong>When Power Pages calls a flow</strong> as your trigger action. There you can select your inputs. For this sample, we will select two: file input called customFile and text input called customTextField.</p><p><img alt="Cloud flow trigger with inputs" src=https://www.dancingwithcrm.com/assets/img/2023-11-05-cloud-trigger-param.jpg></p><p>Next, we need to create a new note. We will select the Add Row action, select Notes as our Table and provide Title, DocumentBody (which is file content) and FileName. To get the file name we will use the next expression <strong>triggerBody()?['file']?['name']</strong>. To get the file body we could use the provided customFile option, however, because our file content is base64 string system sometimes throws the error when parsing (<strong>"Invalid character in field 'documentbody': '.', hexadecimal value 0x00, is an invalid character."</strong>). To avoid this problem we can encode and decode string directly using the next formula: <strong>decodeBase64(base64(triggerBody()?['file']?['contentBytes']))</strong>.</p><p><img alt="Add note step" src=https://www.dancingwithcrm.com/assets/img/2023-11-05-add-note.jpg></p><p>Now when the note is created let's retrieve it back from the system.</p><p><img alt="Get note step" src=https://www.dancingwithcrm.com/assets/img/2023-11-05-get-note.jpg></p><p>Last we need to return values to Power Pages: in responseFile output, we will return our image from the newly created note and in responseText output, we will return "Some returned text".</p><p><img alt="Return to Power Pages step" src=https://www.dancingwithcrm.com/assets/img/2023-11-05-return-to-power-pages.jpg></p><p>Now our flow looks like this.</p><p><img alt="Full cloud flow" src=https://www.dancingwithcrm.com/assets/img/2023-11-05-full-flow.jpg></p><p>Let's move to the Power Pages part.</p><h2>Add cloud flow to Power Pages</h2><p>In Power Pages Studio navigate to the Set up section and select Cloud flows under integration. Select Add existing flow, choose newly created flow and select Web Roles that will be allowed to call the flow.</p><p><img alt="Add cloud flow in Power Pages studio" src=https://www.dancingwithcrm.com/assets/img/2023-11-05-add-cloud-flow-in-studio.jpg></p><p>After that click the Copy icon in the URL field to copy the URL of the flow. We will use it later to call the flow from the code.</p><h2>Call flow from Power Pages</h2><p>Open a page from which you want to call the flow. In this example, I will use VSCode online for code editing.</p><p>Let's add the next HTML code: simple file input ( with the sendAndReceiveFile function that will be triggered on the change event), an image to show the returned file and some explanation text.</p><pre><code class=language-html>&lt;p>Choose a file to save as a note in the system&lt;/p>
&lt;input type="file" name="" onchange="sendAndReceiveFile()" />
&lt;img id="returnedImage" />
</code></pre><p>First, we need to manage our file input. We will create a function called sendAndReceiveFile which will create a new file reader and read our file as a data URL (we need this as flow accepts files as base64 encoded string).</p><pre><code class=language-javascript>  function sendAndReceiveFile() {
    // get our input
    const file = document.querySelector("input[type=file]").files[0];
    //initialize new file reader
    const reader = new FileReader();

    // trigger sendToFlow function
    // when file reading ends
    reader.addEventListener(
      "load",
      () => {
        // get base64 string without 'data:image/jpeg;base64' part
        const base64str = reader.result.split(',')[1];
        sendToFlow(file.name, base64str);
      },
      false,
    );

    if (file) {
      // read file from our input as base64 encoded string
      reader.readAsDataURL(file);
    }

  }
</code></pre><p>Next, we need to create a function which will call the flow itself. We will call it sendToFlow and will accept the file name and base64 encoded file body.</p><p>We need to form a request object. First, we create an object with our inputs. The name of the inputs should be the same as in the Power Pages trigger. File input should be an object with two properties: name - to hold the name of the file and contentBytes - which will contain base64 string.</p><pre><code class=language-javascript>    let data = {
      customTextField: "Hi, I am text from site",
      customFile: {
        name: fileName,
        contentBytes: b64str
      }
    };
</code></pre><p>The request object itself should only have one parameter eventData - which should be a stringified version of the object with inputs.</p><pre><code class=language-javascript>    let requestObj = {
      eventData: JSON.stringify(data)
    };
</code></pre><p>To call the flow I will be using shell.ajaxSafePost method provided by MS - this way I don't need to worry about adding a CSRF token to my requests. However, you are free to use fetch or another way to create a POST request as long as you handle token generation and include it in the correct header with the request.</p><p>The URL for the request would be a simplified version of the URL we copied when we configured cloud flow. We removed a part before /api to make it universal and avoid issues with environment-specific deployments.</p><pre><code class=language-javascript>    shell.ajaxSafePost({
      type: "POST",
      contentType: "application/json",
      url: "/_api/cloudflow/v1.0/trigger/YOUR_CLOUD_FLOW_GUID",
      data: JSON.stringify(requestObj),
      processData: false,
      global: false
    })
      .done(function (response) {
        handleFlowResponse(response);
      })
      .fail(function (er) {
        console.error(er);
      });
</code></pre><p>Last, we need to create a function that will handle the response from the flow. handleFlowResponse will parse the returned string as an object. This object will contain our outputs. <strong>IMPORTANT</strong> - as of now returned object have our output name in lowercase so make sure you use the correct names. Let's add our returned image to the page and observe the result.</p><pre><code class=language-javascript>  function handleFlowResponse(response) {
    const responseObj = JSON.parse(response);
    console.log("responseObj", responseObj);
    const previewImg = document.getElementById("returnedImage");
    previewImg.src = "data:image/jpeg;base64," + responseObj.responsefile;
  }
</code></pre><h2>RESULT</h2><p>Below you can find small demo of sending and receiving file form cloud flow:</p><h2>ERRORS AND ISSUES</h2><p>When you call the flow you might receive an error that input parameters are in incorrect format. Please check your flow as an error might be caused by problems in other parts of the flow unrelated to the inputs.</p><p><strong>IMPORTANT</strong> Currently, there is an issue with sending files more than 2mb in size. You will get: "Error during serialization or deserialization using the JSON JavaScriptSerializer. The length of the string exceeds the value set on the maxJsonLength property. Parameter name: input". Microsoft was notified about this problem.</p><p>Potential workaround: thanks to <a href=https://github.com/LyleStanton>Lyle Stanton</a> for suggestion.</p><p>To avoid this issue try to not provide contentType explicetly, remove processData and global from the ajaxSafePost request and do not stringify request object</p><pre><code class=language-javascript>  function sendToFlow(fileName, b64str) {
    // create object with inputs
    // file input should have both name which holds filename
    // and contentBytes which holds base64 string representation of the file
    let data = {
      customTextField: "Hi, I am text from site",
      customFile: {
        name: fileName,
        contentBytes: b64str
      }
    };

    // actual request object should have only one parameter
    // called eventData which shoould be stringified
    // version of the object with inputs
    let requestObj = {
      eventData: JSON.stringify(data)
    };

    // I am using ajaxSafePost as it automatically adds
    // CSRF token, however, you can use fetch or other method
    // as long as you manually handle getting the token
    // and adding it to appropriate header
    shell.ajaxSafePost({
      type: "POST",
      contentType: "application/json",
      url: "/_api/cloudflow/v1.0/trigger/YOUR_CLOUD_FLOW_GUID",
      data: requestObj,
    })
      .done(function (response) {
        handleFlowResponse(response);
      })
      .fail(function (er) {
        console.error(er);
      });
  }
</code></pre><h2>CONCLUSION</h2><p>Cloud flows in Power Pages are a huge addition to the product, which extends its capabilities tremendously. And the addition of File type input pushed it even further. Calling cloud flows from the site is quite straightforward, however, documentation around it could be better. I am happy the product is growing, and moving forward will be using Power Automate to achieve even more. Hope you find this article useful.</p><h2>Full code</h2><pre><code class=language-html>&lt;p>Choose a file to save as a note in the system&lt;/p>
&lt;input type="file" name="" onchange="sendAndReceiveFile()" />
&lt;img id="returnedImage" />
&lt;script>

  function handleFlowResponse(response) {
    const responseObj = JSON.parse(response);
    console.log("responseObj", responseObj);
    const previewImg = document.getElementById("returnedImage");

    //using lower case responsefile instead of responseFile
    //as currently cloud flow returns everything lowercase 
    previewImg.src = "data:image/jpeg;base64," + responseObj.responsefile; 
  }

  function sendToFlow(fileName, b64str) {
    // create object with inputs
    // file input should have both name which holds filename
    // and contentBytes which holds base64 string representation of the file
    let data = {
      customTextField: "Hi, I am text from site",
      customFile: {
        name: fileName,
        contentBytes: b64str
      }
    };

    // actual request object should have only one parameter
    // called eventData which shoould be stringified
    // version of the object with inputs
    let requestObj = {
      eventData: JSON.stringify(data)
    };

    // I am using ajaxSafePost as it automatically adds
    // CSRF token, however, you can use fetch or other method
    // as long as you manually handle getting the token
    // and adding it to appropriate header
    shell.ajaxSafePost({
      type: "POST",
      contentType: "application/json",
      url: "/_api/cloudflow/v1.0/trigger/YOUR_CLOUD_FLOW_GUID",
      data: JSON.stringify(requestObj),
      processData: false,
      global: false
    })
      .done(function (response) {
        handleFlowResponse(response);
      })
      .fail(function (er) {
        console.error(er);
      });
  }

  function sendAndReceiveFile() {

    // get our input
    const file = document.querySelector("input[type=file]").files[0];
    //initialize new file reader
    const reader = new FileReader();

    // trigger sendToFlow function
    // when file reading ends
    reader.addEventListener(
      "load",
      () => {
        // get base64 string without 'data:image/jpeg;base64' part
        const base64str = reader.result.split(',')[1];
        sendToFlow(file.name, base64str);
      },
      false,
    );

    if (file) {
      // read file from our input as base64 encoded string
      reader.readAsDataURL(file);
    }

  }
&lt;/script>
</code></pre>]]></content:encoded>
            <author>oleksandr@dancingwithcrm.com (Oleksandr Olashyn)</author>
            <enclosure url="https://www.dancingwithcrm.com/assets/img/2023-11-05-cover-rss.jpg" length="0" type="image/jpg"/>
        <media:content url="https://www.dancingwithcrm.com/assets/img/2023-11-05-cover-rss.jpg" medium="image"/>
        </item>
        <item>
            <title><![CDATA[Bootstrap 5 migration]]></title>
            <link>https://www.dancingwithcrm.com/bootstrap5-in-power-pages/</link>
            <guid isPermaLink="false">https://www.dancingwithcrm.com/bootstrap5-in-power-pages/</guid>
            <pubDate>Thu, 12 Oct 2023 01:45:20 GMT</pubDate>
            <description><![CDATA[Learn how to migrate your Power Pages site to Bootstrap 5]]></description>
            <content:encoded><![CDATA[<p>Bootstrap 5 is finally here. One of the most requested features for a very long time is available for us. So let's see what it can do.</p><p>Before we begin I would like to highlight that currently, this feature is in Preview status meaning it is not suitable for production. Although the GA date is set for November of this year I would advise to really test it out before moving to production.</p><h2><strong>How to migrate</strong></h2><p>First, we need to enable Bootstrap 5 for our environment. To do so go to Power Platform Admin Center, select an environment that you need, and in the Resources tab select Power Pages sites, then in the header select Enable Bootstrap v5 for new sites (preview).</p><p><img alt="Bootstrap 5 switch in Admin Center" src=https://www.dancingwithcrm.com/assets/img/2023-10-11-enable-bootstrap5.jpg></p><p>Next, we would need to use PAC CLI to convert our site to use Bootstrap 5. Microsoft will not automatically convert your existing website to keep backward compatibility. Instead, they provide a new command <strong>pac powerpages bootstrap-migrate</strong> to migrate your site.</p><p>Connect to the environment you want via PAC CLI (either via auth command or through Power Platform Tools extension). Download your website via the <strong>pac powerpages download</strong> command. Then run <strong>pac powerpages bootstrap-migrate -p "path-to-your-folder"</strong>. You can also use my <a href="https://marketplace.visualstudio.com/items?itemName=oleksandr-olashyn.portal-helper-vscode">Power Pages Helper extension</a> to invoke this command.</p><p>Bootstrap-migrate will create a new folder with the Bootstrap 5 version of your site. In the folder,beside the updated portal, you will find a logs.txt document which will highlight which files were changed by the migration tool. This is useful for troubleshooting, validation and general knowledge purposes. For example, I have a template called bootstrap3-test that uses a Bootstrap 3 carousel for images. After migration in logs.txt, I can find a detailed description of which classes were removed or replaced with their Bootstrap 5 alternatives.</p><p><img alt="Migration logs file" src=https://www.dancingwithcrm.com/assets/img/2023-10-11-logsfile.jpg></p><p>Also in the Site Settings file, you can find a new setting called Site/BootstrapV5Enabled which says site to use Bootstrap 5.</p><p><img alt="Migration logs file" src=https://www.dancingwithcrm.com/assets/img/2023-10-11-bootstrap5-setting.jpg></p><p>After migration we just need to upload the updated version back to the system using the <strong>pac powerpages upload</strong> command and enjoy new styles.</p><h2>Version comparison</h2><p><em><strong>Main Page and Navigation Menu Bootstrap 3</strong></em></p><p><img alt="Main Page and Navigation Menu Bootstrap 3" src=https://www.dancingwithcrm.com/assets/img/2023-10-11-main-nav-bv3.jpg></p><p><em><strong>Main Page and Navigation Menu Bootstrap 5</strong></em></p><p><img alt="Main Page and Navigation Menu Bootstrap 5" src=https://www.dancingwithcrm.com/assets/img/2023-10-11-main-nav-bv5.jpg></p><p>As you can see after migration navigation looks more condensed and paddings around links are completely different. Also, the logo and company name are shifted to the right.</p><p><em><strong>Contact Us form Bootstrap 3</strong></em></p><p><img alt="Contact Us form Bootstrap 3" src=https://www.dancingwithcrm.com/assets/img/2023-10-11-form-bv3.jpg></p><p><em><strong>Contact Us form Bootstrap 5</strong></em></p><p><img alt="Contact Us form Bootstrap 5" src=https://www.dancingwithcrm.com/assets/img/2023-10-11-form-bv5.jpg></p><p>Form controls look almost the same, but in the Bootstrap 5 version, they are more rounded by default.</p><p>As you can see not everything went smoothly - the navigation menu after migration requires additional CSS styling to look good. So be careful and make sure you inspect all pages as your styling might break in unexpected places.</p><h2>Other changes</h2><p>Beyond Bootstrap 5 Microsoft also updated icons to use Font Awesome 6 - so you can enjoy a bigger selection of icons, styles and improved performance.</p><h2>Conclusion</h2><p>Bootstrap 5 was long awaited update for Power Pages. This change will allow us to create more complex and modern-looking websites. However, the transition isn't smooth and will require effort to ensure that migrated styles work as well as the originals.</p>]]></content:encoded>
            <author>oleksandr@dancingwithcrm.com (Oleksandr Olashyn)</author>
            <enclosure url="https://www.dancingwithcrm.com/assets/img/2023-01-25-bootstrap5-rss.jpg" length="0" type="image/jpg"/>
        <media:content url="https://www.dancingwithcrm.com/assets/img/2023-01-25-bootstrap5-rss.jpg" medium="image"/>
        </item>
        <item>
            <title><![CDATA[How to fix "XRM Network error operation not allowed as mime type is blocked"]]></title>
            <link>https://www.dancingwithcrm.com/xrm-network-error/</link>
            <guid isPermaLink="false">https://www.dancingwithcrm.com/xrm-network-error/</guid>
            <pubDate>Tue, 25 Jul 2023 01:45:20 GMT</pubDate>
            <description><![CDATA[Learn how to fix "XRM Network error operation not allowed as mime type is blocked"]]></description>
            <content:encoded><![CDATA[<p>Recently when uploading the Power Pages site to a new environment I was met with the next error: <strong>"XRM Network error operation not allowed as mime type img/svg is blocked"</strong>. And fixing this error led me to discover some differences between the old and new UI for the Power Platform Admin Center for an environment and new options that are now available to us. So let's check it out.</p><h2>The old way</h2><p>Power Platform environment configurations allowed us to manage the list of blocked extensions for quite some time. To do so you need to go to your environment, click <strong>Advanced Settings > Settings > Administration > System Settings</strong>. There in the <strong>General</strong> tab, you will see a list of the blocked extensions for the attachments.</p><p><img alt="Old UI" src=https://www.dancingwithcrm.com/assets/img/2023-07-24-oldui.jpg></p><p>However, in my case, there was no svg there already. So what was blocking my file upload? Well, the new UI gives us more options to control extensions.</p><h2>The new way and solution</h2><p>Let's go to the <strong>Power Platform Admin Center</strong>, and select an environment that we need. In the environment go to <strong>Settings</strong>, expand the <strong>Product</strong> section and click <strong>Privacy+Security</strong>. There are 3 options which allow you to control files for the environment:</p><ul><li><strong>Blocked Attachments</strong> - this same option as before where you can provide a list of blocked extension fields</li><li><strong>Blocked Mime Type</strong> - a new option allows you to block specific mime types allowing more granular control over attachments</li><li><strong>Allowed Mime Types</strong> - a new option which is useful when you have only a couple of mime types that should be allowed in your system. This will take precedence over the Blocked Mime Types option.</li></ul><p><img alt="New UI" src=https://www.dancingwithcrm.com/assets/img/2023-07-24-newui.jpg></p><p>The second option was a culprit in my case as image/svg+xml was set in this field preventing svg upload to the system. Clearing up <strong>"Blocked Mime Type"</strong> resolved the problem.</p><h2>Conclusion</h2><p>So to reiterate - if you have an "XRM Network error operation not allowed as mime type is blocked" issue go to the Power Platform Admin Center and make sure that your file's mime type is not in the list of blocked attachments or mime types (or added to the allowed mime type list if your environment has stricter level of security). Hope this will help.</p>]]></content:encoded>
            <author>oleksandr@dancingwithcrm.com (Oleksandr Olashyn)</author>
            <enclosure url="https://www.dancingwithcrm.com/assets/img/2023-07-02-cover.jpg" length="0" type="image/jpg"/>
        <media:content url="https://www.dancingwithcrm.com/assets/img/2023-07-02-cover.jpg" medium="image"/>
        </item>
        <item>
            <title><![CDATA[Top 7 things from Power Platform 2023 release wave 2]]></title>
            <link>https://www.dancingwithcrm.com/top-things-from-2023-release-wave-2/</link>
            <guid isPermaLink="false">https://www.dancingwithcrm.com/top-things-from-2023-release-wave-2/</guid>
            <pubDate>Thu, 20 Jul 2023 01:45:20 GMT</pubDate>
            <description><![CDATA[Learn about top 7 things I think are the most interesting in Power Platform 2023 release wave 2]]></description>
            <content:encoded><![CDATA[<p>A couple of days ago, Microsoft published their Power Platform and Dynamics 365 2023 release wave 2 notes (see <a href=https://learn.microsoft.com/en-us/power-platform/release-plan/2023wave2/>here</a> for Power Platform and <a href=https://learn.microsoft.com/en-us/dynamics365/release-plan/2023wave2/>here</a> for Dynamics 365) and as usual it has a lot of exciting things, especially in regards to AI and Bootstrap 5 ~~at last~~. Below you can find my top things from this release in no particular order. If you are interested in older release notes check my previous <a href=https://www.dancingwithcrm.com/top-things-from-2023-release-1/>blogs</a> on this topic.</p><h2>Solution context in Maker Portal</h2><p><img alt=Containers src=https://www.dancingwithcrm.com/assets/img/2023-07-19-container.jpg></p><p>If you ever created new things in Dataverse (like Tables, Apps, Web resources etc) directly via Maker Portal, outside of the specific solution, those changes would automatically appear in the so-called <strong>Common Data Services Default Solution</strong> with default publisher prefix. Although experienced creators are aware of this behaviour and are working in predefined solutions, new makers and developers might not know or expect this, which can lead to the necessity to recreate and rebuild some things.</p><p>Luckily we will be able to change that soon with the new ability to select any other unmanaged solution as the active solution context. More importantly, this will set the publisher from that solution as the default one as well (no more random cr6eb20 anymore). This will help to promote easier usage of the Maker Portal, better customization hygiene and create a setup for more successful application lifecycle management.</p><p>Admin will be able to configure this feature in <strong>Public Preview</strong> from <strong>Oct 2023</strong>. There is no <strong>GA</strong> date yet. The original announcement can be found <a href=https://learn.microsoft.com/en-us/power-platform/release-plan/2023wave2/power-apps/makers-set-solution-context-customizations-outside-solution-explorer>here</a>.</p><h2>Clean data with AI</h2><p><img alt=Network src=https://www.dancingwithcrm.com/assets/img/2023-07-19-network.jpg></p><p>Microsoft is truly becoming an AI company and bringing AI everywhere. Microsoft understands the value of good quality data, so this time it brings AI to the Dataverse to help with data cleanup tasks in the form of the smarter data layer and data hygiene suit. So far we don't have a full description of what a "smarter data layer" exactly will be, but according to Microsoft - a smarter data layer will be infused with AI models, to allow the makers to create smarter and context-aware apps with better data quality.</p><p>In regards to the data hygiene suite MS says that it will include semantically smart data validation for email and phone numbers, smart data deduplication rules, and native content AI experiences fully integrated into Dataverse and Power Platform to complete the data-insights-action loop.</p><p>This feature will start with email-type columns to help bring more sophisticated email validation that will include the reason why the data entry is invalid. This will help both users and admins to maintain a higher quality of the data on a proactive basis.</p><p>I hope that in the future this feature will allow us to configure our smart data layer for custom fields with custom validation which will help to improve data quality drastically and resolve so many different scenarios. But unfortunately, at least for now, we don't have any more details about this feature or its future.</p><p>It will become <strong>GA</strong> for all users in <strong>Mar 2024</strong>. The original announcement can be found <a href=https://learn.microsoft.com/en-us/power-platform/release-plan/2023wave2/data-platform/accelerate-value-clean-data>here</a>.</p><h2>Power Pages Copilot</h2><p><img alt=Robot src=https://www.dancingwithcrm.com/assets/img/2023-07-19-robot.jpg> Image by <a href=https://claudeai.wiki/>ClaudeAI.wiki</a></p><p>Another feature - another AI infusion. This time it is for Power Pages. Previously MS brought Copilot to Power Pages to help with Form creation. This time scope is bigger and consists of next things:</p><ul><li>site creation</li><li>Q&A to help build the site</li><li>Copilot actions for end-users</li></ul><h3>Site creation</h3><p>Let's start with site creation. You just need to describe what website you want to create and Copilot will generate it for you - including sitemap, different pages, home page layout and theme. If you don't like the suggestion you could ask Copilot to regenerate them or use it later to refine it.</p><p>As of now, we don't know how specific Copilot would be (ie can we generate a Home page with a specific layout or in the context of the specific company theme), but regardless it will simplify initial website creation drastically and allow for more time spent on refining and making the website look better than long initial creation process. I hope that in the future we will have the ability to create even more with Copilot like specific pages and more.</p><p>This feature will be available for <strong>Public Preview</strong> in <strong>Oct 2023</strong>. The original announcement can be found <a href=https://learn.microsoft.com/en-us/power-platform/release-plan/2023wave2/power-pages/use-copilot-create-website>here</a>.</p><h3>Q&A</h3><p>With this feature you won't need to leave Power Pages Studion anymore to find more information - just ask Copilot. Q&A will summarize the information that is spread across Learn, community, and other key resources (maybe even this website, who knows) to answer makers' questions regarding everything Power Pages to unlock better website creation with less hassle.</p><p>This feature will be available for <strong>Public Preview</strong> in <strong>Oct 2023</strong>. The original announcement can be found <a href=https://learn.microsoft.com/en-us/power-platform/release-plan/2023wave2/power-pages/use-copilot-get-answers-questions>here</a>.</p><h3>Website Copilot Actions</h3><p>Right now you can easily enable the Power Virtual Agents chatbot for your Power Pages site. But this feature takes it to the next level - Website Copilot Actions will allow your Power Pages users to ask the chatbot for help to complete actions like filling simple forms with basic validation in place, finding and getting redirected to resources on the website, and more.</p><p>This will be a huge boost for both end-user experience and site accessibility. Unfortunately, we don't know how easy would be to configure specific actions and scenarios, whether can we use existing chatbots, etc. But this sounds amazingly promising and I cannot wait to try this one out.</p><p>This feature will be available for <strong>Public Preview</strong> in <strong>Nov 2023</strong>. The original announcement can be found <a href=https://learn.microsoft.com/en-us/power-platform/release-plan/2023wave2/power-pages/enable-website-copilot-actions-userss>here</a>.</p><h2>Low-code plugins</h2><p>As somebody said when Power FX was first introduced - it will be everywhere. And seems like that prediction is becoming a reality with the introduction of low-code plugins. You will be able to create a real-time server-side plugin using Power FX. They will have almost all sets of capabilities of the Power FX from the canvas app (although not all) and will behave similarly to the Dataverse Custom Actions and APIs.</p><p>This doesn't mean that you need to abandon your good old C# plugins and rewrite your current logic with Power FX. Low-code plugins have a particular use-case scenario in mind - to shift complex logic and calculation from canvas apps to the server. It is not a secret, that code for canvas apps can grow out of proportion and need a lot of hacks and tricks to ensure stable performance. Moving complex logic from the app to the server will lead to a boost in performance and a better user experience.</p><p>Low-code plugins will be available in canvas apps for <strong>Public Preview</strong> in <strong>Sep 2023</strong> and should go <strong>GA</strong> in <strong>Dec 2023</strong>. The original announcement can be found <a href=https://learn.microsoft.com/en-us/power-platform/release-plan/2023wave2/data-platform/build-canvas-app-low-code-plugins-better-experiences>here</a> and <a href=https://learn.microsoft.com/en-us/power-platform/release-plan/2023wave2/data-platform/build-own-business-logic-using-dataverse-low-code-plugins>here</a>.</p><h2>Bootstrap 5</h2><p><img alt="Bootstrap 5" src=https://www.dancingwithcrm.com/assets/img/2023-01-25-bootstrap5.png></p><p>Bootstrap 5 for Power Pages is the most requested feature from the community. Originally announced as part of 2023 Release Wave 1 it shifted to this Release Wave. But hopefully this time it will stay.</p><p>Update to the latest version includes a bunch of benefits (like security fixes and UI improvements) but also allows a lot of new front-end developers to start with Power Pages more easily (as they might be more familiar with the most recent version of Bootstrap).</p><p>You will be able to migrate your existing Power Pages site to V5 using the PAC CLI tool (available from version 1.23 but you still need a backend update as well). The tool will create a local copy of your site with changes to V5. Keep in mind that it will only convert standard Bootstrap classes so if you created some custom things you would need to update them manually.</p><p>For some time Power Pages will support both V3 and V5, but eventually, everyone will need to migrate. For now, we don't know what will be the deprecation date (but knowing Microsoft it will be somewhere close to the end of 2024 or even 2025).</p><p>This feature will be available for <strong>Public Preview</strong> in <strong>Jul 2023</strong> and <strong>GA</strong> in <strong>q</strong>. The original announcement can be found <a href=https://learn.microsoft.com/en-us/power-platform/release-plan/2023wave2/power-pages/enable-website-copilot-actions-userss>here</a>.</p><h2>Conclusion</h2><p>In this post, I highlighted the features that I think are the most interesting in the upcoming release. AI is changing how we can interact and create things in the Power Platform and it is exciting - cannot wait to try these features out. But other awesome things are waiting for you in the documentation, so check it <a href=https://learn.microsoft.com/en-us/power-platform/release-plan/2023wave2/>out</a>.</p>]]></content:encoded>
            <author>oleksandr@dancingwithcrm.com (Oleksandr Olashyn)</author>
            <enclosure url="https://www.dancingwithcrm.com/assets/img/2023-07-19-cover.jpg" length="0" type="image/jpg"/>
        <media:content url="https://www.dancingwithcrm.com/assets/img/2023-07-19-cover.jpg" medium="image"/>
        </item>
        <item>
            <title><![CDATA[Top 8 things from Power Platform 2023 release wave 1]]></title>
            <link>https://www.dancingwithcrm.com/top-things-from-2023-release-1/</link>
            <guid isPermaLink="false">https://www.dancingwithcrm.com/top-things-from-2023-release-1/</guid>
            <pubDate>Wed, 25 Jan 2023 02:45:20 GMT</pubDate>
            <description><![CDATA[Learn about top 8 things I think are the most interesting in Power Platform 2023 release wave 1]]></description>
            <content:encoded><![CDATA[<p>Today Microsoft published their Power Platform and Dynamics 365 2023 release wave 1 notes (see <a href=https://learn.microsoft.com/en-us/power-platform/release-plan/2023wave1/>here</a> for Power Platform and <a href=https://learn.microsoft.com/en-us/dynamics365/release-plan/2023wave1/>here</a> for Dynamics 365) and let me tell you - it has a lot of awesome things in stock (especially for Power Pages folks). Below you can find my top things from this release in no particular order. If you are interested in older release notes check my previous <a href=https://www.dancingwithcrm.com/top-things-from-2022-release-wave-1/>blogs</a> on this topic.</p><h2>Automated testing for model-driven apps</h2><p>Running tests is important for any large-scale app development. They ensure the reliability and stability of your code. However, testing your UI was always a more problematic solution, especially if you do not have a dedicated team or specific tools. To tackle this some time ago Microsoft introduced Test Studio which allowed the creation of multiple testing scenarios for Canvas apps using Power FX. And now we finally will have the same capability for model-driven apps and custom pages. Utilizing the power of Playwright enhance Test Engine should allow for faster, more complex and robust testing.</p><p>Elevating this to the next level is the ability to run tests as part of your CI/CD pipeline. PAC CLI, Azure DevOps and GitHub Action will support running an updated Test Engine.</p><p>Both of these features will be available in <strong>Public Preview</strong> in <strong>May 2023</strong>. There is no <strong>GA</strong> date for now. The original announcement for the test studio can be found <a href=https://learn.microsoft.com/en-us/power-platform/release-plan/2023wave1/power-apps/run-automated-tests-custom-pages-model-driven-apps>here</a> and for CICD pipeline support <a href=https://learn.microsoft.com/en-us/power-platform/release-plan/2023wave1/power-apps/test-power-apps-cicd-pipelines>here</a>.</p><h2>Create virtual tables easily in Maker Portal</h2><p><img alt="Virtual tables" src=https://www.dancingwithcrm.com/assets/img/2023-01-25-virtual-table.jpg></p><p>The virtual tables are part of the Power Platform for quite a while. However, from the beginning virtual tables had a lot of restrictions and were mostly accessible by developers only. Gradually more and more features were released making virtual tables a more valuable and effective component. And now one big step to make them more accessible has been made - makers can create a virtual table through the maker portal UI. A guided, step-by-step process will allow users to create a virtual table just in a few clicks. The new editor will also allow to create relationships between virtual and regular Dataverse tables.</p><p>This feature will be available for <strong>Public Preview</strong> in <strong>Jan 2023</strong> and should be <strong>GA</strong> in <strong>Jun 2023</strong>. The original announcement for the test studio can be found <a href=https://learn.microsoft.com/en-us/power-platform/release-plan/2023wave1/power-apps/create-virtual-tables-easily-maker-portal>here</a>.</p><h2>Refreshed model-driven and canvas apps UI</h2><p><img alt="Fluent UI" src=https://www.dancingwithcrm.com/assets/img/2023-01-25-fluent-ui.jpg></p><p>Model-driven and canvas apps will receive a UI update to align them with the latest changes in the Microsoft Fluent UI design system. This will include controls, fonts, colours, icons and more. For both model-driven and canvas, it will be an opt-in feature, so if for some business or other needs you need to keep your old design there is nothing to worry about. Changes will be gradual and things will be updated as MS will introduce new updates to the Fluent UI. This should make the design more modern and up-to-date overall.</p><p>For model-driven apps, <strong>Public Preview</strong> will start in <strong>Jan 2023</strong> with <strong>GA</strong> in <strong>Apr 2023</strong>. For canvas apps, <strong>Public Preview</strong> will start in <strong>Feb 2023</strong> with <strong>GA</strong> in <strong>Jun 2023</strong>. The original announcement for model-driven can be found <a href=https://learn.microsoft.com/en-us/power-platform/release-plan/2023wave1/power-apps/enjoy-new-look-feel-model-driven-power-apps>here</a> and for canvas <a href=https://learn.microsoft.com/en-us/power-platform/release-plan/2023wave1/power-apps/build-canvas-apps-using-new-refreshed-controls>here</a>.</p><h2>Cloud flow in Power Pages</h2><p><img alt="Cloud flow in Power Pages" src=https://www.dancingwithcrm.com/assets/img/2023-01-25-cloud-flow.png></p><p>Calling cloud flow from Power Pages was possible for quite some time, however not in easy to use and manage fashion. You needed to create HTTP-triggered cloud flow, maintain different URLs between environments, and think about the security of your calls. All of that made the task of using cloud flow in Power Pages unnecessarily complex. With this release, this no longer will be the case - welcome native cloud flow in Power Pages.</p><p>A new connector will be available in Power Automate that will allow to create a cloud flow that can be easily and securely invoked from Power Pages. This connector will also support bi-directional data exchange which in many scenarios is a necessity for Power Pages.</p><p>Right now we don't have details about what endpoint we will need to use to invoke cloud flow or how exactly we will manage security, but I am excited about this feature as it will allow many citizen developers to create secure and custom endpoint enhancing Power Pages capabilities even more.</p><p>This feature will be available for <strong>Public Preview</strong> in <strong>Jun 2023</strong>. No <strong>GA</strong> date yet. The original announcement for this feature can be found <a href=https://learn.microsoft.com/en-us/power-platform/release-plan/2023wave1/power-pages/use-cloud-flows-power-pages-sites>here</a>.</p><h2>Bootstrap 5</h2><p><img alt="Bootstrap 5" src=https://www.dancingwithcrm.com/assets/img/2023-01-25-bootstrap5.png></p><p>Power Pages uses Bootstrap 3 as its CSS framework of choice. And let me tell you - upgrading from Boostrap 3 to a newer version was the number 1 request from the community for a long time. And finally, it is happening - Power Pages will start using Bootstrap 5 OOTB. Hooray!</p><p>Update to the latest version includes a bunch of benefits (like security fixes and UI improvements) but also allows a lot of new front-end developers to start with Power Pages more easily (as they might be more familiar with the most recent version of Bootstrap).</p><p>Power Pages will also provide you with a migration tool to upgrade your code to the latest version. The tool will provide reports on what was changed, so you can control and update code if necessary.</p><p>This feature will be available for <strong>Public Preview</strong> in <strong>Jun 2023</strong>. No <strong>GA</strong> date yet. The original announcement for this feature can be found <a href=https://learn.microsoft.com/en-us/power-platform/release-plan/2023wave1/power-pages/use-bootstrap-version-5-power-pages>here</a>.</p><h2>ALM Power Pages using solutions</h2><p>ALM in Power Pages was always a tricky thing - from the get-go, you needed to separate your solution with Dateverse tables, plugins etc, and your portal configuration (web pages, web files, etc). With the introduction of PAC CLI support for Power Pages things became easier, but still, data lived separately. Well, all of this is about to change.</p><p>Introducing support for Power Pages in solutions. Finally, they will start to feel like a full part of the Power Platform. At this time we don't know how much we will actually include in the solution (only the website record or the whole configuration) or how PAC CLI will work with it. However, I am excited to see how much this will simplify ALM and help new developers and makers get started with Power Pages ALM more easily in a more familiar manner.</p><p>This feature will be available for <strong>Public Preview</strong> in <strong>Apr 2023</strong>. No <strong>GA</strong> date yet. The original announcement for this feature can be found <a href=https://learn.microsoft.com/en-us/power-platform/release-plan/2023wave1/power-pages/manage-power-pages-alm-using-solutions>here</a>.</p><h2>Dependent assemblies in plugins</h2><p>ILMerge no more! The ability to use external assemblies in your plugins became available in Public Preview back in Mar 2022. This was a big breakthrough as using an unsupported and hack-y way of ILMerge was considered not the best, but unfortunately, in many cases your only choice when you needed some external assemblies in your code. During the last year we patiently waited for the GA - and now we have it. Dependent assemblies should be <strong>GA</strong> in <strong>Apr 2023</strong>.</p><p>The original announcement for this feature can be found <a href=https://learn.microsoft.com/en-us/power-platform/release-plan/2023wave1/data-platform/include-dependent-assemblies-plug-ins>here</a>.</p><h2>Data ingestion optimizations</h2><p><img alt="Data ingestion" src=https://www.dancingwithcrm.com/assets/img/2023-01-25-data.jpg></p><p>There is plenty of scenarios when you need to perform various bulk operations. And as a developer, you are always in search of how you can make those operations as performant as you can to smooth your user experience. Well, now we have new messages just for this purpose: CreateMultiple, UpdateMultiple and UpsertMultiple.</p><p>There are 3 key improvements:</p><ul><li>these messages apply the change to the database for all records at the same time, not sequentially;</li><li>common internal operations will be performed once for each request rather than for each row;</li><li>you will be able to apply custom business logic in the plugin that will be invoked just once for a request and applied to all records in it.</li></ul><p>These messages will not replace existing bulk methods like <em>$batch</em> or <em>ExecuteMultiple</em>. New messages cannot continue on error as operations are performed simultaneously not per row bases. Because of that, you should use new messages when you are sure that bulk operation will not fail and you need to optimize the performance.</p><p>Create and Update messages aren't deprecated, however, Microsoft recommends switching to CreateMultiple and UpdateMultiple messages respectively and updating your plugin code if additional performance is needed for certain tables.</p><p>Overall, this will spark a huge shift in plugin development and I am curious to see how this will go.</p><p>You might notice that there is no DeleteMultiple. This is because the Delete operation often involved cascading operations which cannot be properly evaluated and acted upon in the limited period of execution.</p><p>This feature will be <strong>GA</strong> in May <strong>2023</strong>. The original announcement for this feature can be found <a href=https://learn.microsoft.com/en-us/power-platform/release-plan/2023wave1/data-platform/optimize-data-ingestion-performance-createmultiple-updatemultiple-apis>here</a>.</p><h2>Conclusion</h2><p>In this post, I highlighted the features that I think are most interesting in the upcoming release. I am excited as there are many features that were requested for a long time and a lot of overall improvements. But there are much more changes waiting for you in the documentation, so check it <a href=https://learn.microsoft.com/en-us/power-platform/release-plan/2023wave1/>out</a>.</p><p>Screenshots of new functionality are taken from Microsoft release wave documentation.</p>]]></content:encoded>
            <author>oleksandr@dancingwithcrm.com (Oleksandr Olashyn)</author>
            <enclosure url="https://www.dancingwithcrm.com/assets/img/2023-01-24-cover.jpg" length="0" type="image/jpg"/>
        <media:content url="https://www.dancingwithcrm.com/assets/img/2023-01-24-cover.jpg" medium="image"/>
        </item>
        <item>
            <title><![CDATA[(Quick Tip) Hiding fields by default on Power Pages - CSS way]]></title>
            <link>https://www.dancingwithcrm.com/hide-fields-with-css/</link>
            <guid isPermaLink="false">https://www.dancingwithcrm.com/hide-fields-with-css/</guid>
            <pubDate>Tue, 01 Nov 2022 02:05:20 GMT</pubDate>
            <description><![CDATA[Learn a simple trick on how to hide fields by default in Power Pages with CSS]]></description>
            <content:encoded><![CDATA[<p>One of the most common things you need to do as the Power Pages developer is conditionally hiding/showing fields/sections etc. This often includes hiding fields by default. And there is a quite straightforward way to do that - by using javascript (check <a href=https://oliverrodrigues365.com/2020/07/19/power-apps-portals-javascript-tip-01-hide-show-elements/>this article</a> from Oliver Rodrigues to find out how to do that). However, with fields hidden by default, this can lead to a blinking page problem when, for a brief moment, your fields are visible and only then are hidden by the code. Blinking can be especially harsh on slow networks or forms with lots of hidden fields that are scattered across regular ones.</p><h2>CSS to the rescue</h2><p>So how we can hide fields by default without using JS? Using CSS may sound obvious, until we take a look at how the fields are rendered on Power Pages. Let's say we have a couple of fields (Issue Type, Issue Severity and Feature Priority) that should be hidden by default and later conditionally shown.</p><p><img alt="Initial Form" src=https://www.dancingwithcrm.com/assets/img/2022-10-31-initial-form.jpg></p><p>Let's take a closer look at the Issue Type (crfbd_issuetype). This is a simple text field. Usually, websites render text fields like label/input combo. But in Power Pages it looks a bit different (simplified HTML):</p><pre><code class=language-html>&lt;tr>
&lt;td>
    &lt;div class="info">
        &lt;label for="crfbd_issuetype" id="crfbd_issuetype_label" class="field-label">Issue Type&lt;/label>
    &lt;/div>
    &lt;div class="control">
        &lt;input type="text" maxlength="100" id="crfbd_issuetype" class="text form-control " >
    &lt;/div>
&lt;/td>
&lt;/tr>
</code></pre><p>As you can see label and input itself are in different containers. We can hide them by targeting input and label independently, but we would have an empty tr which wouldn't look nice on the form. So ideally we would need to somehow target their common parent. But there is no parent selector in CSS. Or is it?</p><p>Welcome <strong>:has</strong>. This selector allows you to target the parent or relative sibling of something. This opens the door for a wide variety of interesting styling options, including hiding the parent when some conditions satisfied for the child. Learn more about <strong>:has</strong> pseudo-class <a href=https://developer.mozilla.org/en-US/docs/Web/CSS/:has>here</a>.</p><p>In our case, CSS would look simple - tr as the tag name and id as a selector:</p><pre><code class=language-css>tr:has(#crfbd_issuetype) {
    display: none;
}
</code></pre><p>And that's it - now your fields are hidden by default, no JS necessary. Below you can find a small demo:</p><p>Additional bonus - they are also hidden in the Power Page editor.</p><p><img alt="Form in editor" src=https://www.dancingwithcrm.com/assets/img/2022-10-31-form-in-editor.jpg></p><h2>Caveat</h2><p>But this approach has a caveat. And in this case, it is a support across browsers. According to <a href=https://caniuse.com/css-has>caniuse</a> - Firefox and some mobile browsers (like Opera Mini or Samsung Browser) don't support this pseudo-class. So to make sure it will work always I would advise using both CSS and JS - if the browser doesn't support :has JS will do the trick.</p><p><img alt=Caniuseit src=https://www.dancingwithcrm.com/assets/img/2022-10-31-caniuse.jpg></p><h2>Conclusion</h2><p>Now we have a clear way to hide fields by default using some CSS magic. And we can still use JS to ensure that it will work across the browsers.</p>]]></content:encoded>
            <author>oleksandr@dancingwithcrm.com (Oleksandr Olashyn)</author>
            <enclosure url="https://www.dancingwithcrm.com/assets/img/2022-10-31-cover.jpg" length="0" type="image/jpg"/>
        <media:content url="https://www.dancingwithcrm.com/assets/img/2022-10-31-cover.jpg" medium="image"/>
        </item>
        <item>
            <title><![CDATA[3 new things from Power Pages that you might have missed]]></title>
            <link>https://www.dancingwithcrm.com/3-new-things-from-power-pages/</link>
            <guid isPermaLink="false">https://www.dancingwithcrm.com/3-new-things-from-power-pages/</guid>
            <pubDate>Tue, 18 Oct 2022 02:05:20 GMT</pubDate>
            <description><![CDATA[Learn about 3 new things in Power Pages that you might have missed]]></description>
            <content:encoded><![CDATA[<p>So it happened - Power Apps Portal no more! Long live Power Pages! With the official announcement of Power Pages GA, there is a lot of interesting new stuff - seamless transition from Portals to Pages, new templates, better editor (with support for ~~Advance~~ Multistep forms), refined licensing (Pay-as-you-go is now a thing), security and performance improvements, etc. There are so many new things and you can learn more about them in <a href=https://powerpages.microsoft.com/en-us/blog/microsoft-power-pages-is-now-generally-available/>the official post</a> from MS or in <a href=https://readyxrm.blog/2022/10/12/power-apps-portals-is-now-power-pages/>this awesome article</a> from Nick Doelmann.</p><p>However, some things are getting lost in this big announcement. That's why I decided to write about 3 new, awesome and maybe a bit overlooked features:</p><ul><li>Content Delivery Network or CDN</li><li>Web Application Firewall</li><li>Content Security Policy</li></ul><h2>CDN</h2><p><img alt="CDN image" src=https://www.dancingwithcrm.com/assets/img/2022-10-17-cdn.jpg></p><p>So what is a CDN? Well in short a content delivery network is just a group of servers that are located in different geographical locations that deliver Internet content (CSS and js files, images and videos, and other static content) to a user faster by serving a cached version of the content from locations that are physically closer to the user, which reduces latency and improves overall website performance.</p><p>CDN is a really popular way to improve your website performance. Here are some statistics about CDN usage:</p><ul><li>A CDN can also decrease website latency by an average of 83%.</li><li>Using a CDN can take a load of 60% or more off your origin server.</li><li>Using CDN can boost a website's SEO score.</li></ul><p>Having said that, enabling CDN might not boost performance significantly for every website. For example, CDN for Power Pages doesn't serve files that are under the pages that are protected by Web Page Access Control rules for security reasons. This means if you have a website with a lot of private content your performance might not be boosted as much as a website that has a lot of public-facing images, videos etc.</p><p>Overall, I would say you should definitely enable CDN for Power Pages as any performance improvement and latency reduction is a positive thing for your user. Plus you don't need to pay for this feature, which is a great competitive advantage over other similar services.</p><p>You can find more about how to enable CDN in the official documentation <a href=https://learn.microsoft.com/en-us/power-apps/maker/portals/configure/configure-cdn>here</a>.</p><h2>Web Application Firewall</h2><p><img alt="Web Application Firewall" src=https://www.dancingwithcrm.com/assets/img/2022-10-17-wall.jpg></p><p>The next awesome feature for Power Pages is the Web Application Firewall. In short, WAF help to protect your website by monitoring and filtering HTTP traffic between your website and the Internet. You can imagine it as a set of rules that find and blocks common malicious attacks.</p><p>Web Application Firewall for Power Pages is powered by the same technology as a CDN for Power Pages - by Azure Front Door - which in itself is a Microsoft CDN system.</p><blockquote><p><strong>Important</strong></p><p>To be able to use WAF you need to enable CDN first.</p></blockquote><p>Currently, WAF for Power Pages protects you against the next type of attacks:</p><ul><li>Cross-site scripting</li><li>Local file inclusion</li><li>Remote file inclusion</li><li>Session fixation</li><li>Protocol attackers</li><li>Protocol enforcement</li></ul><p>Unfortunately, as of now, you cannot configure the set of rules that are available for WAF, but even the initial set is covering the most common scenarios.</p><p>Overall, it is an awesome addition, that allows users to improve the security of the Power Pages without the need to understand how exactly WAF works and reduce implementation overhead. And the fact that you can enable this in one click is just the cherry on top. You can find more about how to enable WAF in the official documentation <a href=https://learn.microsoft.com/en-us/power-pages/security/web-application-firewall>here</a>.</p><p>However, if you do need to have more extended security and CDN capabilities you can always configure the core technology itself - Azure Front Door and connect it to your Power Page. To find out how to do that check <a href=https://learn.microsoft.com/en-us/power-apps/maker/portals/azure-front-door>this official documentation</a>.</p><h2>Content Security Policy</h2><p><img alt="Content Security Policy" src=https://www.dancingwithcrm.com/assets/img/2022-10-17-key.jpg></p><p>Last in this article, but certainly not on the list overall is Content Security Policy or CSP. It is another great addition to the Power Pages security.</p><p>Content Security Policy allows you to specify a set of directives or rules, that help to control which resources (ie script files, CSS files etc) your website can load and from which sources. This helps to mitigate some common attacks like the distribution of malware or data theft.</p><p>If you were working with Power Apps Portal for a while you might say that CSP was part of the platform for a while. Indeed, now, the same as before, you need to configure HTTP/Content-Security-Policy site setting to make it work. However, previously this setting was hidden inside the documentation, often obscured and unexplained for regular users. Now, however, we finally have a clear reference on how and why you should use it in the platform.</p><p>You can find more about how to enable CSP in the official documentation <a href=https://learn.microsoft.com/en-us/power-apps/maker/portals/configure/manage-content-security-policy>here</a>.</p><h2>Conclusion</h2><p>Power Pages is an awesome evolution of an existing product that brings a lot of new and awesome features to the table. I really like, that with the platform growth it offers more in terms of security and performance, which will help it to stand up against the competition.</p>]]></content:encoded>
            <author>oleksandr@dancingwithcrm.com (Oleksandr Olashyn)</author>
            <enclosure url="https://www.dancingwithcrm.com/assets/img/2022-07-05-cover.jpg" length="0" type="image/jpg"/>
        <media:content url="https://www.dancingwithcrm.com/assets/img/2022-07-05-cover.jpg" medium="image"/>
        </item>
        <item>
            <title><![CDATA[Partial portal download with Portal CLI]]></title>
            <link>https://www.dancingwithcrm.com/portal-helper-v1-3/</link>
            <guid isPermaLink="false">https://www.dancingwithcrm.com/portal-helper-v1-3/</guid>
            <pubDate>Mon, 22 Aug 2022 02:31:20 GMT</pubDate>
            <description><![CDATA[Learn how to download only portal entities that you need with new PAC CLI update]]></description>
            <content:encoded><![CDATA[<p>Recently, Microsoft announced the release of the new update for their Power Platform CLI, which included a lot of interesting improvements and additions. And there was a quiet one for portals as well - new parameters for download command.</p><h2>Download command</h2><p>Before this release, when you downloaded the portal it was always all of it with all tables and files. Generally, this is not a huge problem and loading time is in the range of 30+ seconds. However, I saw projects, especially with older portals, with huge amounts of web files and pages where loading times were outrageous (the worst one downloaded portal in 5 min). And there was no way to solve this. But not anymore. Welcome release 1.17.5 and new optional parameters for download command - <strong>includeEntities</strong> and <strong>excludeEntities</strong>.</p><p><img alt="Download Command with new parameters" src=https://www.dancingwithcrm.com/assets/img/2022-08-21-new-download-parameters.jpg></p><p>For both of these parameters, you need to specify a list of comma-separated entity logical names. Include entities will define which entities the portal should download and will skip all others. Exclude entities will define which entities to omit during the download.</p><p>The example command will look something like this:</p><pre><code class=language-javascript>pac paportal download -p "c:\demo" -id guid -ie "adx_entityform,adx_webpage,adx_webtemplate"
</code></pre><p>The addition of those parameters makes using CLI much more effective, especially in edge case scenarios where portals are just too big to deal with them effectively. Also, if working in branches, it opens the door for the possibility of just loading entities that you will be working on (like content snippets, templates etc).</p><p>But what if you don't want to memorize the entity's logical name? Or don't want to mistype them? Well, the new update for my Portal VS Code extension might help you with that.</p><h2>Portal helper to the rescue</h2><p><a href="https://marketplace.visualstudio.com/items?itemName=oleksandr-olashyn.portal-helper-vscode">Portal Helper</a> is an extension that I developed to help users to work with Portal CLI more efficiently. With this update, I am adding two new things.</p><p>Firstly, the Download command now supports both include and exclude entity parameters. More importantly, instead of the free text option, it is a selection of all available entities, by their friendly name, so you don't need to remember logical names and can just select them from the list.</p><p>Secondly, my Download Latest command, which downloads the portal from the current folder, now supports setting include and exclude parameters in the VS Code settings. You can do this both on a global level or on a workspace level (meaning you can have different configurations for different portals).</p><p>To set those setting navigate to Settings and type Portal Helper. You will see two options available to you.</p><p><img alt="VS Code settings for Portal Helper" src=https://www.dancingwithcrm.com/assets/img/2022-08-21-portal-helper-vscode-settings.jpg></p><h2>Conclusion</h2><p>I am really happy that MS continues to develop the portal part of MS Power Platform CLI allowing us to do much more. With each release, it becomes easier and more convenient to work inside VS Code. And I will continue to improve my extension to make VS Code experience even better.</p>]]></content:encoded>
            <author>oleksandr@dancingwithcrm.com (Oleksandr Olashyn)</author>
            <enclosure url="https://www.dancingwithcrm.com/assets/img/2022-08-20-cover.jpg" length="0" type="image/jpg"/>
        <media:content url="https://www.dancingwithcrm.com/assets/img/2022-08-20-cover.jpg" medium="image"/>
        </item>
        <item>
            <title><![CDATA[Web Form Steps Visualizer v1]]></title>
            <link>https://www.dancingwithcrm.com/webformstepsvisualizer-v1/</link>
            <guid isPermaLink="false">https://www.dancingwithcrm.com/webformstepsvisualizer-v1/</guid>
            <pubDate>Mon, 15 Aug 2022 04:05:20 GMT</pubDate>
            <description><![CDATA[Make it easier to work with Web Forms Steps by visualizing them Web Form Steps as a flow chart with my updated and improved Web Form Steps Visualizer PCF Component.]]></description>
            <content:encoded><![CDATA[<p>A long time ago I created a PCF component called Web Form Steps Visualizer (see <a href=https://www.dancingwithcrm.com/web-form-steps-visualizer/>original post</a>). Its main purpose was to help you to visualize your web form steps as a flow chart. It provided very basic functionality, but did get the job done. As it was in early development stage I marked that version 0.0.2. I had a lot of things planned for it, but limitation of the library that I choose for a flow chart plus general focus on other projects drove me away from it. Not so long ago, Microsoft added new and awesome PCF control type - virtual. I decided to try it out and to rebuild Web Form Steps Visualizer from ground up to add all necessary features. So welcome new and improved Web Form Steps Visualizer version 1.0.0.0!</p><h2>Version 1.0.0.0</h2><p>The main purpose of this control is to simpify your work with advance (web) form steps, by allowing you to visualize their order and connections, update it, create new steps, update and delete existing.</p><blockquote><p><strong>Important</strong></p><p>This control will not replace regular experience. You still need to open web form steps to add metadata, specify forms that should be used, etc. Control just simplifies general management of steps.</p></blockquote><p>With Web Form Steps Visualizer you can perform next operations:</p><ul><li>Create new steps</li><li>Update existing steps</li><li>Delete steps</li><li>Open step in a new tab</li><li>Delete connection between step - this action clears lookup of target step to remove reference of the next step (or default next step for conditional step). To perform this you need to click on connection line (it will turn blue), then click Backspace on your keyboard, and then click Save changes button.</li></ul><p>You can find demos of each action below.</p><p>You can find and download the latest version from <a href=https://github.com/OOlashyn/PCF-WebFormStepVisualizer/releases>Release page</a> of my Github repo.</p><h2>Demo</h2><h3>Create new step</h3><h3>Update existing step</h3><h3>Update connection</h3><h3>Delete step</h3>]]></content:encoded>
            <author>oleksandr@dancingwithcrm.com (Oleksandr Olashyn)</author>
            <enclosure url="https://www.dancingwithcrm.com/assets/img/2020-06-28-flow-chart.jpg" length="0" type="image/jpg"/>
        <media:content url="https://www.dancingwithcrm.com/assets/img/2020-06-28-flow-chart.jpg" medium="image"/>
        </item>
    </channel>
</rss>