ACDC 2024 – Why this is the ultimate hackathon

It’s almost been 3 weeks since the hackathon and I am a bit late on my summary of experiences. February has been hectic, but better late than never 🙂

I have written about ACDC earlier and they all have a bit of a different perspective each time that I participate:
ACDC 2021 – Summary
ACDC 2022 – Summary
ACDC 2023 – Summary

This hackathon provides the most unique learning experience for Microsoft techies in every single way. 13 teams locked into a giant auditorium for 3 days having a blast learning things they didn’t know before! Sure there are other hackathons with many teams, but they are not forced to eat, sleep, breathe together for 3 days straight 😂

Being humbled is a valuable learning experience

I have about 16 years of experience in this world of Dynamics / Power Apps, and have to admit that I am extremely comfortable in the space of Dynamics 365. Like many other people in my situation, it’s easy to stay inside my safe area, and let others handle technology that is not so well known. This also defeats the purpose of the hackathon.

Long story short, Nick Doelman contacted me this year asking if he could be a part of Point Taken’s team, and I was instantly intrigued about it. I know him as an MVP, but we haven’t hung out a lot before. Nick is one of the best you can get on Power Pages, and is generally a very high end consultant within Dynamics / Power Platform. It was given that he would provide the team with a lot of important knowledge, so we agreed to give it a go. Ohhhh what a joyful experience it was! 🥰

Nick’s first demand was:

No one is going to do what they normally do in their everyday work!

Nick Doelman

Probably a bit dramatic quoting him like that, but what’s not to love with a little theatrics😂 The main reason for him to say this was because we are here to learn. If we end up solving Business issues like we do every single day, we could just as well stay at our customers and billed hours. He literally pushed us out of our comfort zone even before we had started. We all agreed that it would be fun, but none of us at this point were sure what to expect.

Nick took the lead on the app that would be the main point of our delivery. In short, it was a Canvas app (😂”low code”😂 ) that would use OpenAI to randomly generate maps for a game. Not only was the game randomized, but the themes would also be random based on keywords entered at the beginning of the setup. Read more about our app below⬇️

What impressed me the most was seeing how Nick embraced the new technology that he hadn’t worked a lot with yet, while mastering highly advanced elements in such a short time. I do often mention the quote of the “Old dog new tricks”, but it’s a real feeling. Having the energy to constantly learn new technology is not easy.

While sharing his knowledge with the team, he also showed us some really important skills as a consultant. His logical approach to solve a problem, and his ability to talk about what was going to be solved. A good sales pitch itself can land large scale projects even before the customer has seen any evidence that it works. This could also have been on of our problems as a team, because the first day pitch was so amazing that we almost couldn’t deliver on our promise in the end😂

I consider myself a quick-thinking consultant with a charm that usually helps me win projects. However, Nick’s many years of experience not only nailed the pitch but also showcased how to be a top-notch consultant in all aspects during a challenging 3-day hackathon. I’m not trying to be Nick – I’m Thomas. I can take his wisdom and make it work with my own style, learning and growing in the process.

That is the core value of ACDC https://arcticclouddeveloperchallenge.net

Final Delivery

Interested in seeing what we did, have a look at our blog post

Teams Meetings Corrupted in Dynamics 365 Client Sync

Recently faced an issue where Microsoft Team meetings were corrupted when synchronized with Dynamics 365 via App for Outlook.

Before Tracking

Look at the Teams Meeting link. Everything seems ok before tracking

After Tracking

The tracking link got corrupted for some strange reason.

Solution

I had to contact support to better understand the problem, and the solution was really strange. They made me download the OrgDBSettings tool (a common tool to do certain modifications) in Dataverse/Dynamics. Here they told me to modify the “Appointmentricheditorexperience” value to TRUE. This is actually a value that is default FALSE.

I just can’t make any sense of that, because why on earth do you need that to be false? In every case you would want this to be true if you plan to sync appointments?!?

Anyways….. Just make sure it’s true, and you should be ready to go😊

Organization Settings Editor – in case you need it

Download here:
https://github.com/seanmcne/OrgDbOrgSettings/releases

Import to Dynamics, and open via Classic Mode

Security Roles – The 3 Phases

Security Roles the least fun part about Power Platform – Dataverse / Dynamics 365. I would rather use hours finding the right ICON for every single custom entity😂. Yet security roles happen to be the pilar of why our platform is so much more powerful than other technical platforms.

I find that my approach to security roles almost always ends up the same way when I deliver new solutions. Therefore I thought I would share a bit of what goes through my mind in the different stages:)

1. Creating the role

There are many blogs about creating roles, and recently we even received a new modern way of modifying security roles. I’m not going to cover the hows, but you can pick up a few tips from the following blogs:

