Modules/businessdev.ALbuild.Containers/Private/Resolve-BcWebClientUrl.ps1

function Resolve-BcWebClientUrl {
    <#
    .SYNOPSIS
        Builds the web client URL that opens one page or report, with company, tenant, filter and mode.
 
    .DESCRIPTION
        URL construction lives here, in PowerShell, rather than in the capture engine - because this
        is the half that can be tested without a browser, and because these are the parts of the
        contract that are actually DOCUMENTED and stable:
 
            page=<id> report=<id> filter=<expr> mode=View|Edit
            company=<name> tenant=<name>
 
        (Web client URL parameters, learn.microsoft.com/dynamics365/business-central/dev-itpro/
        developer/devenv-web-client-urls#url-parameters)
 
        'mode' is supported for CARD pages. For List, RoleCenter and CardPart the client silently
        stays in View - which is why the engine verifies afterwards that an Edit it was asked for
        actually arrived, instead of trusting the URL.
 
        'Create' is deliberately not offered anywhere: it writes data, and this command promises a
        picture.
 
    .PARAMETER BaseUrl
        The container's web client URL, e.g. 'http://172.25.0.77/BC/'.
 
    .PARAMETER Kind
        'page' or 'report'.
 
    .PARAMETER Id
        The object id.
 
    .PARAMETER Company
        Company to open. Empty = the container's default company.
 
    .PARAMETER Tenant
        Tenant. Default 'default'.
 
    .PARAMETER Filter
        A record filter, e.g. "'No.' IS '10000'" - how a card is addressed, since clicking a row in a
        list only selects it.
 
    .PARAMETER Mode
        'View' (default) or 'Edit'.
 
    .OUTPUTS
        String.
 
    .EXAMPLE
        Resolve-BcWebClientUrl -BaseUrl 'http://10.1.0.42/BC/' -Kind page -Id 22 -Company 'CRONUS DE'
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $BaseUrl,
        [Parameter(Mandatory)] [ValidateSet('page', 'report')] [string] $Kind,
        [Parameter(Mandatory)] [int] $Id,
        [string] $Company,
        [string] $Tenant = 'default',
        [string] $Filter,
        [ValidateSet('', 'View', 'Edit')] [string] $Mode = ''
    )

    # Keep whatever query the base URL already carries (Get-BcContainerWebClientUrl appends the
    # tenant) instead of appending a second '?' and producing a URL the client ignores.
    $split = "$BaseUrl" -split '\?', 2
    $root = $split[0]
    if (-not $root.EndsWith('/')) { $root += '/' }

    $query = [System.Collections.Generic.List[string]]::new()
    $existing = @{}
    if ($split.Count -gt 1 -and $split[1]) {
        foreach ($pair in ($split[1] -split '&')) {
            if (-not $pair) { continue }
            $name = ($pair -split '=', 2)[0]
            $existing[$name.ToLowerInvariant()] = $true
            $query.Add($pair)
        }
    }

    # [uri]::EscapeDataString does NOT produce the same string on both hosts ALbuild targets: .NET
    # Framework follows RFC 2396, where an apostrophe is an unreserved 'mark' and survives unescaped,
    # while .NET (Core) follows RFC 3986 and writes %27. A record filter is built out of apostrophes -
    # "'No.' IS '10000'" - so the same call produced two different URLs depending on whether the task
    # ran under Windows PowerShell 5.1 or PowerShell 7. Escaping it afterwards is a no-op on .NET,
    # where it is already %27, and makes 5.1 agree.
    $escape = { param($value) ([uri]::EscapeDataString("$value")) -replace "'", '%27' }

    $add = {
        param($name, $value)
        if ([string]::IsNullOrWhiteSpace("$value")) { return }
        if ($existing.ContainsKey($name.ToLowerInvariant())) { return }
        $query.Add("$name=$(& $escape $value)")
    }

    & $add 'tenant' $Tenant
    & $add 'company' $Company
    $query.Add("$Kind=$Id")
    if ($Filter) { $query.Add("filter=$(& $escape $Filter)") }
    # Only for pages: a report has no view mode, and sending one is noise in a URL that is read by
    # a human when something goes wrong.
    if ($Mode -and $Kind -eq 'page') { $query.Add("mode=$Mode") }

    return $root + '?' + ($query -join '&')
}