bcdocker 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/ps/Helpers.ps1 ADDED
@@ -0,0 +1,505 @@
1
+ function Write-BCBanner {
2
+ param([string]$Title, [string]$Color = "Cyan")
3
+
4
+ $line = "=" * 50
5
+ Write-Host ""
6
+ Write-Host " $line" -ForegroundColor $Color
7
+ Write-Host " $Title" -ForegroundColor $Color
8
+ Write-Host " $line" -ForegroundColor $Color
9
+ Write-Host ""
10
+ }
11
+
12
+ function Write-BCStep {
13
+ param([string]$Step, [string]$Message)
14
+ Write-Host " [$Step] $Message" -ForegroundColor Yellow
15
+ }
16
+
17
+ function Write-BCInfo {
18
+ param([string]$Message)
19
+ Write-Host " $Message" -ForegroundColor Gray
20
+ }
21
+
22
+ function Write-BCSuccess {
23
+ param([string]$Message)
24
+ Write-Host " $Message" -ForegroundColor Green
25
+ }
26
+
27
+ function Write-BCError {
28
+ param([string]$Message)
29
+ Write-Host " $Message" -ForegroundColor Red
30
+ }
31
+
32
+ function Write-BCProperty {
33
+ param([string]$Label, [string]$Value, [string]$ValueColor = "White")
34
+ Write-Host " $($Label.PadRight(18)): " -ForegroundColor Gray -NoNewline
35
+ Write-Host $Value -ForegroundColor $ValueColor
36
+ }
37
+
38
+ function Assert-DockerReady {
39
+ try {
40
+ $null = docker version --format '{{.Server.Version}}' 2>$null
41
+ }
42
+ catch {
43
+ Write-BCError "Docker is not running or not installed."
44
+ Write-BCError "Install Docker Desktop and switch to Windows containers."
45
+ return $false
46
+ }
47
+
48
+ $osType = docker info --format '{{.OSType}}' 2>$null
49
+ if ($osType -ne "windows") {
50
+ Write-BCError "Docker is in Linux containers mode."
51
+ Write-BCInfo "Right-click Docker Desktop tray icon -> 'Switch to Windows containers...'"
52
+ return $false
53
+ }
54
+ return $true
55
+ }
56
+
57
+ function Assert-BcContainerHelper {
58
+ $module = Get-Module -ListAvailable -Name BcContainerHelper |
59
+ Sort-Object Version -Descending |
60
+ Select-Object -First 1
61
+
62
+ if (-not $module) {
63
+ Write-BCStep "INIT" "Installing BcContainerHelper..."
64
+ Install-Module BcContainerHelper -Force -AllowClobber -Scope CurrentUser
65
+ }
66
+
67
+ Import-Module BcContainerHelper -DisableNameChecking -ErrorAction Stop
68
+ return $true
69
+ }
70
+
71
+ function Select-BCContainer {
72
+ [CmdletBinding()]
73
+ param(
74
+ [string]$Title = "Select a container",
75
+ [switch]$AllowMultiple
76
+ )
77
+
78
+ if (-not (Assert-BcContainerHelper)) { return $null }
79
+
80
+ $containers = Get-BcContainers
81
+ if ($containers.Count -eq 0) {
82
+ Write-BCError "No BC containers found."
83
+ return $null
84
+ }
85
+
86
+ if ($AllowMultiple) {
87
+ return $containers | Out-GridView -PassThru -Title $Title
88
+ }
89
+ else {
90
+ $selected = $containers | Out-GridView -PassThru -Title $Title
91
+ if ($selected -is [array]) {
92
+ Write-BCError "Please select only one container."
93
+ return $null
94
+ }
95
+ return $selected
96
+ }
97
+ }
98
+
99
+ function Get-BCCredential {
100
+ param(
101
+ [string]$UserName = "admin",
102
+ [string]$Password = "P@ssw0rd!"
103
+ )
104
+ $secure = ConvertTo-SecureString $Password -AsPlainText -Force
105
+ return [PSCredential]::new($UserName, $secure)
106
+ }
107
+
108
+ function Select-AppFiles {
109
+ param([string]$Title = "Select .app file(s)")
110
+
111
+ Add-Type -AssemblyName System.Windows.Forms
112
+ $dialog = New-Object System.Windows.Forms.OpenFileDialog
113
+ $dialog.Filter = "AL App files (*.app)|*.app|All files (*.*)|*.*"
114
+ $dialog.Multiselect = $true
115
+ $dialog.Title = $Title
116
+
117
+ if ($dialog.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {
118
+ return $dialog.FileNames
119
+ }
120
+ return $null
121
+ }
122
+
123
+ function Add-DarkScrollStyle {
124
+ param([System.Windows.Window]$Window)
125
+
126
+ $rdXaml = @"
127
+ <ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
128
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
129
+ <Style TargetType="{x:Type ScrollBar}">
130
+ <Setter Property="Background" Value="Transparent"/>
131
+ <Setter Property="Width" Value="10"/>
132
+ <Setter Property="MinWidth" Value="10"/>
133
+ <Setter Property="Template">
134
+ <Setter.Value>
135
+ <ControlTemplate TargetType="{x:Type ScrollBar}">
136
+ <Grid Background="Transparent" Width="10">
137
+ <Track x:Name="PART_Track" IsDirectionReversed="True">
138
+ <Track.DecreaseRepeatButton>
139
+ <RepeatButton Command="ScrollBar.PageUpCommand" Focusable="False">
140
+ <RepeatButton.Template>
141
+ <ControlTemplate><Border Background="Transparent"/></ControlTemplate>
142
+ </RepeatButton.Template>
143
+ </RepeatButton>
144
+ </Track.DecreaseRepeatButton>
145
+ <Track.Thumb>
146
+ <Thumb x:Name="thumb">
147
+ <Thumb.Template>
148
+ <ControlTemplate TargetType="{x:Type Thumb}">
149
+ <Border x:Name="tb" Background="#404040" CornerRadius="5" Margin="2,0">
150
+ <Border.Triggers>
151
+ <EventTrigger RoutedEvent="MouseEnter">
152
+ <BeginStoryboard>
153
+ <Storyboard>
154
+ <ColorAnimation Storyboard.TargetName="tb"
155
+ Storyboard.TargetProperty="(Border.Background).(SolidColorBrush.Color)"
156
+ To="#686868" Duration="0:0:0.15"/>
157
+ </Storyboard>
158
+ </BeginStoryboard>
159
+ </EventTrigger>
160
+ <EventTrigger RoutedEvent="MouseLeave">
161
+ <BeginStoryboard>
162
+ <Storyboard>
163
+ <ColorAnimation Storyboard.TargetName="tb"
164
+ Storyboard.TargetProperty="(Border.Background).(SolidColorBrush.Color)"
165
+ To="#404040" Duration="0:0:0.3"/>
166
+ </Storyboard>
167
+ </BeginStoryboard>
168
+ </EventTrigger>
169
+ </Border.Triggers>
170
+ </Border>
171
+ </ControlTemplate>
172
+ </Thumb.Template>
173
+ </Thumb>
174
+ </Track.Thumb>
175
+ <Track.IncreaseRepeatButton>
176
+ <RepeatButton Command="ScrollBar.PageDownCommand" Focusable="False">
177
+ <RepeatButton.Template>
178
+ <ControlTemplate><Border Background="Transparent"/></ControlTemplate>
179
+ </RepeatButton.Template>
180
+ </RepeatButton>
181
+ </Track.IncreaseRepeatButton>
182
+ </Track>
183
+ </Grid>
184
+ </ControlTemplate>
185
+ </Setter.Value>
186
+ </Setter>
187
+ </Style>
188
+ </ResourceDictionary>
189
+ "@
190
+ $rd = [System.Windows.Markup.XamlReader]::Load(
191
+ [System.Xml.XmlReader]::Create([System.IO.StringReader]::new($rdXaml))
192
+ )
193
+ $Window.Resources.MergedDictionaries.Add($rd)
194
+ }
195
+
196
+ function Select-Folder {
197
+ param([string]$Description = "Select folder")
198
+
199
+ Add-Type -AssemblyName System.Windows.Forms
200
+ $dialog = New-Object System.Windows.Forms.FolderBrowserDialog
201
+ $dialog.Description = $Description
202
+
203
+ if ($dialog.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {
204
+ return $dialog.SelectedPath
205
+ }
206
+ return $null
207
+ }
208
+
209
+ function Show-BCDContainerForm {
210
+ Add-Type -AssemblyName PresentationFramework
211
+
212
+ $bc = [System.Windows.Media.BrushConverter]::new()
213
+ $bgWindow = $bc.ConvertFrom("#252526")
214
+ $bgField = $bc.ConvertFrom("#3c3c3c")
215
+ $fgText = $bc.ConvertFrom("#cccccc")
216
+ $fgValue = $bc.ConvertFrom("#e0e0e0")
217
+ $fgDim = $bc.ConvertFrom("#5a5a5a")
218
+ $fgHint = $bc.ConvertFrom("#666666")
219
+ $brField = $bc.ConvertFrom("#4a4a4a")
220
+ $brFocus = $bc.ConvertFrom("#0078d4")
221
+
222
+ $window = New-Object System.Windows.Window
223
+ $window.Title = "BCDocker"
224
+ $window.Width = 580
225
+ $window.Height = 680
226
+ $window.WindowStartupLocation = "CenterScreen"
227
+ $window.ResizeMode = "NoResize"
228
+ $window.Background = $bgWindow
229
+ $window.FontFamily = New-Object System.Windows.Media.FontFamily("Segoe UI")
230
+ $window.FontSize = 13
231
+
232
+ $root = New-Object System.Windows.Controls.DockPanel
233
+ $root.Margin = [System.Windows.Thickness]::new(32, 24, 32, 24)
234
+
235
+ # Title
236
+ $titlePanel = New-Object System.Windows.Controls.StackPanel
237
+ [System.Windows.Controls.DockPanel]::SetDock($titlePanel, "Top")
238
+ $titlePanel.Margin = [System.Windows.Thickness]::new(0, 0, 0, 8)
239
+ $t1 = New-Object System.Windows.Controls.TextBlock; $t1.Text = "Create Container"; $t1.FontSize = 20; $t1.FontWeight = "SemiBold"; $t1.Foreground = $fgValue
240
+ $t2 = New-Object System.Windows.Controls.TextBlock; $t2.Text = "Configure a new Business Central Docker sandbox"; $t2.FontSize = 11.5; $t2.Foreground = $fgDim; $t2.Margin = [System.Windows.Thickness]::new(0,3,0,0)
241
+ $titlePanel.Children.Add($t1) | Out-Null
242
+ $titlePanel.Children.Add($t2) | Out-Null
243
+ $root.Children.Add($titlePanel) | Out-Null
244
+
245
+ # Buttons
246
+ $btnPanel = New-Object System.Windows.Controls.StackPanel
247
+ $btnPanel.Orientation = "Horizontal"
248
+ $btnPanel.HorizontalAlignment = "Right"
249
+ $btnPanel.Margin = [System.Windows.Thickness]::new(0, 16, 0, 0)
250
+ [System.Windows.Controls.DockPanel]::SetDock($btnPanel, "Bottom")
251
+ $btnCancel = New-Object System.Windows.Controls.Button; $btnCancel.Content = "Cancel"; $btnCancel.Padding = [System.Windows.Thickness]::new(22,7,22,7)
252
+ $btnCancel.Background = $bc.ConvertFrom("#333333"); $btnCancel.Foreground = $fgText; $btnCancel.BorderBrush = $bc.ConvertFrom("#505050"); $btnCancel.Margin = [System.Windows.Thickness]::new(0,0,10,0)
253
+ $btnCreate = New-Object System.Windows.Controls.Button; $btnCreate.Content = "Create Container"; $btnCreate.Padding = [System.Windows.Thickness]::new(22,7,22,7)
254
+ $btnCreate.Background = $brFocus; $btnCreate.Foreground = [System.Windows.Media.Brushes]::White; $btnCreate.BorderThickness = [System.Windows.Thickness]::new(0); $btnCreate.FontWeight = "SemiBold"; $btnCreate.IsDefault = $true
255
+ $btnPanel.Children.Add($btnCancel) | Out-Null; $btnPanel.Children.Add($btnCreate) | Out-Null
256
+ $root.Children.Add($btnPanel) | Out-Null
257
+
258
+ # Form body
259
+ $scroll = New-Object System.Windows.Controls.ScrollViewer; $scroll.VerticalScrollBarVisibility = "Auto"
260
+ $formBody = New-Object System.Windows.Controls.StackPanel
261
+
262
+ $addSection = {
263
+ param($text, $topMargin)
264
+ $tb = New-Object System.Windows.Controls.TextBlock; $tb.Text = $text; $tb.FontSize = 10.5; $tb.FontWeight = "SemiBold"; $tb.Foreground = $fgDim
265
+ $tb.Margin = [System.Windows.Thickness]::new(0, $topMargin, 0, 8)
266
+ $formBody.Children.Add($tb) | Out-Null
267
+ }
268
+
269
+ $fields = @{}
270
+ $addField = {
271
+ param($label, $key, $default, $hint)
272
+ $row = New-Object System.Windows.Controls.DockPanel; $row.Margin = [System.Windows.Thickness]::new(0,0,0,2)
273
+ $lbl = New-Object System.Windows.Controls.TextBlock; $lbl.Text = $label; $lbl.Width = 120; $lbl.Foreground = $fgText; $lbl.VerticalAlignment = "Center"
274
+ $tb = New-Object System.Windows.Controls.TextBox; $tb.Text = $default; $tb.Background = $bgField; $tb.Foreground = $fgValue
275
+ $tb.BorderBrush = $brField; $tb.Padding = [System.Windows.Thickness]::new(8,6,8,6); $tb.CaretBrush = $fgValue
276
+ $tb.Add_GotFocus({ $this.BorderBrush = [System.Windows.Media.BrushConverter]::new().ConvertFrom("#0078d4") })
277
+ $tb.Add_LostFocus({ $this.BorderBrush = [System.Windows.Media.BrushConverter]::new().ConvertFrom("#4a4a4a") })
278
+ [System.Windows.Controls.DockPanel]::SetDock($lbl, "Left")
279
+ $row.Children.Add($lbl) | Out-Null; $row.Children.Add($tb) | Out-Null
280
+ $formBody.Children.Add($row) | Out-Null
281
+ if ($hint) {
282
+ $h = New-Object System.Windows.Controls.TextBlock; $h.Text = $hint; $h.FontSize = 11; $h.Foreground = $fgHint
283
+ $h.Margin = [System.Windows.Thickness]::new(120, 0, 0, 6)
284
+ $formBody.Children.Add($h) | Out-Null
285
+ } else {
286
+ $sp = New-Object System.Windows.Controls.Border; $sp.Height = 4
287
+ $formBody.Children.Add($sp) | Out-Null
288
+ }
289
+ $fields[$key] = $tb
290
+ }
291
+
292
+ # --- Build form ---
293
+ & $addSection "CONTAINER" 0
294
+ & $addField "Name" "ContainerName" "bcsandbox" $null
295
+ & $addField "Version" "Version" "sandbox" "sandbox, onprem, or specific version (e.g. 26.0)"
296
+ & $addField "Country" "Country" "us" "us, w1, gb, nl, de, dk, fr, it, es, ca, au"
297
+
298
+ & $addSection "CREDENTIALS" 12
299
+ & $addField "Username" "UserName" "admin" $null
300
+ & $addField "Password" "Password" "P@ssw0rd!" $null
301
+
302
+ & $addSection "CONFIGURATION" 12
303
+ & $addField "Memory" "MemoryLimit" "8G" "4G, 8G, 12G, or 16G"
304
+ & $addField "Isolation" "Isolation" "hyperv" "hyperv (Win10/11) or process (Server)"
305
+ & $addField "Test Toolkit" "TestToolkit" "Libraries" "None, Libraries (faster), or Full (all MS tests)"
306
+
307
+ & $addSection "OPTIONS" 12
308
+
309
+ $chkCDN = New-Object System.Windows.Controls.CheckBox; $chkCDN.Foreground = $fgText
310
+ $chkCDN.Content = " Bypass CDN (blob storage - fixes Win11 25H2 failures)"; $chkCDN.Margin = [System.Windows.Thickness]::new(0,0,0,10)
311
+ $formBody.Children.Add($chkCDN) | Out-Null
312
+
313
+ $licRow = New-Object System.Windows.Controls.DockPanel; $licRow.Margin = [System.Windows.Thickness]::new(0,0,0,6)
314
+ $licLbl = New-Object System.Windows.Controls.TextBlock; $licLbl.Text = "License File"; $licLbl.Width = 120; $licLbl.Foreground = $fgText; $licLbl.VerticalAlignment = "Center"
315
+ $licBrowse = New-Object System.Windows.Controls.Button; $licBrowse.Content = "Browse"; $licBrowse.Padding = [System.Windows.Thickness]::new(10,4,10,4)
316
+ $licBrowse.Background = $bc.ConvertFrom("#333333"); $licBrowse.Foreground = $fgText; $licBrowse.BorderBrush = $bc.ConvertFrom("#505050"); $licBrowse.Width = 70
317
+ $licTb = New-Object System.Windows.Controls.TextBox; $licTb.Background = $bgField; $licTb.Foreground = $fgValue; $licTb.BorderBrush = $brField
318
+ $licTb.Padding = [System.Windows.Thickness]::new(8,6,8,6); $licTb.CaretBrush = $fgValue; $licTb.Margin = [System.Windows.Thickness]::new(0,0,6,0)
319
+ [System.Windows.Controls.DockPanel]::SetDock($licLbl, "Left")
320
+ [System.Windows.Controls.DockPanel]::SetDock($licBrowse, "Right")
321
+ $licRow.Children.Add($licLbl) | Out-Null; $licRow.Children.Add($licBrowse) | Out-Null; $licRow.Children.Add($licTb) | Out-Null
322
+ $formBody.Children.Add($licRow) | Out-Null
323
+ $licHint = New-Object System.Windows.Controls.TextBlock; $licHint.Text = "Optional - path or URL to .bclicense / .flf"; $licHint.FontSize = 11; $licHint.Foreground = $fgHint
324
+ $licHint.Margin = [System.Windows.Thickness]::new(120, 0, 0, 0)
325
+ $formBody.Children.Add($licHint) | Out-Null
326
+
327
+ $scroll.Content = $formBody
328
+ $root.Children.Add($scroll) | Out-Null
329
+ $window.Content = $root
330
+ Add-DarkScrollStyle -Window $window
331
+
332
+ $script:formResult = $null
333
+
334
+ $licBrowse.Add_Click({
335
+ Add-Type -AssemblyName System.Windows.Forms
336
+ $dlg = New-Object System.Windows.Forms.OpenFileDialog
337
+ $dlg.Filter = "BC License (*.bclicense;*.flf)|*.bclicense;*.flf|All files (*.*)|*.*"
338
+ if ($dlg.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) { $licTb.Text = $dlg.FileName }
339
+ })
340
+
341
+ $btnCreate.Add_Click({
342
+ if ([string]::IsNullOrWhiteSpace($fields["ContainerName"].Text)) {
343
+ [System.Windows.MessageBox]::Show("Container name is required.", "Validation", "OK", "Warning"); return
344
+ }
345
+ $script:formResult = @{
346
+ ContainerName = $fields["ContainerName"].Text.Trim()
347
+ Version = $fields["Version"].Text.Trim()
348
+ Country = $fields["Country"].Text.Trim()
349
+ UserName = $fields["UserName"].Text.Trim()
350
+ Password = $fields["Password"].Text
351
+ MemoryLimit = $fields["MemoryLimit"].Text.Trim()
352
+ Isolation = $fields["Isolation"].Text.Trim()
353
+ TestToolkit = $fields["TestToolkit"].Text.Trim()
354
+ BypassCDN = $chkCDN.IsChecked
355
+ LicenseFile = $licTb.Text.Trim()
356
+ }
357
+ $window.Close()
358
+ })
359
+ $btnCancel.Add_Click({ $window.Close() })
360
+
361
+ $null = $window.ShowDialog()
362
+ return $script:formResult
363
+ }
364
+
365
+ function Show-BCDMainMenu {
366
+ Add-Type -AssemblyName PresentationFramework
367
+
368
+ $menuItems = @(
369
+ @{ Tag = "create"; Icon = [char]0xE710; Label = "Create new container" }
370
+ @{ Tag = "list"; Icon = [char]0xE8FD; Label = "List containers" }
371
+ @{ Tag = "info"; Icon = [char]0xE946; Label = "Container info" }
372
+ @{ Tag = "start"; Icon = [char]0xE768; Label = "Start container" }
373
+ @{ Tag = "stop"; Icon = [char]0xE71A; Label = "Stop container" }
374
+ @{ Tag = "restart"; Icon = [char]0xE72C; Label = "Restart container" }
375
+ @{ Tag = "remove"; Icon = [char]0xE74D; Label = "Remove container" }
376
+ @{ Tag = "webclient"; Icon = [char]0xE774; Label = "Open Web Client" }
377
+ @{ Tag = "_sep1" }
378
+ @{ Tag = "apps"; Icon = [char]0xE8F1; Label = "List apps" }
379
+ @{ Tag = "install"; Icon = [char]0xE8E5; Label = "Install .app file" }
380
+ @{ Tag = "uninstall"; Icon = [char]0xE738; Label = "Uninstall app" }
381
+ @{ Tag = "publish"; Icon = [char]0xE74E; Label = "Compile and publish" }
382
+ @{ Tag = "_sep2" }
383
+ @{ Tag = "toolkit"; Icon = [char]0xE9D5; Label = "Import Test Toolkit" }
384
+ @{ Tag = "license"; Icon = [char]0xEB95; Label = "Import license" }
385
+ @{ Tag = "tests"; Icon = [char]0xE9D9; Label = "Run tests" }
386
+ )
387
+
388
+ $xaml = @"
389
+ <Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
390
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
391
+ Title="BCDocker" Width="400" Height="580"
392
+ WindowStartupLocation="CenterScreen" ResizeMode="NoResize"
393
+ Background="#252526" FontFamily="Segoe UI">
394
+ <DockPanel Margin="24,22,24,22">
395
+ <StackPanel DockPanel.Dock="Top" Margin="0,0,0,16">
396
+ <TextBlock Text="BCDocker" FontSize="20" FontWeight="SemiBold" Foreground="#cccccc"/>
397
+ <TextBlock Text="Business Central Container Management" FontSize="11.5" Foreground="#555555" Margin="0,3,0,0"/>
398
+ </StackPanel>
399
+ <ScrollViewer VerticalScrollBarVisibility="Auto" Focusable="False">
400
+ <StackPanel x:Name="spMenu"/>
401
+ </ScrollViewer>
402
+ </DockPanel>
403
+ </Window>
404
+ "@
405
+
406
+ $reader = [System.Xml.XmlReader]::Create([System.IO.StringReader]::new($xaml))
407
+ $window = [System.Windows.Markup.XamlReader]::Load($reader)
408
+ $spMenu = $window.FindName("spMenu")
409
+ Add-DarkScrollStyle -Window $window
410
+
411
+ $bc = [System.Windows.Media.BrushConverter]::new()
412
+
413
+ # Custom button template that removes all default Windows chrome
414
+ $templateXaml = @"
415
+ <ControlTemplate xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
416
+ TargetType="Button">
417
+ <Border Background="{TemplateBinding Background}"
418
+ BorderBrush="{TemplateBinding BorderBrush}"
419
+ BorderThickness="{TemplateBinding BorderThickness}"
420
+ Padding="{TemplateBinding Padding}"
421
+ SnapsToDevicePixels="True">
422
+ <ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
423
+ VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
424
+ </Border>
425
+ </ControlTemplate>
426
+ "@
427
+ $templateReader = [System.Xml.XmlReader]::Create([System.IO.StringReader]::new($templateXaml))
428
+ $btnTemplate = [System.Windows.Markup.XamlReader]::Load($templateReader)
429
+
430
+ $script:menuChoice = $null
431
+
432
+ $sections = @("CONTAINERS", "APPS", "TESTS AND LICENSE")
433
+ $sectionIdx = 0
434
+
435
+ foreach ($item in $menuItems) {
436
+ if ($item.Tag.StartsWith("_sep")) {
437
+ $sectionIdx++
438
+ continue
439
+ }
440
+
441
+ if (($sectionIdx -eq 0 -and $item.Tag -eq "create") -or
442
+ ($sectionIdx -eq 1 -and $item.Tag -eq "apps") -or
443
+ ($sectionIdx -eq 2 -and $item.Tag -eq "toolkit")) {
444
+
445
+ $header = New-Object System.Windows.Controls.TextBlock
446
+ $header.Text = $sections[$sectionIdx]
447
+ $header.FontSize = 10.5
448
+ $header.FontWeight = "SemiBold"
449
+ $header.Foreground = $bc.ConvertFrom("#5a5a5a")
450
+ $header.Margin = [System.Windows.Thickness]::new(2, $(if ($sectionIdx -eq 0) { 0 } else { 16 }), 0, 6)
451
+ $spMenu.Children.Add($header) | Out-Null
452
+ }
453
+
454
+ $btn = New-Object System.Windows.Controls.Button
455
+ $btn.Tag = $item.Tag
456
+ $btn.Template = $btnTemplate
457
+ $btn.Background = $bc.ConvertFrom("#2d2d2d")
458
+ $btn.BorderBrush = $bc.ConvertFrom("#2d2d2d")
459
+ $btn.BorderThickness = [System.Windows.Thickness]::new(1)
460
+ $btn.HorizontalContentAlignment = "Left"
461
+ $btn.Padding = [System.Windows.Thickness]::new(10, 8, 10, 8)
462
+ $btn.Margin = [System.Windows.Thickness]::new(0, 1, 0, 1)
463
+ $btn.Cursor = [System.Windows.Input.Cursors]::Hand
464
+ $btn.Focusable = $false
465
+
466
+ $sp = New-Object System.Windows.Controls.StackPanel
467
+ $sp.Orientation = "Horizontal"
468
+
469
+ $iconBlock = New-Object System.Windows.Controls.TextBlock
470
+ $iconBlock.Text = [string]$item.Icon
471
+ $iconBlock.FontFamily = New-Object System.Windows.Media.FontFamily("Segoe MDL2 Assets")
472
+ $iconBlock.FontSize = 14
473
+ $iconBlock.Foreground = $bc.ConvertFrom("#858585")
474
+ $iconBlock.VerticalAlignment = "Center"
475
+ $iconBlock.Width = 24
476
+
477
+ $labelBlock = New-Object System.Windows.Controls.TextBlock
478
+ $labelBlock.Text = $item.Label
479
+ $labelBlock.FontSize = 13
480
+ $labelBlock.Foreground = $bc.ConvertFrom("#cccccc")
481
+ $labelBlock.VerticalAlignment = "Center"
482
+
483
+ $sp.Children.Add($iconBlock) | Out-Null
484
+ $sp.Children.Add($labelBlock) | Out-Null
485
+ $btn.Content = $sp
486
+
487
+ $btn.Add_MouseEnter({
488
+ $this.Background = [System.Windows.Media.BrushConverter]::new().ConvertFrom("#37373d")
489
+ $this.BorderBrush = [System.Windows.Media.BrushConverter]::new().ConvertFrom("#37373d")
490
+ })
491
+ $btn.Add_MouseLeave({
492
+ $this.Background = [System.Windows.Media.BrushConverter]::new().ConvertFrom("#2d2d2d")
493
+ $this.BorderBrush = [System.Windows.Media.BrushConverter]::new().ConvertFrom("#2d2d2d")
494
+ })
495
+ $btn.Add_Click({
496
+ $script:menuChoice = $this.Tag
497
+ $window.Close()
498
+ })
499
+
500
+ $spMenu.Children.Add($btn) | Out-Null
501
+ }
502
+
503
+ $null = $window.ShowDialog()
504
+ return $script:menuChoice
505
+ }
package/ps/Tests.ps1 ADDED
@@ -0,0 +1,144 @@
1
+ function Invoke-BCDTests {
2
+ <#
3
+ .SYNOPSIS
4
+ Runs AL tests inside a BC Docker container.
5
+
6
+ .PARAMETER ContainerName
7
+ Target container. Default: "bcsandbox"
8
+
9
+ .PARAMETER TestCodeunitId
10
+ Run a single test codeunit by ID. Omit to run all.
11
+
12
+ .PARAMETER TestFunctionName
13
+ Run a single test function. Requires -TestCodeunitId.
14
+
15
+ .PARAMETER AppProjectFolder
16
+ Path to an AL test app project. Compiles, publishes, and runs tests.
17
+
18
+ .PARAMETER Credential
19
+ Container credentials.
20
+
21
+ .PARAMETER TestResultsFile
22
+ Path for XUnit results XML. Default: ".\TestResults.xml"
23
+
24
+ .PARAMETER ExtensionId
25
+ Run tests from a specific extension only.
26
+
27
+ .PARAMETER DisabledTests
28
+ Array of hashtables to skip: @(@{ codeunitId = 134000; method = "TestToSkip" })
29
+ #>
30
+ [CmdletBinding()]
31
+ param(
32
+ [string]$ContainerName = "bcsandbox",
33
+ [int]$TestCodeunitId = 0,
34
+ [string]$TestFunctionName = "",
35
+ [string]$AppProjectFolder = "",
36
+ [PSCredential]$Credential,
37
+ [string]$TestResultsFile = ".\TestResults.xml",
38
+ [string]$ExtensionId = "",
39
+ [hashtable[]]$DisabledTests = @()
40
+ )
41
+
42
+ if (-not (Assert-BcContainerHelper)) { return }
43
+
44
+ if (-not $Credential) {
45
+ $Credential = Get-BCCredential
46
+ }
47
+
48
+ Write-BCBanner "BC Test Runner"
49
+ Write-BCProperty "Container" $ContainerName
50
+ Write-BCProperty "Results" $TestResultsFile
51
+
52
+ if ($AppProjectFolder -and (Test-Path $AppProjectFolder)) {
53
+ Write-BCStep "1/3" "Compiling test app from: $AppProjectFolder"
54
+ $appFile = Compile-AppInBcContainer `
55
+ -containerName $ContainerName `
56
+ -appProjectFolder $AppProjectFolder `
57
+ -credential $Credential `
58
+ -UpdateSymbols
59
+
60
+ Write-BCSuccess "Compiled: $appFile"
61
+
62
+ Write-BCStep "2/3" "Publishing..."
63
+ Publish-BcContainerApp `
64
+ -containerName $ContainerName `
65
+ -appFile $appFile `
66
+ -credential $Credential `
67
+ -install -sync `
68
+ -syncMode ForceSync `
69
+ -skipVerification `
70
+ -useDevEndpoint
71
+
72
+ Write-BCSuccess "Test app installed."
73
+
74
+ if (-not $ExtensionId) {
75
+ $appJson = Get-Content (Join-Path $AppProjectFolder "app.json") | ConvertFrom-Json
76
+ $ExtensionId = $appJson.id
77
+ }
78
+ }
79
+ else {
80
+ Write-BCStep "1/3" "No test app folder — running installed tests."
81
+ }
82
+
83
+ Write-BCStep "3/3" "Running tests..."
84
+ $runParams = @{
85
+ containerName = $ContainerName
86
+ credential = $Credential
87
+ detailed = $true
88
+ returnTrueIfAllPassed = $true
89
+ }
90
+
91
+ if ($TestCodeunitId -gt 0) { $runParams.testCodeunit = $TestCodeunitId }
92
+ if ($TestFunctionName) { $runParams.testFunction = $TestFunctionName }
93
+ if ($ExtensionId) { $runParams.extensionId = $ExtensionId }
94
+ if ($DisabledTests.Count -gt 0) { $runParams.disabledTests = $DisabledTests }
95
+ if ($TestResultsFile) { $runParams.XUnitResultFileName = $TestResultsFile }
96
+
97
+ $startTime = Get-Date
98
+ $allPassed = Run-TestsInBcContainer @runParams
99
+ $elapsed = (Get-Date) - $startTime
100
+ $elapsedStr = "{0:mm\:ss}" -f $elapsed
101
+
102
+ if ($allPassed) {
103
+ Write-BCBanner "ALL TESTS PASSED ($elapsedStr)" "Green"
104
+ }
105
+ else {
106
+ Write-BCBanner "SOME TESTS FAILED ($elapsedStr)" "Red"
107
+ }
108
+
109
+ if ($TestResultsFile -and (Test-Path $TestResultsFile)) {
110
+ [xml]$results = Get-Content $TestResultsFile
111
+ $total = $passed = $failed = $skipped = 0
112
+
113
+ foreach ($assembly in $results.assemblies.assembly) {
114
+ $total += [int]$assembly.total
115
+ $passed += [int]$assembly.passed
116
+ $failed += [int]$assembly.failed
117
+ $skipped += [int]$assembly.skipped
118
+ }
119
+
120
+ Write-BCProperty "Total" $total
121
+ Write-BCProperty "Passed" $passed "Green"
122
+ Write-BCProperty "Failed" $failed $(if ($failed -gt 0) { "Red" } else { "White" })
123
+ Write-BCProperty "Skipped" $skipped "Yellow"
124
+
125
+ if ($failed -gt 0) {
126
+ Write-Host ""
127
+ Write-BCError "Failed tests:"
128
+ foreach ($assembly in $results.assemblies.assembly) {
129
+ foreach ($collection in $assembly.collection) {
130
+ foreach ($test in $collection.test) {
131
+ if ($test.result -eq "Fail") {
132
+ Write-Host " - $($test.name)" -ForegroundColor Red
133
+ if ($test.failure.message) {
134
+ Write-Host " $($test.failure.message.'#text')" -ForegroundColor DarkRed
135
+ }
136
+ }
137
+ }
138
+ }
139
+ }
140
+ }
141
+ }
142
+
143
+ return $allPassed
144
+ }