Blogs about the new ways of creating roles:
https://learn.microsoft.com/en-us/power-platform/admin/security-roles-privileges
https://malindonosomartnes.com/2023/06/29/new-security-roles-admin/
https://nishantrana.me/2023/05/01/manage-security-roles-using-the-new-modern-ui-preview-power-platform-admin-center/

What I do want to share is my personal opinion on where to start. As you know I mostly work with Dynamics 365 and not pure Power Platform environments. If I am delivering a Sales solution I almost always start off by copying the Salesperson’s security role.

Don’t ever start a security role from scratch. It’s simply not worth the time.

So for Dynamics sales I would typically just copy the Salesperson as a start

And for Power Platform I might start with Basic User as a start.

A typical error I do is hoping that I can end up with a single security role to make things simple. Pushing all users into the same role because why not. I am one for more access to users, and depend on teaching people to handle data with respect. Within the first few weeks, you soon realize that you probably need an admin role for administrative tasks.

PS: One thing I always do is of course to limit any delete actions for ALL users the first few weeks so that they don’t blame the system for GHOST deleting items. Yes, that has happened! 🤷‍♂️

2. Assigning the role

After hours of banging your head on the wall, trying to figure out what security privileges you are missing for the application to work as expected, you arrive at the next task of assigning the security roles to the users.

The next error that I have done more than once is thinking that we can manage the security roles directly on the user. To begin with I use a tool like XrmToolBox to deploy roles to multiple users at the same time, but a few weeks/months down the line people hired/fired/re-org having to make these changes often is going to take way too much time. Having to bill for these changes is also going to be frowned upon because it’s “just a simple task”.

Traditionally this has been very straightforward for the sales and customer service users, but lately, this is becoming more and more of a challenge. With the release of new functions, you will soon understand that keeping up with security roles is almost impossible. With new solutions from Microsoft being installed automatically, you might also see a few security roles to follow along. Examples of this would be the Forecasting functionality to sales.

Normally one would think “Let’s just add these to the main security role for all users” BUT there are times when the actual security role ID/Name is the one opening up the functionality.

The Dynamics 365 app for Outlook User is one of these apps I believe. In order to actually qualify for the app deployment, you need this security role.

So my first initial thought that one user could have “Salesperson” as a single role with all of the functionality is now turning into a nightmare handling pr user. I now have to manage lots and lots of users/changes etc while trying to maintain the correct roles given.
The image below is just an example

3. Structuring the security roles with teams

This leads us to the final phase of security roles for me, where I realize that I should have done things right from the beginning. Using the extra time early on so that I save time when the project is live😤

Just like marketing people are oddly obsessed with personas, we have to think about what types of users do we expect to see in Dynamics / Power Platform. In a simple solution, you could easily have the following personas.

  1. Sales User
  2. Sales Admin
  3. Marketing user

Dynamics users are well aware of teams being able to connect security roles, but Power Platform users are not always aware of this.

Create a team, add users, and then add the security roles. This is a much simpler way of managing changes in security roles because changes only need to happen in 1 place.

Hold up

The problem with this solution is that Dynamics / Power Platform consultant or someone familiar with Dynamics / Power Platform needs to make these changes. In a worst-case scenario, a company would have IT create a user and assign a license. THEN they would have to contact other consultants to add security roles or add someone to the team for correct access. This method is flawed and will at some time become a problem. Microsoft has fortunately given us a great solution for this.

Solution✅ – AAD Security Groups / Entra Identity Security Group

I am not going to cover how to create a security group in AD and later on, add it to Dynamics / Dataverse. Have a look at the following posts for these tasks:

https://learn.microsoft.com/en-us/power-platform/admin/manage-group-teams#create-a-group-team
https://forwardforever.com/how-groups-teams-work-in-power-apps-dataverse/

What I think is so important about the Security Group is that it removes the burden of access from the Dynamics / Power Platform consultant, and puts it in the hands of IT managing general access to all systems in AD. This just makes so much more sense, because these people normally do the on/off-boarding for all technical aspects. All you have to worry about is that security roles are given to the correct team.

Within Dynamics / Dataverse there is no difference in how to assign security roles BUT there is a major difference in having the members auto-populated based on AD security groups.

Moral of the story

Use Security Groups unless you have a very good reason not to 😘

Code is code – You are a developer! 💯

For those of you that follow me on twitter and other media, I recently posted out a few questions to trigger some conversation about code. Specifically it was JavaScript code for a functional consultant. The feedback was extremely varied, and that was my hope. I of course have an opinion on the matter, but didn’t want to voice it before I heard what people in general feel about the subject.

This is only from my twitter, but had a similar thing on the Dynamics group of Facebook. There are not a lot of votes here, but in general it’s fair to conclude that most don’t agree with me in regards to Dynamics 365 JavaScript.

