src/Solutions/Customization/Dashboards/Upsert-XrmDashboard.ps1

<#
    .SYNOPSIS
    Create or update a dashboard in Microsoft Dataverse.

    .DESCRIPTION
    Upsert a systemform record of type dashboard by Id: classic (type = 0) by default, interactive experience with FormType 10. Delegates to Upsert-XrmForm.
    Dataverse keeps the type of an existing form and ignores the one sent: without FormType, an existing dashboard keeps its type.

    .PARAMETER XrmClient
    Xrm connector initialized to target instance. Use latest one by default. (Dataverse ServiceClient)

    .PARAMETER Id
    Dashboard (systemform) Id used as the upsert key.

    .PARAMETER Name
    Dashboard display name.

    .PARAMETER Labels
    Hashtable of language code to display name. Alternative to -Name. The stored 'name' is resolved from -LanguageCode (fallback: lowest language code), and every provided language is persisted as a real translation via SetLocLabels (delegated to Upsert-XrmForm). Example: @{ 1033 = "Sales Dashboard"; 1036 = "Tableau de bord des ventes" }

    .PARAMETER LanguageCode
    Language code used to pick the stored 'name' from -Labels. Default: 1033.

    .PARAMETER FormXml
    Dashboard form XML definition.

    .PARAMETER Description
    Dashboard description.

    .PARAMETER SolutionUniqueName
    Unmanaged solution unique name. When provided, the dashboard is added to this solution.

    .PARAMETER FormType
    Dashboard type: 0 = classic (default), 10 = interactive experience. When given, an existing dashboard of another type raises an error before anything is written: its type cannot change (delete it, then upsert it again).

    .OUTPUTS
    Microsoft.Xrm.Sdk.EntityReference. Reference to the upserted systemform record.

    .EXAMPLE
    $ref = Upsert-XrmDashboard -Id $dashboardId -Name "Sales Dashboard" -FormXml $xml -SolutionUniqueName "MySolution";

    .EXAMPLE
    $ref = Upsert-XrmDashboard -Id $dashboardId -Name "Cash follow-up" -FormXml $interactiveXml -FormType 10;

    .LINK
    https://github.com/AymericM78/PowerDataOps/blob/main/documentation/commands/Upsert-XrmDashboard.md
#>

function Upsert-XrmDashboard {
    [CmdletBinding(DefaultParameterSetName = "ByName", SupportsShouldProcess)]
    [OutputType([Microsoft.Xrm.Sdk.EntityReference])]
    param
    (
        [Parameter(Mandatory = $false, ValueFromPipeline)]
        [Microsoft.PowerPlatform.Dataverse.Client.ServiceClient]
        $XrmClient = $Global:XrmClient,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [Guid]
        $Id,

        [Parameter(Mandatory = $true, ParameterSetName = "ByName")]
        [ValidateNotNullOrEmpty()]
        [string]
        $Name,

        [Parameter(Mandatory = $true, ParameterSetName = "ByLabels")]
        [ValidateNotNullOrEmpty()]
        [Hashtable]
        $Labels,

        [Parameter(Mandatory = $false)]
        [int]
        $LanguageCode = 1033,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $FormXml,

        [Parameter(Mandatory = $false)]
        [string]
        $Description,

        [Parameter(Mandatory = $false)]
        [string]
        $SolutionUniqueName,

        [Parameter(Mandatory = $false)]
        [ValidateSet(0, 10)]
        [int]
        $FormType = 0
    )
    begin {
        $StopWatch = [System.Diagnostics.Stopwatch]::StartNew();
        Trace-XrmFunction -Name $MyInvocation.MyCommand.Name -Stage Start -Parameters ($MyInvocation.MyCommand.Parameters);
    }
    process {
        # The upsert would keep the type of an existing dashboard without a word: an explicit type is checked first
        if ($PSBoundParameters.ContainsKey('FormType')) {
            $existing = Get-XrmRecord -XrmClient $XrmClient -LogicalName "systemform" -Id $Id -Columns "type" -IfExists;
            if ($existing) {
                $existingType = $existing | Get-XrmAttributeValue -Name "type" -Raw;
                if ($existingType -ne $FormType) {
                    throw [System.InvalidOperationException]::new("Dashboard $Id exists with form type $existingType, not $($FormType): Dataverse keeps the type of an existing form. Delete it, then upsert it again to change its type.");
                }
            }
        }

        $params = @{
            XrmClient = $XrmClient;
            Id        = $Id;
            FormXml   = $FormXml;
            FormType  = $FormType;
        };

        if ($PSCmdlet.ParameterSetName -eq "ByLabels") {
            $params["Labels"] = $Labels;
            $params["LanguageCode"] = $LanguageCode;
        }
        else {
            $params["Name"] = $Name;
        }

        if ($PSBoundParameters.ContainsKey('Description')) {
            $params["Description"] = $Description;
        }
        if ($PSBoundParameters.ContainsKey('SolutionUniqueName')) {
            $params["SolutionUniqueName"] = $SolutionUniqueName;
        }

        Upsert-XrmForm @params;
    }
    end {
        $StopWatch.Stop();
        Trace-XrmFunction -Name $MyInvocation.MyCommand.Name -Stage Stop -StopWatch $StopWatch;
    }
}

Export-ModuleMember -Function Upsert-XrmDashboard -Alias *;