src/Solutions/Customization/Views/Upsert-XrmView.ps1

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

    .DESCRIPTION
    Upsert a savedquery record (system view) by Id using the Upsert SDK message. If the record exists it is updated; otherwise it is created with the provided Id. Delegates to Upsert-XrmRecord.

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

    .PARAMETER Id
    View (savedquery) Id used as the upsert key.

    .PARAMETER EntityLogicalName
    Table / Entity logical name the view belongs to.

    .PARAMETER Name
    View 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). Example: @{ 1033 = "Active accounts"; 1036 = "Comptes actifs" }

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

    .PARAMETER FetchXml
    FetchXml query for the view.

    .PARAMETER LayoutXml
    Layout XML defining column widths and order.

    .PARAMETER QueryType
    View query type. Default: 0 (public view).

    .PARAMETER Description
    View description.

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

    .PARAMETER IsDefault
    Make the view the default one of its type for the table. With $true, the flag is cleared on the previous default views of the same table and type (the platform keeps it otherwise).

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

    .EXAMPLE
    $ref = Upsert-XrmView -Id $viewId -EntityLogicalName "account" -Name "Active Accounts" -FetchXml $fetchXml -LayoutXml $layoutXml -SolutionUniqueName "MySolution";

    .EXAMPLE
    $ref = Upsert-XrmView -XrmClient $xrmClient -Id $viewId -EntityLogicalName "account" -Name "My Accounts" -FetchXml $fetchXml -LayoutXml $layoutXml -IsDefault $true;

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

function Upsert-XrmView {
    [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)]
        [ValidateNotNullOrEmpty()]
        [string]
        $EntityLogicalName,

        [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]
        $FetchXml,

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

        [Parameter(Mandatory = $false)]
        [int]
        $QueryType = 0,

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

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

        [Parameter(Mandatory = $false)]
        [bool]
        $IsDefault
    )
    begin {
        $StopWatch = [System.Diagnostics.Stopwatch]::StartNew();
        Trace-XrmFunction -Name $MyInvocation.MyCommand.Name -Stage Start -Parameters ($MyInvocation.MyCommand.Parameters);
    }
    process {
        if ($PSCmdlet.ParameterSetName -eq "ByLabels") {
            $Name = Get-XrmLabelText -Labels $Labels -LanguageCode $LanguageCode;
        }

        $attributes = @{
            "returnedtypecode" = $EntityLogicalName;
            "name"             = $Name;
            "fetchxml"         = $FetchXml;
            "layoutxml"        = $LayoutXml;
            "querytype"        = $QueryType;
        };
        if ($PSBoundParameters.ContainsKey('Description')) {
            $attributes["description"] = $Description;
        }
        if ($PSBoundParameters.ContainsKey('IsDefault')) {
            $attributes["isdefault"] = $IsDefault;
        }

        $record = New-XrmEntity -LogicalName "savedquery" -Id $Id -Attributes $attributes;

        $XrmClient | Upsert-XrmRecord -Record $record | Out-Null;
        $viewReference = New-XrmEntityReference -LogicalName "savedquery" -Id $Id;

        # The platform keeps the previous default view flagged: clear it, one default per table and view type
        if ($PSBoundParameters.ContainsKey('IsDefault') -and $IsDefault) {
            $previousDefaults = @(Get-XrmViews -XrmClient $XrmClient -EntityLogicalName $EntityLogicalName -QueryType $QueryType -IsDefault -Columns "savedqueryid" | Where-Object { $_.Id -ne $Id });
            foreach ($previousDefault in $previousDefaults) {
                $XrmClient | Update-XrmRecord -Record (New-XrmEntity -LogicalName "savedquery" -Id $previousDefault.Id -Attributes @{ isdefault = $false });
            }
        }

        # Persist the multilingual name as real translations (SetLocLabels) so each language sees its own label.
        if ($PSCmdlet.ParameterSetName -eq "ByLabels") {
            Set-XrmLocalizedLabel -XrmClient $XrmClient -EntityMoniker $viewReference -AttributeName "name" -Labels $Labels | Out-Null;
        }

        if ($PSBoundParameters.ContainsKey('SolutionUniqueName')) {
            Add-XrmSolutionComponent -XrmClient $XrmClient -SolutionUniqueName $SolutionUniqueName -ComponentId $Id -ComponentType 26 -DoNotIncludeSubcomponents $false | Out-Null;
        }

        $viewReference;
    }
    end {
        $StopWatch.Stop();
        Trace-XrmFunction -Name $MyInvocation.MyCommand.Name -Stage Stop -StopWatch $StopWatch;
    }
}

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

Register-ArgumentCompleter -CommandName Upsert-XrmView -ParameterName "EntityLogicalName" -ScriptBlock {
    param($CommandName, $ParameterName, $WordToComplete, $CommandAst, $FakeBoundParameters)
    $validLogicalNames = Get-XrmEntitiesLogicalName;
    return $validLogicalNames | Where-Object { $_ -like "$wordToComplete*" };
}