I personally think that functional consultants need to know BASIC JavasScript for Model Driven Apps. Examples of basic to me is the Hide/Show with potentially a few calculations. The obvious reason for this is the user expecting to se changes in the app visually while they are entering data. Having things calculated asynchronous is just not good enough when working in a Model Driven App.

But JavaScript doesn’t run server side. You cant use that!

Someone

True, but sometimes the code doesn’t need to be server side, because we can guarantee that the usage is being done within the app. Other times integrations might directly update data and the scenario might be different. The point I am trying to make, is that you should be able to choose the technology because you know how to do it. Not choose the tech based on what your limitations are. There are way to many implementations that are absolutely chaos taking over because of Business Rules, Calculated Fields or Formula Column bombardment.

But I’m not a developer??!?🤷‍♀️

Well.. Is that really true? I hope you don’t mind me quoting you Steve, but I 100% agree with you on Code is Code. I know you are referring to the idea that this should be done by dev’s, but what would life be if we all agreed on everything? hehe.

I see tons of functional consultants accepting that PowerFX in Canvas Apps is just “Excel formula”. Looking at the example below of an update statement….. How is this not ProCode?

How is this any easier than JavaScript if you just look at syntax for a hide/show?

I understand that these are to absolutely different functions, but the whole idea of accepting PowerFX as a Low-Code language should mean that you also accept JavaScript to be a Low-Code language.

These to languages are so extremely different in logic. My mind is made up of sequences of actions, and that’s why JavaScript makes sense, while PowerFX in Canvas is the result of whatever happened in a prior action(and that fries my brain sometimes).

But JavaScript it’s to hard to learn!

Nothing is harder than learn to do Canvas Apps correctly. I have been doing it on and off for years already, and I still feel like I am a terrible app developer in Canvas. You all know I am from the Dynamics background and wish most things would be in the context of a Model Driven App. I accept that I am not a creative person, and that a button is a good button as long as it works (not because of rounded corners😂).

JavaScript is just something that you have to do a few times in a test environment and you will quickly see that it’s a lot simpler than you actually thought. Most of the stuff being done in JavaScript these days is hide/show’s and simple calculations. The heavy JavaScript’s that we used to see have in many cases been replaced with other tech (Custom Pages, PCF Components etc).

Later this year I will try to release a YouTube course “JavaScript 4 Dummies, by a Dummie”. It will be my take on simple JavaScript, and I will make sure that everyone will understand the basic principles of HowTo JavaScript.

🙃BUT UNTILL THEN🙃
I am actually going to record a series on “Plugin’s for Dummies, by a Dummie”. Wonder if people will accept a Plugin as Low-Code?!???! haha.. Don’t worry.. It’s Pro-Code, but it’s not impossible now that we have GPT to help us out with syntax! More info to come👏❤

Dynamics 365 – Adding products issue

This might be the dumbest blog in a while, but I am adding it in case others have problems with the same issue.

Recently I needed to add new products to the product table of Dynamics, and the buttons for adding the products/families were gone.

I of course had to look at the documentation in case there was something obvious I was missing. The documentation only wanted to ensure that I had a license + security role.

All of this was checking out on my side as I had a license and I was an administrator. Still, I was desperate trying to understand such a stupid issue that I made sure I had enough roles:

Eventually, I got some help from friends, and a huge thank you to Vivian Voss for pointing me in the right direction. First I was told to check the “obvious”.

Are you able to configure products via the old UI?

Vivian Voss

Well, turns out I was.

Solution?

Go to the Default Solution of configuration and make sure that “read only” was not on for the product table in the new UI😂😂🤷‍♂️🤷‍♀️🤷

The only thing that I can think of is an update from Microsoft changing this value for some odd reason. The fact that it is read-only in the Unified Client makes no sense, but hey. Not the first time something like that has happened.. hehe.

Just make sure this is OFF, Save changes, and then publish the table. You should be able to configure products again.

Custom Page – Opportunity Close Solution Download

Initially, I was just going to explore some possibilities with Custom Pages, but decided to make a solution out of it instead. This way you can just download the solution and do the required modifications for your project (GIPHY API key), and you are good to go😁

I am really excited to hear your feedback after seeing/trying this out, and hopefully, I will be able to make it a lot better in next revisions of the Custom Page!

Go to my GitHub to get a hold of the solution. It’s a pretty “thin” solution that should be easy to configure and easy to delete if you no longer need it.

💾 DOWNLOAD 💾

I have also included a video displaying how you install and use the solution

Installation and setup

Because of ribbon changes, I was forced to create a managed solution for this. The only way it wouldn’t overwrite the current opportunity ribbon.

If you want to play around with the solution, download the unmanaged. Just make sure you don’t have any other ribbon customizations on Opportunity. If you do back it up first!!

