Public/ConfigCommands/Add-RpConfigCommand.ps1

function Add-RpConfigCommand {
    <#
    .SYNOPSIS
    Exports command configurations from RemotePro to a JSON file.
    Allows GUI-based or parameter-based input for command selection.
 
    .DESCRIPTION
    The `Add-RpConfigCommand` function exports specified commands from a
    PowerShell module to a JSON configuration file. It can gather input
    interactively via a GUI dialog or directly through parameters.
 
    If the configuration file does not exist, a new one is created. Existing
    commands can be updated by specifying an ID. Parameters include a
    module name, command names, file path, optional description, and
    show dialog.
 
    .COMPONENT
    ConfigCommands
 
    .PARAMETER ModuleName
    Specifies the name of the PowerShell module from which to export
    commands. Required.
 
    .PARAMETER CommandNames
    Specifies a list of specific commands to export from the
    module.
 
    .PARAMETER ConfigFilePath
    Specifies the path where the JSON configuration file is saved. If the
    file does not exist, it will be created. Default: `Get-RpConfigPath`
 
    .PARAMETER Description
    Optional. A brief description for the command being exported.
 
    .PARAMETER ShowDialog
    Optional. Displays a GUI dialog for input. If selected, all other
    parameters are ignored, and user input is collected via the dialog.
 
    .PARAMETER SETS
    ShowDialog: Activates GUI-based input for all parameters.
    ConfigurationItems: Uses parameter-based input directly without GUI.
 
    .EXAMPLE
    Add-RpConfigCommand -ModuleName "RemotePro" -ConfigFilePath "C:\Config.json" `
                        -CommandNames "Get-RpEventHandlers", "Set-RpConfig" `
                        -Description "Initial configuration export"
 
    Exports "Get-RpEventHandlers" and "Set-RpConfig" commands from the
    "RemotePro" module to a JSON file at `C:\Config.json`.
 
    .EXAMPLE
    Add-RpConfigCommand -ModuleName "RemotePro" -ShowDialog
 
    Opens a GUI dialog for selecting module, commands, configuration path,
    and description, then exports the chosen commands to the JSON file.
 
    .NOTES
    Ensure that the PresentationFramework assembly is available for WPF
    support to allow GUI interaction. A new configuration file will be created
    if it doesn’t already exist.
 
    .LINK
    https://www.remotepro.dev/en-US/Add-RpConfigCommand
 
    #>

    [CmdletBinding(DefaultParameterSetName = 'ShowDialog')]
    param (
        # Name of the module to export commands from
        [Parameter(Mandatory=$false, Position=0, ParameterSetName='ConfigurationItems')]
        [string]$ModuleName,

        # Optional: List of specific commands to export
        [Parameter(Mandatory=$false, Position=1, ParameterSetName='ConfigurationItems')]
        [string[]]$CommandNames,

        # Path to save the configuration JSON file
        [Parameter(Mandatory=$false, Position=2, ParameterSetName='ConfigurationItems')]
        [string]$ConfigFilePath,

        # Optional description for the command
        [Parameter(Mandatory=$false)]
        [string]$Description = "",

        # Optional: Show GUI dialog for input
        [Parameter(Mandatory=$false, ParameterSetName='ShowDialog')]
        [switch]$ShowDialog
    )

    begin {
        # Use appdata path if there is not a filepath value.
        if (-not $PSBoundParameters.ContainsKey('ConfigFilePath') -or [string]::IsNullOrWhiteSpace($ConfigFilePath)) {
            $ConfigFilePath = Get-RpConfigPath
        }

        if (-not ($ConfigFilePath)){
            $ConfigFilePath = Get-RpConfigPath
        }


        # Check if the configuration file exists
        if (-not (Test-Path -Path $(Get-RpConfigPath))){
            New-RpConfigCommandJson -Type EmptyJson
        }

        # Initialize variables
        elseif ($ShowDialog) {
            Add-Type -AssemblyName PresentationFramework

            $resultText = @()
        }

        # Check if required parameters are missing when not using the dialog
        if (-not $PSBoundParameters.ContainsKey('ModuleName') -or -not $PSBoundParameters.ContainsKey('CommandNames')) {
            Write-Warning "Required parameters: `"ModuleName`" and `"CommandNames`" are not fully provided. Opening dialog window for input."
            $ShowDialog = $true
        }
    }

    process {
        # Show WPF dialog if -ShowDialog is used
        if ($ShowDialog) {

        # Define XAML layout for WPF GUI
        $xaml = @"
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
        xmlns:mdControls="clr-namespace:MaterialDesignThemes.Wpf;assembly=MaterialDesignThemes.Wpf"
        Title="Add-RpConfigCommand Dialog"
        Width="400" Height="550"
        WindowStartupLocation="CenterScreen"
        SizeToContent="WidthAndHeight"
        Background="{DynamicResource MaterialDesignPaper}">
 
    <Window.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <materialDesign:BundledTheme x:Key="AppTheme" BaseTheme="Light" PrimaryColor="Grey" SecondaryColor="Lime" />
                <ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesign2.Defaults.xaml" />
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Window.Resources>
 
    <StackPanel Margin="10">
        <TextBlock Text="Select Module:" Margin="0,10,0,5"/>
        <ComboBox Name="ModuleComboBox" Width="350" />
 
        <TextBlock Text="Select Commands:" Margin="0,10,0,5"/>
        <ListBox Name="CommandListBox" Width="350" Height="350" SelectionMode="Multiple"/>
 
        <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="10">
            <Button Name="SubmitButton" Content="Submit" Width="75" Margin="5" Style="{StaticResource MaterialDesignFlatButton}"/>
            <Button Name="CancelButton" Content="Cancel" Width="75" Margin="5" Style="{StaticResource MaterialDesignFlatButton}"/>
        </StackPanel>
    </StackPanel>
</Window>
"@


            # Parse XAML and load WPF elements
            $window = [Windows.Markup.XamlReader]::Parse($xaml)

            # Set the window icon
            if ($null -ne $window) {
                Set-RpWindowIcon -window $window
            } else {
                Write-Warning "WPF window failed to load. Cannot set icon."
            }

            # Set the window icon
            if ($null -ne $window) {
                Set-RpWindowIcon -window $window
            } else {
                Write-Warning "WPF window failed to load. Cannot set icon."
            }

            # Find WPF Controls
            $moduleComboBox = $window.FindName("ModuleComboBox")
            $commandListBox = $window.FindName("CommandListBox")
            $submitButton = $window.FindName("SubmitButton")
            $cancelButton = $window.FindName("CancelButton")

            # Create a mapping of DisplayName -> ModuleName
            $moduleMapping = @{}

            foreach ($module in (Get-Module -ListAvailable)) {
                $displayName = "$($module.Name) ($($module.Version))"
                $moduleMapping[$displayName] = $module.Name
                [void]$moduleComboBox.Items.Add($displayName)
            }

            # Set default selection to the first item if available
            if ($moduleComboBox.Items.Count -gt 0) {
                $moduleComboBox.SelectedIndex = 0
            }

            # Handle module selection and extract the actual module name
            $moduleComboBox.add_SelectionChanged({
                [System.Windows.Input.Mouse]::OverrideCursor = [System.Windows.Input.Cursors]::Wait
                try {
                    $commandListBox.Items.Clear()
                    $selectedDisplayName = $moduleComboBox.SelectedItem
                    if ($selectedDisplayName) {
                        $selectedModule = $moduleMapping[$selectedDisplayName]
                        foreach ($command in (Get-Command -Module $selectedModule)) {
                            $commandListBox.Items.Add($command.Name)
                        }
                    }
                } finally {
                    [System.Windows.Input.Mouse]::OverrideCursor = $null
                }
            })

            # Initialize a hashtable to capture results
            $dialogResults = [ordered]@{
                ModuleName = $null
                CommandNames = @()
            }

            # Event Handler for Submit Button
            $submitButton.Add_Click({
                [System.Windows.Input.Mouse]::OverrideCursor = [System.Windows.Input.Cursors]::Wait
                try {
                    # Capture GUI input values in the hashtable
                    $dialogResults.ModuleName = $moduleMapping[$moduleComboBox.SelectedItem]
                    $dialogResults.CommandNames = @($commandListBox.SelectedItems | ForEach-Object { $_.ToString() })

                    # Close dialog
                    $window.DialogResult = $true
                    $window.Close()
                } finally {
                    [System.Windows.Input.Mouse]::OverrideCursor = $null  # Reset the cursor
                }
            })
            # Event Handler for Cancel Button
            $cancelButton.Add_Click({
                [System.Windows.Input.Mouse]::OverrideCursor = [System.Windows.Input.Cursors]::Wait
                try {
                    # Capture GUI input values in the hashtable
                    $dialogResults.ModuleName = $moduleMapping[$moduleComboBox.SelectedItem]
                    $dialogResults.CommandNames = @($commandListBox.SelectedItems | ForEach-Object { $_.ToString() })

                    # Close dialog
                    $window.DialogResult = $true
                    $window.Close()
                    $window.DialogResult = $false
                    $window.Close()
                } finally {
                    [System.Windows.Input.Mouse]::OverrideCursor = $null  # Reset the cursor
                }
            })

            # Show the WPF Window
            $result = $window.ShowDialog()
            if (-not $result) { Write-Verbose "Operation canceled by user."; return }

            # Assign GUI input results to the main function parameters
            $ModuleName = $dialogResults.ModuleName
            $CommandNames = $dialogResults.CommandNames

        }

        # Core logic starts here...
        # Load existing configuration or create a new PSCustomObject if the file does not exist
        if (Test-Path -Path $ConfigFilePath) {
            $configContent = Get-Content -Path $ConfigFilePath -Raw
            $config = $configContent | ConvertFrom-Json
        } else {
            # Create a new configuration if the file doesn't exist
            $config = [pscustomobject]@{ ConfigCommands = @{} }
            Write-Verbose "No existing configuration found. Creating a new one."
        }

        # Use the actual ModuleName (not DisplayName or other variables)
        $actualModuleName = if ($ShowDialog) { $dialogResults.ModuleName } else { $ModuleName }

        # Ensure ConfigCommands and Module sections exist
        if (-not $config.PSObject.Properties['ConfigCommands']) {
            $config | Add-Member -MemberType NoteProperty -Name 'ConfigCommands' -Value @{}
        }
        if (-not $config.ConfigCommands.PSObject.Properties[$actualModuleName]) {
            $config.ConfigCommands | Add-Member -MemberType NoteProperty -Name $actualModuleName -Value @()
        } elseif (-not ($config.ConfigCommands.$actualModuleName -is [System.Collections.IList])) {
            $config.ConfigCommands.$actualModuleName = @($config.ConfigCommands.$actualModuleName)
        }

        # List to store generated commands
        $generatedCommands = @()

        # Add selected commands to the configuration file
        $commands = if ($commandNames) {
            try {
                Get-Command -Module $actualModuleName | Where-Object { $commandNames -contains $_.Name }
            }
            catch {
                Write-Error "Command was not selected: $_"
                return
            }
        }

        foreach ($command in $commands) {
            $paramConfig = [pscustomobject]@{}
            foreach ($param in $command.Parameters.Keys) {
                $paramDetails = [pscustomobject]@{
                    'Type'      = $command.Parameters[$param].ParameterType.FullName
                    'Mandatory' = $command.Parameters[$param].Attributes.Mandatory
                    'Value'     = "$null"  # Initialize Value to null
                }
                $paramConfig | Add-Member -MemberType NoteProperty -Name $param -Value $paramDetails
            }

            # Create command details for configuration
            $commandDetails = [pscustomobject]@{
                'ModuleName'  = $actualModuleName  # Use actual module name
                'CommandName' = $command.Name
                'Id'          = [System.Guid]::NewGuid().ToString()
                'Description' = if ($Description) { $Description } else { "$($command.Name) command description" }
                'Parameters'  = $paramConfig
            }

            # Check for existing command and add if unique
            $existingCommands = $config.ConfigCommands.$ModuleName
            if (-not ($existingCommands | Where-Object { $_.ID -eq $Id })) {
                $config.ConfigCommands.$ModuleName += $commandDetails
                $generatedCommands += $commandDetails  # Add to the list of generated commands
            } else {
                Write-Verbose "Command with ID $Id already exists in module '$moduleName'. Skipping addition."
            }

            # Collect the ID for later use
            $collectedIds += $commandDetails.Id

            $resultText += "Assigned ModuleName: $moduleName`n"
            $resultText += "Assigned CommandNames: $commandNames`n"
            $resultText += "Assigned Id: $($commandDetails.Id)`n`n"
            # Debugging output to confirm assignment
            Write-Verbose "$($resultText)"
        }

        # Convert updated configuration back to JSON and save
        $jsonConfig = $config | ConvertTo-Json -Depth 4
        if ($configFilePath) {
            Set-Content -Path $ConfigFilePath -Value $jsonConfig
            Write-Verbose "Configuration for module '$ModuleName' saved to $configFilePath"
        } else {
            Write-Verbose "Error: ConfigFilePath is empty. Unable to save configuration."
        }

        if ($showDialog -and $commands) {
            $window = New-Object System.Windows.Window
            $window.Title = "Information"
            $window.Width = 425
            $window.Height = 220
            $window.WindowStartupLocation = "CenterScreen"
            $window.ResizeMode = "NoResize"
            $window.WindowStyle = "SingleBorderWindow"

            $stackPanel = New-Object System.Windows.Controls.StackPanel
            $stackPanel.Orientation = "Vertical"

            # TextBox for displaying resultText
            $textBox = New-Object System.Windows.Controls.TextBox
            $textBox.Text = $resultText
            $textBox.Margin = "10"
            $textBox.TextWrapping = "Wrap"
            $textBox.VerticalAlignment = "Top"
            $textBox.HorizontalAlignment = "Stretch"
            $textBox.Height = 100  # Restrict height to prevent overflowing
            $textBox.IsReadOnly = $true
            $textBox.VerticalScrollBarVisibility = "Auto"
            $textBox.HorizontalScrollBarVisibility = "Disabled"
            [void]$stackPanel.Children.Add($textBox)

            # Panel for buttons
            $buttonPanel = New-Object System.Windows.Controls.StackPanel
            $buttonPanel.Orientation = "Horizontal"
            $buttonPanel.HorizontalAlignment = "Center"
            $buttonPanel.Margin = "10"

            # Copy Button
            $copyButton = New-Object System.Windows.Controls.Button
            $copyButton.Content = "Copy Id(s)"
            $copyButton.Margin = "5"
            $copyButton.Width = 80
            $copyButton.Add_Click({
                try {
                    # Join the collected IDs into a comma-separated string
                    $idsString = $collectedIds -join ", "

                    if (-not [string]::IsNullOrWhiteSpace($idsString)) {
                        # Copy the IDs to the clipboard
                        [System.Windows.Clipboard]::SetText($idsString)
                        [System.Windows.MessageBox]::Show("Command ID(s) copied to clipboard.", "Copied", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Information)
                    } else {
                        [System.Windows.MessageBox]::Show("No IDs found to copy.", "Error", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Error)
                    }
                } catch {
                    # Handle any errors gracefully
                    [System.Windows.MessageBox]::Show("An error occurred while copying IDs: $($_.Exception.Message)", "Error", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Error)
                }
            })

            [void]$buttonPanel.Children.Add($copyButton)

            # OK Button
            $okButton = New-Object System.Windows.Controls.Button
            $okButton.Content = "OK"
            $okButton.Margin = "5"
            $okButton.Width = 80
            $okButton.Add_Click({ $window.Close() })
            [void]$buttonPanel.Children.Add($okButton)

            # Add the button panel to the stack panel
            [void]$stackPanel.Children.Add($buttonPanel)

            # Set the stack panel as the content of the window
            $window.Content = $stackPanel
            [void]$window.ShowDialog()
        }
    }
    end {
        if ($showDialog){
            $window.Close()
        } elseif ($commands) {
            Set-RpConfigCommands # Update RpControllerObject with added commands.
        }
        return $generatedCommands  # Return the list of generated commands
    }
}