XurrentHelpers.psm1
|
#Region './Private/Get-Section.ps1' -1 function Get-Section { <# .SYNOPSIS Extracts the body of a named Markdown H2 section. .DESCRIPTION Returns the trimmed text body of a ## Heading section from a Markdown string. Matches content from the heading line to the next same-level ## heading or end of string. Returns an empty string when the requested section is not found. .PARAMETER Content The full Markdown document content as a string. .PARAMETER Heading The section heading to search for, without the leading ## prefix. .EXAMPLE Get-Section -Content $markdown -Heading 'Description' #> [CmdletBinding()] [OutputType([string])] param ( [Parameter(Mandatory = $true)] [string] $Content, [Parameter(Mandatory = $true)] [string] $Heading ) process { $pattern = "(?ms)^##\s+$([regex]::Escape($Heading))\s*$\r?\n(.*?)(?=\r?\n^##\s+|\z)" if ($Content -match $pattern) { return $Matches[1].Trim() } return '' } } #EndRegion './Private/Get-Section.ps1' 44 #Region './Public/ConvertFrom-XurrentKnowledgeArticle.ps1' -1 function ConvertFrom-XurrentKnowledgeArticle { <# .SYNOPSIS Converts a Xurrent knowledge article CSV row to a Markdown file. .DESCRIPTION Takes a PSCustomObject in the Xurrent / 4me bulk-import CSV schema (as produced by Import-Csv or ConvertTo-XurrentKnowledgeArticle) and writes a *KnowledgeArticle.md file with the correct structure: H1 for Subject, **ID:**, **Service:**, **Service Instances:**, and **Keywords:** metadata lines, plus ## Description and ## Instructions sections. Accepts pipeline input for batch processing of CSV rows. Invalid filename characters in Subject are replaced with hyphens. .PARAMETER InputObject A PSCustomObject with ID, Service, Service Instances, Subject, Description, Instructions, and Keywords properties, matching the Xurrent knowledge article CSV schema. Accepts pipeline input. .PARAMETER Path The folder in which to write the Markdown file. Defaults to the current working directory. .EXAMPLE Import-Csv .\knowledge_articles.csv | ConvertFrom-XurrentKnowledgeArticle -Path .\Articles .EXAMPLE $row = [PSCustomObject]@{ Subject = 'VPN Setup'; Description = 'How to...'; Instructions = 'Step 1...'; Keywords = 'vpn' } ConvertFrom-XurrentKnowledgeArticle -InputObject $row -Path .\Output #> [CmdletBinding(SupportsShouldProcess = $true)] [OutputType([System.IO.FileInfo])] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [PSCustomObject] $InputObject, [Parameter()] [ValidateScript({ Test-Path $_ -PathType Container })] [string] $Path = (Get-Location).Path ) process { $id = if ($InputObject.ID) { [string]$InputObject.ID } else { '' } $service = if ($null -ne $InputObject.PSObject.Properties['Service'] -and $InputObject.PSObject.Properties['Service'].Value) { [string]$InputObject.Service } else { '' } $serviceInstances = if ($null -ne $InputObject.PSObject.Properties['Service Instances'] -and $InputObject.PSObject.Properties['Service Instances'].Value) { [string]$InputObject.'Service Instances' } elseif ($null -ne $InputObject.PSObject.Properties['ServiceInstances'] -and $InputObject.PSObject.Properties['ServiceInstances'].Value) { [string]$InputObject.ServiceInstances } else { '' } $subject = if ($InputObject.Subject) { [string]$InputObject.Subject } else { '' } $description = if ($InputObject.Description) { [string]$InputObject.Description } else { '' } $instructions = if ($InputObject.Instructions) { [string]$InputObject.Instructions } else { '' } $keywords = if ($InputObject.Keywords) { [string]$InputObject.Keywords } else { '' } $invalidChars = [System.IO.Path]::GetInvalidFileNameChars() -join '' $safeName = $subject -replace "[$([regex]::Escape($invalidChars))]", '-' $safeName = $safeName.Trim('-', ' ') if (-not $safeName) { $safeName = 'Unknown' } $fileName = '{0}KnowledgeArticle.md' -f $safeName $filePath = Join-Path -Path $Path -ChildPath $fileName $content = @" # $subject **ID:** $id **Service:** $service **Service Instances:** $serviceInstances **Keywords:** $keywords ## Description $description ## Instructions $instructions "@ if ($PSCmdlet.ShouldProcess($filePath, 'Create knowledge article Markdown file')) { Set-Content -Path $filePath -Value $content -Encoding UTF8 Get-Item -Path $filePath } } } #EndRegion './Public/ConvertFrom-XurrentKnowledgeArticle.ps1' 111 #Region './Public/ConvertTo-XurrentCrossEnvironmentImport.ps1' -1 function ConvertTo-XurrentCrossEnvironmentImport { <# .SYNOPSIS Prepares Xurrent CSV exports for import into another environment by moving ID into Source ID. .DESCRIPTION Prepares Xurrent CSV exports from one environment (e.g. QA) for import into another environment (e.g. Production). An import row that carries an ID updates the record with that ID in the target environment. Moving the value to the Source ID column makes the import create a new record instead, while the original ID stays traceable. For every row with a non-empty ID, Source ID is set to the ID and the ID is emptied; the ID column itself stays in the file. Rows with an empty ID are left untouched, so a second run does not wipe the Source ID written by the first. An existing, different Source ID is overwritten with a warning. A file without rows, or without the ID or Source ID column, is skipped with a warning. The file is overwritten in place. Column order and all other values are kept, including multi-line quoted fields such as the HTML, CSS and JavaScript of UI extension exports. Every field is written quoted, as UTF-8 without a byte-order mark (BOM); Xurrent rejects a BOM with "Illegal quoting in line 1". Files are selected by name (-Path, also from the pipeline) or by download time (-LastHours, the default). .PARAMETER Path One or more CSV files to convert. Relative names are resolved against -Folder. Accepts pipeline input by value and by property name (FullName), so the output of Get-ChildItem can be piped in. A name that is not found produces a warning and is skipped. .PARAMETER LastHours Selects every *.csv file in -Folder whose CreationTime or LastWriteTime lies within the last N hours. Defaults to 1. Applies when -Path is not given. .PARAMETER Folder Folder that relative -Path names are resolved against and that -LastHours scans. Defaults to the Downloads folder in the user's home directory. .PARAMETER IdColumn Name of the CSV column holding the record ID that is moved and then emptied. Defaults to 'ID'. .PARAMETER SourceIdColumn Name of the CSV column that receives the record ID. Defaults to 'Source ID'. .OUTPUTS One object per processed file with the properties File (full path), Moved (number of moved IDs) and Status: Moved, NothingToMove, Skipped, or WhatIf when the file was not written because of -WhatIf or a declined -Confirm. .EXAMPLE ConvertTo-XurrentCrossEnvironmentImport Converts every CSV file downloaded to the Downloads folder within the last hour. .EXAMPLE ConvertTo-XurrentCrossEnvironmentImport -LastHours 3 -WhatIf Shows which CSV files of the last three hours would be changed, without writing them. .EXAMPLE ConvertTo-XurrentCrossEnvironmentImport -Path slas-1.csv Converts slas-1.csv in the Downloads folder. .EXAMPLE ConvertTo-XurrentCrossEnvironmentImport -Path teams-1.csv, slas-1.csv -Folder .\Exports Converts two files in the Exports folder below the current directory. .EXAMPLE Get-ChildItem -Path .\Exports -Filter '*slas*.csv' | ConvertTo-XurrentCrossEnvironmentImport Converts the files passed through the pipeline. #> [CmdletBinding(SupportsShouldProcess = $true, DefaultParameterSetName = 'ByTime')] [OutputType([PSCustomObject])] param ( [Parameter(Mandatory = $true, ParameterSetName = 'ByName', Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [Alias('FullName')] [string[]] $Path, [Parameter(ParameterSetName = 'ByTime')] [double] $LastHours = 1, [Parameter()] [string] $Folder = (Join-Path $HOME 'Downloads'), [Parameter()] [string] $IdColumn = 'ID', [Parameter()] [string] $SourceIdColumn = 'Source ID' ) begin { $files = [System.Collections.Generic.List[object]]::new() } process { # The process block also runs once when nothing is piped in; only -Path adds files here. if ($PSCmdlet.ParameterSetName -ne 'ByName') { return } foreach ($name in $Path) { # Split-Path -IsAbsolute recognises PowerShell drives (e.g. TestDrive:\x.csv), which # [System.IO.Path]::IsPathRooted reports as relative. $fullPath = if (Split-Path -Path $name -IsAbsolute) { $name } else { Join-Path -Path $Folder -ChildPath $name } if (Test-Path -LiteralPath $fullPath -PathType Leaf) { $files.Add((Get-Item -LiteralPath $fullPath)) } else { Write-Warning "Not found: $fullPath" } } } end { if ($PSCmdlet.ParameterSetName -eq 'ByTime') { # On Windows CreationTime is the download time. LastWriteTime can be older when the # browser keeps the server's timestamp, so a file counts when either one is recent. $cutoff = (Get-Date).AddHours(-$LastHours) Get-ChildItem -LiteralPath $Folder -Filter '*.csv' -File | Where-Object { $_.CreationTime -ge $cutoff -or $_.LastWriteTime -ge $cutoff } | Sort-Object -Property Name | ForEach-Object { $files.Add($_) } } if ($files.Count -eq 0) { Write-Warning 'No CSV files selected.' return } foreach ($file in $files) { $moved = 0 $status = 'Skipped' # Without -Encoding, Windows PowerShell 5.1 reads the file as ANSI. $rows = @(Import-Csv -LiteralPath $file.FullName -Encoding UTF8) $columns = if ($rows.Count -gt 0) { $rows[0].PSObject.Properties.Name } if ($rows.Count -eq 0) { Write-Warning "$($file.Name): no rows, skipped" } elseif ($columns -notcontains $IdColumn -or $columns -notcontains $SourceIdColumn) { Write-Warning "$($file.Name): column '$IdColumn' or '$SourceIdColumn' missing, skipped" } else { foreach ($row in $rows) { $id = $row.$IdColumn # An empty ID means the row was already converted (or never had one). Skipping it # keeps the Source ID of an earlier run instead of overwriting it with nothing. if ([string]::IsNullOrWhiteSpace($id)) { continue } $existing = $row.$SourceIdColumn if (-not [string]::IsNullOrWhiteSpace($existing) -and $existing -ne $id) { Write-Warning "$($file.Name): '$SourceIdColumn' '$existing' overwritten with '$id'" } $row.$SourceIdColumn = $id $row.$IdColumn = '' $moved++ } if ($moved -eq 0) { $status = 'NothingToMove' Write-Verbose "$($file.Name): nothing to move" } elseif ($PSCmdlet.ShouldProcess($file.FullName, "Move $moved ID(s) to '$SourceIdColumn'")) { # Export-Csv cannot write UTF-8 without a BOM on Windows PowerShell 5.1, and # Xurrent rejects a BOM ("Illegal quoting in line 1"). ConvertTo-Csv quotes every # field, which the import accepts, and keeps multi-line values intact. $csvLines = $rows | ConvertTo-Csv -NoTypeInformation [System.IO.File]::WriteAllLines($file.FullName, $csvLines, [System.Text.UTF8Encoding]::new($false)) $status = 'Moved' Write-Verbose "$($file.Name): $moved ID(s) moved to '$SourceIdColumn'" } else { $status = 'WhatIf' } } [PSCustomObject]@{ File = $file.FullName Moved = $moved Status = $status } } } } #EndRegion './Public/ConvertTo-XurrentCrossEnvironmentImport.ps1' 217 #Region './Public/ConvertTo-XurrentKnowledgeArticle.ps1' -1 function ConvertTo-XurrentKnowledgeArticle { <# .SYNOPSIS Converts a Markdown knowledge article file to a Xurrent import object. .DESCRIPTION Reads a *KnowledgeArticle.md file and extracts the Subject (first H1 heading), Description (## Description section), Instructions (## Instructions section), Keywords (**Keywords:** line), ID (**ID:** line), Service (**Service:** line), and Service Instances (**Service Instances:** line) into a PSCustomObject formatted for the Xurrent / 4me bulk-import CSV schema. Service metadata from the Markdown prelude is used first; the Service and ServiceInstances parameters are fallbacks. .PARAMETER File The input file to convert. Supports FileInfo, path strings, and objects that expose a FullName or Path property. Accepts pipeline input. .PARAMETER Service The Xurrent service name to write to the Service column of the import row. .PARAMETER ServiceInstances The Xurrent service instance name(s) for the Service Instances column. Optional - the Xurrent API does not require a value. .EXAMPLE Get-Item .\MyAppKnowledgeArticle.md | ConvertTo-XurrentKnowledgeArticle -Service 'techwork automator' -ServiceInstances 'techwork automator for ACS' .EXAMPLE Get-ChildItem -Recurse -Filter '*KnowledgeArticle.md' | ConvertTo-XurrentKnowledgeArticle -Service 'my svc' -ServiceInstances 'my inst' #> [CmdletBinding()] [OutputType([PSCustomObject])] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [Alias('FullName', 'Path', 'PSPath')] [object] $File, [Parameter(Mandatory = $true)] [string] $Service, [Parameter()] [string] $ServiceInstances = '' ) process { $filePath = $null if ($File -is [System.IO.FileInfo]) { $filePath = $File.FullName } elseif ($File -is [string]) { $filePath = $File } elseif ($null -ne $File.PSObject.Properties['FullName']) { $filePath = [string]$File.FullName } elseif ($null -ne $File.PSObject.Properties['Path']) { $filePath = [string]$File.Path } if (-not $filePath) { throw 'File must be a path string, FileInfo, or an object with FullName/Path.' } $resolvedFile = Get-Item -LiteralPath $filePath -ErrorAction Stop if ($resolvedFile -isnot [System.IO.FileInfo]) { throw "Input path '$filePath' is not a file." } $raw = Get-Content -Path $resolvedFile.FullName -Raw -Encoding UTF8 $subject = '' if ($raw -match '(?m)^#\s+(.+)$') { $subject = $Matches[1].Trim() } $id = '' $prelude = ($raw -split '(?m)^\s*##\s+', 2)[0] if ($prelude -match '(?m)^\*\*ID:\*\*[ \t]*(.*?)[ \t]*$') { $id = $Matches[1].Trim() } $metadataService = '' if ($prelude -match '(?m)^\*\*Service:\*\*[ \t]*(.*?)[ \t]*$') { $metadataService = $Matches[1].Trim() } $metadataServiceInstances = '' if ($prelude -match '(?m)^\*\*Service Instances:\*\*[ \t]*(.*?)[ \t]*$') { $metadataServiceInstances = $Matches[1].Trim() } $keywords = '' if ($raw -match '(?m)^\*\*Keywords:\*\*[ \t]*(.*?)[ \t]*$') { $keywords = $Matches[1].Trim() } $effectiveService = if ($metadataService) { $metadataService } else { $Service } $effectiveServiceInstances = if ($metadataServiceInstances) { $metadataServiceInstances } else { $ServiceInstances } [PSCustomObject]@{ ID = $id Source = '4me' 'Source ID' = '' Status = 'work_in_progress' Service = $effectiveService 'Service Instances' = $effectiveServiceInstances Subject = $subject Description = Get-Section -Content $raw -Heading 'Description' Instructions = Get-Section -Content $raw -Heading 'Instructions' Keywords = $keywords Template = '' } } } #EndRegion './Public/ConvertTo-XurrentKnowledgeArticle.ps1' 113 #Region './Public/Export-XurrentKnowledgeArticle.ps1' -1 function Export-XurrentKnowledgeArticle { <# .SYNOPSIS Exports Xurrent knowledge articles from Markdown files or objects to a CSV import file. .DESCRIPTION Accepts either a folder path (scanned recursively for *KnowledgeArticle.md files) or an array of pre-built article objects (e.g. the output of ConvertTo-XurrentKnowledgeArticle) and writes a CSV in the Xurrent / 4me bulk-import format. The CSV is encoded as UTF-8 without a byte-order mark (BOM); a BOM makes Xurrent reject the file with "Illegal quoting in line 1". When using the Folder parameter set, Subject is read from the first H1 heading, Description and Instructions from same-named ## sections, Keywords from a **Keywords:** line, and ID/Service/Service Instances from their metadata lines. Service and ServiceInstances can be set via parameters or loaded from a .env file (SERVICE and SERVICE_INSTANCES keys) and are used as fallbacks when the Markdown file does not provide service metadata. A warning naming the article's Subject is written for each article whose effective Service Instances value is empty after applying the fallback. When using the InputObject parameter set the objects are exported as-is; Service, ServiceInstances, and EnvFile parameters are not applicable. .PARAMETER Folder Path to the folder to scan recursively for *KnowledgeArticle.md files. .PARAMETER InputObject One or more knowledge article objects (as returned by ConvertTo-XurrentKnowledgeArticle) to export directly without scanning a folder. Accepts pipeline input. .PARAMETER Service Xurrent service name written to the Service column of every export row. Defaults to 'techwork automator'. Can also be supplied via SERVICE= in a .env file. .PARAMETER ServiceInstances Xurrent service instance name(s) for the Service Instances column. Optional - the Xurrent API does not require a value. Can be supplied via SERVICE_INSTANCES= in a .env file. The value is a fallback for articles without their own **Service Instances:** Markdown metadata; a warning naming the article's Subject is written for each article whose effective Service Instances value is empty, because that article will then be visible to every specialist covered for the service rather than scoped to one instance. .PARAMETER OutputPath Full path of the CSV file to write. When only a directory is supplied (or the path ends with a directory separator), the default file name import-knowledge_articles.csv is appended automatically. Defaults to import-knowledge_articles.csv in the current working directory. .PARAMETER EnvFile Path to a .env file supplying SERVICE and SERVICE_INSTANCES defaults. Defaults to .env in the current working directory when that file exists. .EXAMPLE Export-XurrentKnowledgeArticle -Folder .\ACS -Service 'techwork automator' -ServiceInstances 'techwork automator for ACS' .EXAMPLE Export-XurrentKnowledgeArticle -Folder .\TTTech Loads SERVICE and SERVICE_INSTANCES from .env in the current directory. .EXAMPLE Get-ChildItem -Recurse -Filter '*KnowledgeArticle.md' | ConvertTo-XurrentKnowledgeArticle -Service 'techwork automator' -ServiceInstances 'ACS' | Export-XurrentKnowledgeArticle -OutputPath .\out.csv Pipes pre-converted article objects directly into the export. .EXAMPLE $articles = ConvertTo-XurrentKnowledgeArticle -File .\MyArticleKnowledgeArticle.md -Service 'svc' -ServiceInstances 'inst' Export-XurrentKnowledgeArticle -InputObject $articles Passes an object array directly without pipeline. #> [CmdletBinding(SupportsShouldProcess = $true, DefaultParameterSetName = 'FromFolder')] param ( [Parameter(Mandatory = $true, ParameterSetName = 'FromFolder')] [ValidateScript({ Test-Path $_ -PathType Container })] [string] $Folder, [Parameter(Mandatory = $true, ParameterSetName = 'FromObjects', ValueFromPipeline = $true)] [object[]] $InputObject, [Parameter(ParameterSetName = 'FromFolder')] [string] $Service = 'techwork automator', [Parameter(ParameterSetName = 'FromFolder')] [string] $ServiceInstances = '', [Parameter()] [string] $OutputPath = (Get-Location).Path, [Parameter(ParameterSetName = 'FromFolder')] [string] $EnvFile = (Join-Path (Get-Location).Path '.env') ) begin { $rows = [System.Collections.Generic.List[object]]::new() # When only a directory is supplied (including the default current location), # append the default file name so the export always targets a CSV file. if ((Test-Path -LiteralPath $OutputPath -PathType Container) -or ($OutputPath -and ( $OutputPath.EndsWith([System.IO.Path]::DirectorySeparatorChar) -or $OutputPath.EndsWith([System.IO.Path]::AltDirectorySeparatorChar)))) { $OutputPath = Join-Path $OutputPath 'import-knowledge_articles.csv' } } process { if ($PSCmdlet.ParameterSetName -eq 'FromObjects') { foreach ($obj in $InputObject) { $rows.Add($obj) } return } if (Test-Path $EnvFile) { foreach ($line in (Get-Content $EnvFile)) { if ($line -match "^([A-Z_]+)\s*=\s*'?([^']*?)'?\s*$") { $key = $Matches[1] $val = $Matches[2] if ($key -eq 'SERVICE' -and -not $PSBoundParameters.ContainsKey('Service')) { $Service = $val } if ($key -eq 'SERVICE_INSTANCES' -and -not $PSBoundParameters.ContainsKey('ServiceInstances')) { $ServiceInstances = $val } } } } if (-not $Service) { $Service = Read-Host -Prompt "Enter SERVICE name (e.g. 'techwork automator')" } $files = Get-ChildItem -Path $Folder -Recurse -Filter '*KnowledgeArticle.md' if ($files.Count -eq 0) { Write-Warning "No *KnowledgeArticle.md files found in $Folder" return } Write-Verbose "Found $($files.Count) knowledge article(s) in $Folder" foreach ($file in $files) { Write-Verbose " Processing $($file.FullName)" $article = ConvertTo-XurrentKnowledgeArticle -File $file -Service $Service -ServiceInstances $ServiceInstances if (-not $article.'Service Instances') { Write-Warning "No Service Instances specified for article '$($article.Subject)' (service '$($article.Service)') - the article will be visible to every specialist covered for any instance of this service, rather than scoped to one specific instance." } $rows.Add($article) } } end { if ($rows.Count -eq 0) { return } if ($PSCmdlet.ShouldProcess($OutputPath, 'Export CSV')) { # Write BOM-less UTF-8. Xurrent rejects a leading BOM ("Illegal quoting in line 1"), # and Windows PowerShell 5.1 has no BOM-less option for Export-Csv -Encoding, so build # the CSV text and write it with an explicit encoding that behaves the same on PS 5.1 and 7+. $resolvedPath = $PSCmdlet.GetUnresolvedProviderPathFromPSPath($OutputPath) $utf8NoBom = [System.Text.UTF8Encoding]::new($false) $csvLines = $rows | ConvertTo-Csv -NoTypeInformation [System.IO.File]::WriteAllLines($resolvedPath, $csvLines, $utf8NoBom) Write-Verbose "Exported $($rows.Count) article(s) to $OutputPath" } } } #EndRegion './Public/Export-XurrentKnowledgeArticle.ps1' 185 #Region './Public/Import-XurrentKnowledgeArticle.ps1' -1 function Import-XurrentKnowledgeArticle { <# .SYNOPSIS Imports Xurrent knowledge articles from a CSV export file and writes Markdown files. .DESCRIPTION Reads a CSV file in the Xurrent / 4me bulk-import format and writes one *KnowledgeArticle.md file per row to the specified output folder. Subject becomes the H1 heading and the filename prefix, Description and Instructions populate their respective ## sections, and ID/Service/Service Instances/Keywords are written as metadata lines. Passes -WhatIf through to ConvertFrom-XurrentKnowledgeArticle for dry-run support. .PARAMETER CsvPath Path to the Xurrent knowledge article CSV export file to read. .PARAMETER OutputFolder The folder in which to write the Markdown files. Defaults to the current working directory. .EXAMPLE Import-XurrentKnowledgeArticle -CsvPath .\export-knowledge_articles.csv -OutputFolder .\Articles .EXAMPLE Import-XurrentKnowledgeArticle -CsvPath .\export.csv Writes Markdown files to the current working directory. #> [CmdletBinding(SupportsShouldProcess = $true)] [OutputType([System.IO.FileInfo])] param ( [Parameter(Mandatory = $true)] [ValidateScript({ Test-Path $_ -PathType Leaf })] [string] $CsvPath, [Parameter()] [ValidateScript({ Test-Path $_ -PathType Container })] [string] $OutputFolder = (Get-Location).Path ) process { $rows = @(Import-Csv -Path $CsvPath -Encoding UTF8) if ($rows.Count -eq 0) { Write-Warning "No rows found in $CsvPath" return } Write-Verbose "Found $($rows.Count) row(s) in $CsvPath" if ($PSCmdlet.ShouldProcess($CsvPath, 'Import knowledge articles to Markdown')) { foreach ($row in $rows) { Write-Verbose " Processing '$($row.Subject)'" ConvertFrom-XurrentKnowledgeArticle -InputObject $row -Path $OutputFolder } } } } #EndRegion './Public/Import-XurrentKnowledgeArticle.ps1' 66 #Region './Public/New-XurrentKnowledgeArticleCsvExample.ps1' -1 function New-XurrentKnowledgeArticleCsvExample { <# .SYNOPSIS Creates an example CSV file in the Xurrent knowledge article import format. .DESCRIPTION Writes a CSV file with all columns required by the Xurrent / 4me bulk-import schema for knowledge articles, pre-filled with one illustrative example row. Use it as a reference or starting point before populating real data via Export-KnowledgeArticle. The CSV is encoded as UTF-8 without a byte-order mark (BOM); a BOM makes Xurrent reject the file with "Illegal quoting in line 1". .PARAMETER OutputPath Full path of the CSV file to write. Defaults to example-knowledge_articles.csv in the current working directory. .EXAMPLE New-KnowledgeArticleCsvExample Creates example-knowledge_articles.csv in the current directory. .EXAMPLE New-KnowledgeArticleCsvExample -OutputPath C:\Temp\example.csv Creates the example CSV at the specified path. #> [CmdletBinding(SupportsShouldProcess = $true)] [OutputType([System.IO.FileInfo])] param ( [Parameter()] [string] $OutputPath = (Join-Path (Get-Location).Path 'example-knowledge_articles.csv') ) process { $example = [PSCustomObject]@{ ID = '' Source = '4me' 'Source ID' = '' Status = 'work_in_progress' Service = 'my service' 'Service Instances' = 'my service instance' Subject = 'Example knowledge article subject' Description = 'A brief description of the knowledge article.' Instructions = 'Step 1: Do this. Step 2: Do that.' Keywords = 'example, keyword1, keyword2' Template = '' } if ($PSCmdlet.ShouldProcess($OutputPath, 'Create example knowledge article CSV')) { # Write BOM-less UTF-8. Xurrent rejects a leading BOM ("Illegal quoting in line 1"), # and Windows PowerShell 5.1 has no BOM-less option for Export-Csv -Encoding, so build # the CSV text and write it with an explicit encoding that behaves the same on PS 5.1 and 7+. $resolvedPath = $PSCmdlet.GetUnresolvedProviderPathFromPSPath($OutputPath) $utf8NoBom = [System.Text.UTF8Encoding]::new($false) $csvLines = $example | ConvertTo-Csv -NoTypeInformation [System.IO.File]::WriteAllLines($resolvedPath, $csvLines, $utf8NoBom) Get-Item -LiteralPath $resolvedPath } } } #EndRegion './Public/New-XurrentKnowledgeArticleCsvExample.ps1' 64 #Region './Public/New-XurrentKnowledgeArticleTemplate.ps1' -1 function New-XurrentKnowledgeArticleTemplate { <# .SYNOPSIS Creates a Markdown template file for a Xurrent knowledge article. .DESCRIPTION Writes a *KnowledgeArticle.md template to the specified folder with the correct structure expected by ConvertTo-XurrentKnowledgeArticle: an H1 heading for the Subject, empty **ID:**, **Service:**, and **Service Instances:** metadata lines (left blank so the ConvertTo fallbacks still apply), a **Keywords:** line, and ## Description and ## Instructions sections. .PARAMETER Name The name of the knowledge article, used as the H1 heading and filename prefix. The file is written as <Name>KnowledgeArticle.md. .PARAMETER Path The folder in which to create the template file. Defaults to the current working directory. .EXAMPLE New-XurrentKnowledgeArticleTemplate -Name 'HowToResetPassword' Creates HowToResetPasswordKnowledgeArticle.md in the current directory. .EXAMPLE New-XurrentKnowledgeArticleTemplate -Name 'VPN Setup' -Path C:\Articles Creates 'VPN SetupKnowledgeArticle.md' in C:\Articles. #> [CmdletBinding(SupportsShouldProcess = $true)] [OutputType([System.IO.FileInfo])] param ( [Parameter(Mandatory = $true)] [string] $Name, [Parameter()] [ValidateScript({ Test-Path $_ -PathType Container })] [string] $Path = (Get-Location).Path ) process { $fileName = '{0}KnowledgeArticle.md' -f $Name $filePath = Join-Path -Path $Path -ChildPath $fileName $template = @" # $Name **ID:** **Service:** **Service Instances:** **Keywords:** keyword1, keyword2 ## Description Describe the knowledge article here. ## Instructions Provide step-by-step instructions here. "@ if ($PSCmdlet.ShouldProcess($filePath, 'Create knowledge article template')) { Set-Content -Path $filePath -Value $template -Encoding UTF8 Get-Item -Path $filePath } } } #EndRegion './Public/New-XurrentKnowledgeArticleTemplate.ps1' 76 |