For everyone else, you have to download the managed solution to be on the safe side.

On my Gihub page I have written what components get installed, so you can easily remove it all at a later point in time.

Custom Page – Teams Integration

Did you think my last post was the final stage of the “Custom Page – Win Notification“? Of course not!! We have one more important step to complete the whole solution.

I am all about salespeople being able to boast about their sales. If you meet a salesperson who doesn’t love to brag about closing, are they even in sales? 😉

Communication is key when you work in organizations of any size and location Onprem/Online/Hybrid. Doesn’t matter where you work, we can all agree that spreading the word around, and making sure everyone gets the latest news is hard. Microsoft is making it pretty obvious that Teams is a preferred channel, so that’s where I also wanted to focus my energy.

Notify the team

I decided to extend the solution a bit to reuse the GIF that we got from the last closing screen, and include it in the Teams integration that I wrote about earlier in the Adaptive Cards.

The first thing I did was to add a new group of fields for the Teams notification. I wanted to let the user choose if they wanted to post the sales to Teams or not. The obvious reason is that some opportunities might have to be reopened, and you don’t want the credit 2 times etc. It’s just a simple logo, text and boolean field.

The next step is adding the variable BoolPostTeams to the run statement of the Power Automate when I click “Confirm Win”.

On the Flow side of things, I will now receive 1 more variable that I can handle. Because I wanted to use the bool type, I have to convert the string “true/false” to a boolean

bool(triggerBody()['Initializevariable2_Value']) 

What team are we updating?

Glad you asked 😉 A part of the solution is to include a Team ID and Channel ID. I added these fields to the Business Unit Table. When we store the Teams/Channel ID’s at this level, we can reuse the same solution for several teams in an organization without having to hard code anything.

These are simple text fields that will hold the unique values to the Team and the Channel you want to notify.

NB! A tip for getting the Teams ID and the Channel ID:
Add the Team and Channel via the drop-down selector (flow step below). Then open the “peek code” to see what the ID’s of the teams are. If you don’t understand this step, just have a look at my video in the final post of the series 🎥

Back to the flow!

We update the get Opportunity with extra linked tables. This way we don’t have to do multiple retrieves, and only get the fields we need from the linked tables.

parentaccountid($select=name), owninguser($select=fullname, internalemailaddress), owningbusinessunit($select=saleswin_teamschannelid,saleswin_teamsid)

After we update the opportunity as closed with our Custom Action “close opportunity”, we check if the close dialog wanted to notify the team.

Then we need to get the @mention tag for the given user that made the sales. Here I am using the email address from the SystemUser table we are getting from the Opportunity extended table.

Finally, we post the Adaptive Card to Teams and behold the wonder of the adaptive card!! 💘

{
    "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
    "type": "AdaptiveCard",
    "version": "1.2",
    "body": [
        {
            "speak": "Sales Morale Boost",
            "type": "ColumnSet",
            "columns": [
                {
                    "type": "Column",
                    "width": 8,
                    "items": [
                        {
                            "type": "TextBlock",
                            "text": "🚨WIN ALERT🚨",
                            "weight": "Bolder",
                            "size": "ExtraLarge",
                            "spacing": "None",
                            "wrap": true,
                            "horizontalAlignment": "Center",
                            "color": "Attention",
                            "fontType": "Default"
                        },
                        {
                            "type": "TextBlock",
                            "text": "@{outputs('GetOpptyInfomation')?['body/name']}",
                            "wrap": true,
                            "size": "Large",
                            "weight": "Bolder",
                            "horizontalAlignment": "Center"
                        },
                        {
                            "type": "ColumnSet",
                            "columns": [
                                {
                                    "type": "Column",
                                    "width": 25,
                                    "items": [
                                        {
                                            "type": "TextBlock",
                                            "text": "🏠 Kunde",
                                            "wrap": true,
                                            "size": "Large",
                                            "weight": "Bolder",
                                            "horizontalAlignment": "Right"
                                        },
                                        {
                                            "type": "TextBlock",
                                            "text": "💲 Verdi",
                                            "wrap": true,
                                            "size": "Large",
                                            "weight": "Bolder",
                                            "horizontalAlignment": "Right"
                                        },
                                        {
                                            "type": "TextBlock",
                                            "text": "👩‍🦲 Selger",
                                            "wrap": true,
                                            "size": "Large",
                                            "weight": "Bolder",
                                            "horizontalAlignment": "Right"
                                        }
                                    ]
                                },
                                {
                                    "type": "Column",
                                    "width": 50,
                                    "items": [
                                        {
                                            "type": "TextBlock",
                                            "text": "@{outputs('GetOpptyInfomation')?['body/parentaccountid/name']}",
                                            "size": "Large",
                                            "weight": "Bolder"
                                        },
                                        {
                                            "type": "TextBlock",
                                            "text": "@{outputs('GetOpptyInfomation')?['body/actualvalue']}",
                                            "weight": "Bolder",
                                            "size": "Large"
                                        },
                                        {
                                            "type": "TextBlock",
                                            "text": "@{outputs('Get_an_@mention_token_for_a_user')?['body/atMention']}",
                                            "weight": "Bolder",
                                            "size": "Large"
                                        }
                                    ]
                                }
                            ]
                        }
                    ]
                }
            ]
        },
        {
            "type": "Image",
            "url": "@{body('Parse_JSON')?['data']?['images']?['original']?['url']}",
            "horizontalAlignment": "Center"
        }
    ]
}

Now, this is what an opportunity close dialog should look like!! 🙏🏆🌟🎉

Custom Page – GIF for the win🏆

Most of you might know by now that I am a huge fan of GIF’s. I have written about GIF’s earlier when trying to motivate sales people, and I thought I would turn it up one notch!

Last post I wrote was about the opportunity close dialog. It was functional, but not exciting.

It is still 100% more feedback friendly than the Microsoft OOTB functionality, but it’s a little boring now that we have the option to be creative. This is why I thought I would add my earlier GIF post together with this to create a solution for instant response to the salespeople.

We all know that there is no better feeling than finding the correct GIF! 😂

So let’s start off by including a flow that I have written earlier with the Custom Page Confirmation dialog to make it all a little better than the thumbs up that my last post was showcasing.

RUN Instant Flow when completing sales

In the last post we covered the Power Automate action when the confirmation button was pressed. Now we reopen it to add a few more steps. The steps are identical to the steps I have linked to above in the article that coveres GIF connector.

First let’s query for a WIN gif.

Go to Giphy and setup a developer account for free. I have written about it in the GIF post that I have on top here. Just replace the api_key value in the string:
https://api.giphy.com/v1/gifs/random?api_key=**********&tag=win

Parse the return of this query (SORRY.. This is a bit long!)

{
    "type": "object",
    "properties": {
        "data": {
            "type": "object",
            "properties": {
                "type": {
                    "type": "string"
                },
                "id": {
                    "type": "string"
                },
                "url": {
                    "type": "string"
                },
                "slug": {
                    "type": "string"
                },
                "bitly_gif_url": {
                    "type": "string"
                },
                "bitly_url": {
                    "type": "string"
                },
                "embed_url": {
                    "type": "string"
                },
                "username": {
                    "type": "string"
                },
                "source": {
                    "type": "string"
                },
                "title": {
                    "type": "string"
                },
                "rating": {
                    "type": "string"
                },
                "content_url": {
                    "type": "string"
                },
                "source_tld": {
                    "type": "string"
                },
                "source_post_url": {
                    "type": "string"
                },
                "is_sticker": {
                    "type": "integer"
                },
                "import_datetime": {
                    "type": "string"
                },
                "trending_datetime": {
                    "type": "string"
                },
                "images": {
                    "type": "object",
                    "properties": {
                        "downsized_large": {
                            "type": "object",
                            "properties": {
                                "height": {
                                    "type": "string"
                                },
                                "size": {
                                    "type": "string"
                                },
                                "url": {
                                    "type": "string"
                                },
                                "width": {
                                    "type": "string"
                                }
                            }
                        },
                        "fixed_height_small_still": {
                            "type": "object",
                            "properties": {
                                "height": {
                                    "type": "string"
                                },
                                "size": {
                                    "type": "string"
                                },
                                "url": {
                                    "type": "string"
                                },
                                "width": {
                                    "type": "string"
                                }
                            }
                        },
                        "original": {
                            "type": "object",
                            "properties": {
                                "frames": {
                                    "type": "string"
                                },
                                "hash": {
                                    "type": "string"
                                },
                                "height": {
                                    "type": "string"
                                },
                                "mp4": {
                                    "type": "string"
                                },
                                "mp4_size": {
                                    "type": "string"
                                },
                                "size": {
                                    "type": "string"
                                },
                                "url": {
                                    "type": "string"
                                },
                                "webp": {
                                    "type": "string"
                                },
                                "webp_size": {
                                    "type": "string"
                                },
                                "width": {
                                    "type": "string"
                                }
                            }
                        },
                        "fixed_height_downsampled": {
                            "type": "object",
                            "properties": {
                                "height": {
                                    "type": "string"
                                },
                                "size": {
                                    "type": "string"
                                },
                                "url": {
                                    "type": "string"
                                },
                                "webp": {
                                    "type": "string"
                                },
                                "webp_size": {
                                    "type": "string"
                                },
                                "width": {
                                    "type": "string"
                                }
                            }
                        },
                        "downsized_still": {
                            "type": "object",
                            "properties": {
                                "height": {
                                    "type": "string"
                                },
                                "size": {
                                    "type": "string"
                                },
                                "url": {
                                    "type": "string"
                                },
                                "width": {
                                    "type": "string"
                                }
                            }
                        },
                        "fixed_height_still": {
                            "type": "object",
                            "properties": {
                                "height": {
                                    "type": "string"
                                },
                                "size": {
                                    "type": "string"
                                },
                                "url": {
                                    "type": "string"
                                },
                                "width": {
                                    "type": "string"
                                }
                            }
                        },
                        "downsized_medium": {
                            "type": "object",
                            "properties": {
                                "height": {
                                    "type": "string"
                                },
                                "size": {
                                    "type": "string"
                                },
                                "url": {
                                    "type": "string"
                                },
                                "width": {
                                    "type": "string"
                                }
                            }
                        },
                        "downsized": {
                            "type": "object",
                            "properties": {
                                "height": {
                                    "type": "string"
                                },
                                "size": {
                                    "type": "string"
                                },
                                "url": {
                                    "type": "string"
                                },
                                "width": {
                                    "type": "string"
                                }
                            }
                        },
                        "preview_webp": {
                            "type": "object",
                            "properties": {
                                "height": {
                                    "type": "string"
                                },
                                "size": {
                                    "type": "string"
                                },
                                "url": {
                                    "type": "string"
                                },
                                "width": {
                                    "type": "string"
                                }
                            }
                        },
                        "original_mp4": {
                            "type": "object",
                            "properties": {
                                "height": {
                                    "type": "string"
                                },
                                "mp4": {
                                    "type": "string"
                                },
                                "mp4_size": {
                                    "type": "string"
                                },
                                "width": {
                                    "type": "string"
                                }
                            }
                        },
                        "fixed_height_small": {
                            "type": "object",
                            "properties": {
                                "height": {
                                    "type": "string"
                                },
                                "mp4": {
                                    "type": "string"
                                },
                                "mp4_size": {
                                    "type": "string"
                                },
                                "size": {
                                    "type": "string"
                                },
                                "url": {
                                    "type": "string"
                                },
                                "webp": {
                                    "type": "string"
                                },
                                "webp_size": {
                                    "type": "string"
                                },
                                "width": {
                                    "type": "string"
                                }
                            }
                        },
                        "fixed_height": {
                            "type": "object",
                            "properties": {
                                "height": {
                                    "type": "string"
                                },
                                "mp4": {
                                    "type": "string"
                                },
                                "mp4_size": {
                                    "type": "string"
                                },
                                "size": {
                                    "type": "string"
                                },
                                "url": {
                                    "type": "string"
                                },
                                "webp": {
                                    "type": "string"
                                },
                                "webp_size": {
                                    "type": "string"
                                },
                                "width": {
                                    "type": "string"
                                }
                            }
                        },
                        "downsized_small": {
                            "type": "object",
                            "properties": {
                                "height": {
                                    "type": "string"
                                },
                                "mp4": {
                                    "type": "string"
                                },
                                "mp4_size": {
                                    "type": "string"
                                },
                                "width": {
                                    "type": "string"
                                }
                            }
                        },
                        "preview": {
                            "type": "object",
                            "properties": {
                                "height": {
                                    "type": "string"
                                },
                                "mp4": {
                                    "type": "string"
                                },
                                "mp4_size": {
                                    "type": "string"
                                },
                                "width": {
                                    "type": "string"
                                }
                            }
                        },
                        "fixed_width_downsampled": {
                            "type": "object",
                            "properties": {
                                "height": {
                                    "type": "string"
                                },
                                "size": {
                                    "type": "string"
                                },
                                "url": {
                                    "type": "string"
                                },
                                "webp": {
                                    "type": "string"
                                },
                                "webp_size": {
                                    "type": "string"
                                },
                                "width": {
                                    "type": "string"
                                }
                            }
                        },
                        "fixed_width_small_still": {
                            "type": "object",
                            "properties": {
                                "height": {
                                    "type": "string"
                                },
                                "size": {
                                    "type": "string"
                                },
                                "url": {
                                    "type": "string"
                                },
                                "width": {
                                    "type": "string"
                                }
                            }
                        },
                        "fixed_width_small": {
                            "type": "object",
                            "properties": {
                                "height": {
                                    "type": "string"
                                },
                                "mp4": {
                                    "type": "string"
                                },
                                "mp4_size": {
                                    "type": "string"
                                },
                                "size": {
                                    "type": "string"
                                },
                                "url": {
                                    "type": "string"
                                },
                                "webp": {
                                    "type": "string"
                                },
                                "webp_size": {
                                    "type": "string"
                                },
                                "width": {
                                    "type": "string"
                                }
                            }
                        },
                        "original_still": {
                            "type": "object",
                            "properties": {
                                "height": {
                                    "type": "string"
                                },
                                "size": {
                                    "type": "string"
                                },
                                "url": {
                                    "type": "string"
                                },
                                "width": {
                                    "type": "string"
                                }
                            }
                        },
                        "fixed_width_still": {
                            "type": "object",
                            "properties": {
                                "height": {
                                    "type": "string"
                                },
                                "size": {
                                    "type": "string"
                                },
                                "url": {
                                    "type": "string"
                                },
                                "width": {
                                    "type": "string"
                                }
                            }
                        },
                        "looping": {
                            "type": "object",
                            "properties": {
                                "mp4": {
                                    "type": "string"
                                },
                                "mp4_size": {
                                    "type": "string"
                                }
                            }
                        },
                        "fixed_width": {
                            "type": "object",
                            "properties": {
                                "height": {
                                    "type": "string"
                                },
                                "mp4": {
                                    "type": "string"
                                },
                                "mp4_size": {
                                    "type": "string"
                                },
                                "size": {
                                    "type": "string"
                                },
                                "url": {
                                    "type": "string"
                                },
                                "webp": {
                                    "type": "string"
                                },
                                "webp_size": {
                                    "type": "string"
                                },
                                "width": {
                                    "type": "string"
                                }
                            }
                        },
                        "preview_gif": {
                            "type": "object",
                            "properties": {
                                "height": {
                                    "type": "string"
                                },
                                "size": {
                                    "type": "string"
                                },
                                "url": {
                                    "type": "string"
                                },
                                "width": {
                                    "type": "string"
                                }
                            }
                        },
                        "480w_still": {
                            "type": "object",
                            "properties": {
                                "url": {
                                    "type": "string"
                                },
                                "width": {
                                    "type": "string"
                                },
                                "height": {
                                    "type": "string"
                                }
                            }
                        }
                    }
                },
                "user": {
                    "type": "object",
                    "properties": {
                        "avatar_url": {
                            "type": "string"
                        },
                        "banner_image": {
                            "type": "string"
                        },
                        "banner_url": {
                            "type": "string"
                        },
                        "profile_url": {
                            "type": "string"
                        },
                        "username": {
                            "type": "string"
                        },
                        "display_name": {
                            "type": "string"
                        },
                        "description": {
                            "type": "string"
                        },
                        "is_verified": {
                            "type": "boolean"
                        },
                        "website_url": {
                            "type": "string"
                        },
                        "instagram_url": {
                            "type": "string"
                        }
                    }
                }
            }
        },
        "meta": {
            "type": "object",
            "properties": {
                "msg": {
                    "type": "string"
                },
                "status": {
                    "type": "integer"
                },
                "response_id": {
                    "type": "string"
                }
            }
        }
    }
}

At the end we have to respond to the app with the GIF URL that we want to use. There are many variables that have the same name “URL”, so make sure you find the one that I am using below.

Back to the Custom Page

The “Confirm Win” button now has to do a few more things, so here is the added code.

//Patch the Oppty fields
Patch(
    Opportunities,
    LookUp(
        Opportunities,
        Opportunity = GUID(VarOppportunity.opportunityid)
    ),
    {
        'Actual Close Date': EstClosingDate.Value,
        'Actual Revenue': Int(EstimatedRevenue.Value)
    }
);

//Update Opportunity Close entity and retreive GIF
Set(varFlow, CloseOpptyPostTeams.Run(VarOppportunity.Opportunity));
Set(varGIF, varFlow.urlgif);

//Hide input boxes and show confirmation
Set(
    varConfirmdetails,
    false
);
Set(
    varCongratulations,
    true
);

Mostly the only difference here is adding 2 variables. 1 that is the actual response from Power Automate (all of the fields), and the other one is setting a variable “varGIF” for use in our new image component that we add on the sales confirmation box.

Let’s check out that GIF!

Add an image to the Congratulations group, and reference the varGIF

Final Product 💘

When the sales person now closes the dialog, they get a 100% more awesome experience than the OOTB version!!! 🏆👍🎉

Custom Page – Close Dialog Logic

In the last post, we created a new close dialog, but we didn’t add any logic to the buttons.

Logic – Fields and Buttons

The most important parameter we send in via JavaScript last time was the GUID of the record that we are going to work with.

The first thing we do is add an onload to the app and perform a lookup as the very first step. This will give us all of the data for that given Opportunity that we can use within the Power App. We store the whole record in a variable “varOpportunity”.

A little clever step here is actually the “First(Opportunities)”. For testing purposes, this will open up the first Opportunity in the DB if you open the app without the GUID from Dynamics, and from here you can test the app make.powerapps.com studio without having to pass a parameter to the Custom Page 👍

ONLOAD

Set(VarOppportunity,
If(IsBlank(Param("recordId")),
First(Opportunities),
LookUp(Opportunities, Opportunity = GUID(Param("recordId"))))
)

Fields

Fields can now be added via the “varOpportunity” that contains all of the data to the first opportunity in the system.

BUTTONS

The cancel button only has “back()” as a function to close out the dialog, but the “Confirm WIN” has a patch statement for Opportunity.

//Patch the Opportunity fields
Patch(
    Opportunities,
    LookUp(
        Opportunities,
        Opportunity = GUID(VarOppportunity.opportunityid)
    ),
    {
        'Actual Close Date': EstClosingDate.Value,
        'Actual Revenue': Int(EstimatedRevenue.Value)
    }
);
//Hide input boxes and show confirmation
Set(
    varConfirmdetails,
    false
);
Set(
    varCongratulations,
    true
);

HIDE/SHOW

Because of some challenges I met with multiple screens, I had to use a single Screen with hide/show logic. Therefore I added all the fields to Groups and will hide Show based on groups.

The Congratulations group looks like this.

Closing the Opportunity Challenge

If this were a custom entity we could close the opportunity by setting the Status and Status Reason values. Unfortunately, the Opportunity has a function for closing the Opportunity that will create a Case close dialog. In order for this to work, we have to call a custom service for closing the Case. This does get a bit tricky.

We now have to call an action from Power Automate to close the opportunity as WON. At the moment of writing the blog, the process of calling the Microsoft action in Power Automate wasn’t working, so I created my own action. I will show you how, and honestly maybe even recommend doing it this way for now. It works all of the time and uses the technology that has been working in CRM since 4.0.

Custom Action

Actions work with the same logic as a Workflow, but they can be fired at any time from anywhere. They can receive inputs, and generate outputs. A workflow will only trigger from CRUD events, and work within the context of the record triggering the actual workflow. They are in many ways an underrated function in Dynamics / Dataverse.

It’s a pretty simple step updating the status of the opportunity to “won”, and by doing it this way the system will automatically do the correct calls in the API for Opportunity Close.

This is all you need for the action. After activation, we can go back to the custom page and create a instant flow (Power Automate).

In the Custom Page we now add a line to our “Confirm WIN” button. (Yes, I know we probably should add some logic for success/fail, but that will be a part of the final solution on Github).

//Patch the Opportunity fields
Patch(
    Opportunities,
    LookUp(
        Opportunities,
        Opportunity = GUID(VarOppportunity.opportunityid)
    ),
    {
        'Actual Close Date': EstClosingDate.Value,
        'Actual Revenue': Int(EstimatedRevenue.Value)
    }
);
//Hide input boxes and show confirmation
Set(
    varConfirmdetails,
    false
);
Set(
    varCongratulations,
    true
);
//Update Opportunity Close entity
CloseOpptyPostTeams.Run(VarOppportunity.Opportunity);

WINNER WINNER 🏆🥇

You should now be able to close the opportunity as won via a custom page. Just remember to publish the custom page AND publish the app again. If not it will now show. Do remember to give it a few moments before refreshing after a change.

Ribbon Button – Custom Page / Dialog

The deprecation of Dialogs has been discussed WAAAY too many times, and I still feel there is a gap for simple dialogs that would do lots of magic. A while back Microsoft introduced Custom Pages as one option to solve this missing piece, but simplicity is just not there yet.

I have been reviewing lots of great material from fellow MVP’s to study up a bit on the topic
MCJ – Custom Page updating Model Driven app
Scott Durrow – Adding logic to the new button configurator
Lisa Crosbie – Creating custom page

But I can’t quite shake the feeling that it’s still pretty tech-intensive to be able to create a Custom Page and call in from Dynamics. You need to have a decent understanding of the following:

  1. Creating a ribbon button in the new app configuration (easy, but adding commands not so easy)
  2. Javascript to actually start the Custom Page (not for everyone)
  3. Canvas App configuration (I am not a front-end type of guy, and hate having to start with a blank slate every time).
  4. Power FX for the showing of a button + updating whatever you are doing in the canvas app back to the model-driven app.

All in all, I would say that this is probably what you can expect a Dynamics/Power Apps consultant to understand, but it is not given that everyone feels comfortable while configuring. To be honest, Ribbon Workbench isn’t easy without a few tutorials either, but one gets better over time. (did anyone ever get the invert of a true/false statement right the first time?😂).

What to do?

Let’s just get to the bottom of the real issue. How to solve the problem of the Custom Page with a button and at the same time created a great way for sales to have a lot of fun!😁

Over the next weeks, I will start to publish a series of Custom Page and Sales, to showcase what you can do to improve Sales morale and adoption

Every post will be shown on the new Sales Page that I have “Win Notification” so stay tuned!!

The full solution will be available for download in the end.