diff --git a/docs/Tutorials/Elements/Table.md b/docs/Tutorials/Elements/Table.md index 91cc3a77..6499402c 100644 --- a/docs/Tutorials/Elements/Table.md +++ b/docs/Tutorials/Elements/Table.md @@ -268,3 +268,78 @@ $table | Add-PodeWebTableButton -Name 'Excel' -Icon Database -ScriptBlock { New-PodeWebContainer -Content $table ``` + +## MultiSelect + +You can allow multiple rows to be selected in a table by passing the `-MultiSelect` switch to [`New-PodeWebTable`](../../../Functions/Elements/New-PodeWebTable). This adds a checkbox column to each row. + +!!! important + `-MultiSelect` requires `-DataColumn` to be set. The value of that column for each selected row is what gets passed to button scriptblocks as the selection. + +```powershell +$table = New-PodeWebTable -Name 'Services' -DataColumn Name -MultiSelect -ScriptBlock { + foreach ($svc in (Get-Service)) { + [ordered]@{ + Name = $svc.Name + Status = "$($svc.Status)" + StartType = "$($svc.StartType)" + } + } +} + +New-PodeWebContainer -Content $table +``` + +### Acting on a Selection + +When combined with [`Add-PodeWebTableButton`](../../../Functions/Elements/Add-PodeWebTableButton), the `-DataColumn` values of all checked rows are available in the button's scriptblock via `$WebEvent.Data['Selection']` as a comma-separated string. + +```powershell +$table = New-PodeWebTable -Name 'Services' -DataColumn Name -MultiSelect -ScriptBlock { + foreach ($svc in (Get-Service)) { + [ordered]@{ + Name = $svc.Name + Status = "$($svc.Status)" + StartType = "$($svc.StartType)" + } + } +} + +$table | Add-PodeWebTableButton -Name 'StopSelected' -DisplayName 'Stop Selected' -Icon 'Stop-Circle' -WithText -ScriptBlock { + $selected = $WebEvent.Data['Selection'] -split ',' + if ($selected.Length -eq 0) { + Show-PodeWebToast -Message 'No services selected' -Title 'StopSelected' + } + else { + foreach ($svc in $selected) { + Stop-Service -Name $svc -Force -ErrorAction SilentlyContinue + } + Show-PodeWebToast -Message "Stopped $($selected.Count) service(s)" -Title 'Done' + Sync-PodeWebTable -Name 'Services' + } +} + +New-PodeWebContainer -Content $table +``` + +If you want to confirm the selection before acting on it, you can open a Modal from the button's scriptblock and pass the selected values through using [`Update-PodeWebTextbox`](../../../Functions/Actions/Update-PodeWebTextbox): + +```powershell +$table | Add-PodeWebTableButton -Name 'StopSelected' -DisplayName 'Stop Selected' -Icon 'Stop-Circle' -WithText -ScriptBlock { + $selected = $WebEvent.Data['Selection'] -split ',' + Show-PodeWebModal -Name 'ConfirmStop' -Actions @( + Update-PodeWebTextbox -Name 'SelectedServices' -Value ($selected -join "`n") + ) +} + +New-PodeWebModal -Name 'ConfirmStop' -DisplayName 'Stop Selected Services' -AsForm -Content @( + New-PodeWebTextbox -Name 'SelectedServices' -DisplayName 'Selected Services' -Multiline -ReadOnly +) -ScriptBlock { + $names = ($WebEvent.Data['SelectedServices'] -split "`n") | Where-Object { ![string]::IsNullOrWhiteSpace($_) } + foreach ($svc in $names) { + Stop-Service -Name $svc -Force -ErrorAction SilentlyContinue + } + Show-PodeWebToast -Message "Stopped $($names.Count) service(s)" -Title 'Done' + Hide-PodeWebModal +} +``` diff --git a/examples/tables.ps1 b/examples/tables.ps1 index 16caa5a8..d375d0ca 100644 --- a/examples/tables.ps1 +++ b/examples/tables.ps1 @@ -57,5 +57,63 @@ Start-PodeServer -Threads 2 { ) ) - Add-PodeWebPage -Name 'Home' -Path '/' -HomePage -Content $card1, $card2 -Title 'Tables' + $multiTable = New-PodeWebTable ` + -Name 'MultiSelect' ` + -PageSize 4 ` + -Paginate ` + -Filter ` + -SimpleFilter ` + -Compact ` + -DataColumn 'ID' ` + -MultiSelect ` + -ScriptBlock { + $allProcesses = @(Get-Process | ForEach-Object { + [ordered]@{ + Name = $_.Name + ID = $_.Id + WorkingSet = $_.WorkingSet + CPU = $_.CPU + } + }) + + $totalCount = $allProcesses.Count + $pageIndex = [int]$WebEvent.Data.PageIndex + $pageSize = [int]$WebEvent.Data.PageSize + $processes = $allProcesses[(($pageIndex - 1) * $pageSize) .. (($pageIndex * $pageSize) - 1)] + + $processes | Update-PodeWebTable -Name $ElementData.Name -PageIndex $pageIndex -TotalItemCount $totalCount + } ` + -Columns @( + Initialize-PodeWebTableColumn -Key 'Name' + Initialize-PodeWebTableColumn -Key 'ID' + Initialize-PodeWebTableColumn -Key 'WorkingSet' -Name 'Memory' + Initialize-PodeWebTableColumn -Key 'CPU' -Hide + ) + + $multiTable | Add-PodeWebTableButton -Name 'StopSelected' -DisplayName 'Stop Selected' -Icon 'delete' -WithText -ScriptBlock { + $selected = $WebEvent.Data['Selection'] -split ',' + if ($selected.Length -eq 0) { + Show-PodeWebToast -Message 'No processes selected' -Title 'MultiSelect' -Duration 3000 + } + else { + Show-PodeWebModal -Name 'StopSelected' -Actions @( + Update-PodeWebTextbox -Name 'SelectedProcesses' -Value ($selected -join "`n") + ) + } + } + + $stopModal = New-PodeWebModal -Name 'StopSelected' -DisplayName 'Stop Selected Processes' -Size 'Medium' -AsForm -Content @( + New-PodeWebTextbox -Name 'SelectedProcesses' -DisplayName 'Selected Process IDs' -Multiline -ReadOnly + ) -ScriptBlock { + $ids = ($WebEvent.Data['SelectedProcesses'] -split "`n") | Where-Object { ![string]::IsNullOrWhiteSpace($_) } + foreach ($id in $ids) { + Stop-Process -Id ([int]$id) -Force -ErrorAction SilentlyContinue -WhatIf + } + Show-PodeWebToast -Message "Stopped $($ids.Count) process(es)" -Title 'Done' -Duration 3000 + Hide-PodeWebModal + } + + $card3 = New-PodeWebCard -Name 'MultiSelect Processes' -Content $multiTable + + Add-PodeWebPage -Name 'Home' -Path '/' -HomePage -Content $card1, $card2, $card3, $stopModal -Title 'Tables' } \ No newline at end of file diff --git a/src/Public/Elements.ps1 b/src/Public/Elements.ps1 index cfb58763..258f2e81 100644 --- a/src/Public/Elements.ps1 +++ b/src/Public/Elements.ps1 @@ -2915,6 +2915,9 @@ function New-PodeWebTable { [switch] $AutoRefresh, + [switch] + $MultiSelect, + [switch] $AsCard ) @@ -2968,6 +2971,7 @@ function New-PodeWebTable { RefreshInterval = ($RefreshInterval * 1000) NoRefresh = $NoRefresh.IsPresent NoAuthentication = $NoAuthentication.IsPresent + MultiSelect = $MultiSelect.IsPresent Paging = @{ Enabled = $Paginate.IsPresent Size = $PageSize diff --git a/src/Templates/Public/scripts/templates.js b/src/Templates/Public/scripts/templates.js index 2e624091..073581c0 100644 --- a/src/Templates/Public/scripts/templates.js +++ b/src/Templates/Public/scripts/templates.js @@ -2881,6 +2881,7 @@ class PodeTable extends PodeRefreshableElement { this.dataColumn = data.DataColumn; this.clickableRows = data.Click ?? false; this.clickIsDynamic = data.ClickIsDynamic ?? false; + this.multiSelect = data.MultiSelect ?? false; this.exportable = { enabled: data.Export ?? false @@ -2943,7 +2944,8 @@ class PodeTable extends PodeRefreshableElement { + pode-sort='${this.sort.enabled}' + pode-multiselect='${this.multiSelect}'>
@@ -3326,8 +3328,59 @@ class PodeTable extends PodeRefreshableElement { this.listen(this.element.find('.pode-table-button'), 'click', function(e, target) { obj.tooltip(false, target); var url = `${obj.url}/button/${target.attr('name')}`; - sendAjaxReq(url, obj.export(), obj, true, null, null, { contentType: 'text/csv' }, $(e.currentTarget)); + var reqData, reqOpts; + if (obj.multiSelect) { + var sel = obj.getSelection().join(','); + reqData = `Selection=${encodeURIComponent(sel)}`; + reqOpts = {}; + } + else { + reqData = obj.export(); + reqOpts = { contentType: 'text/csv' }; + } + sendAjaxReq(url, reqData, obj, true, null, null, reqOpts, $(e.currentTarget)); }); + + // multiselect + if (this.multiSelect) { + // select-all header checkbox + this.listen(this.element.find('table thead th.pode-table-select-col input'), 'click', function(e, target) { + e.stopPropagation(); + obj.element.find('table tbody td.pode-table-select-col input').prop('checked', target.prop('checked')); + }, true); + + // row checkbox + this.listen(this.element.find('table tbody td.pode-table-select-col input'), 'click', function(e, target) { + e.stopPropagation(); + obj.updateSelectAllState(); + }, true); + + // clicking the select cell (outside the checkbox input) toggles the checkbox + this.listen(this.element.find('table tbody td.pode-table-select-col'), 'click', function(e, target) { + if ($(e.target).is('input')) { + return; + } + var input = target.find('input.pode-row-select'); + input.prop('checked', !input.prop('checked')); + obj.updateSelectAllState(); + }); + } + } + + getSelection() { + var selected = []; + this.element.find('table tbody td.pode-table-select-col input:checked').each(function() { + selected.push($(this).val()); + }); + return selected; + } + + updateSelectAllState() { + var total = this.element.find('table tbody td.pode-table-select-col input').length; + var checked = this.element.find('table tbody td.pode-table-select-col input:checked').length; + var allCheck = this.element.find('table thead th.pode-table-select-col input'); + allCheck.prop('checked', total > 0 && checked === total); + allCheck.prop('indeterminate', checked > 0 && checked < total); } export() { @@ -3337,9 +3390,10 @@ class PodeTable extends PodeRefreshableElement { } var csv = []; + var obj = this; rows.each((i, row) => { var data = []; - var cols = $(row).find('td, th'); + var cols = $(row).find('td:not(.pode-table-select-col), th:not(.pode-table-select-col)'); cols.each((i, col) => { data.push(col.innerText); @@ -3459,6 +3513,9 @@ class PodeTable extends PodeRefreshableElement { if (head.find('th').length == 0 && columnKeys.length > 0) { value = ''; + if (this.multiSelect) { + value += ``; + } columnKeys.forEach((key) => { value += buildTableHeader(columns[key], direction); @@ -3484,6 +3541,10 @@ class PodeTable extends PodeRefreshableElement { // table headers value = ''; + if (this.multiSelect) { + value += ``; + } + var oldHeader = null; var header = null; @@ -3517,6 +3578,11 @@ class PodeTable extends PodeRefreshableElement { value = ``; elements = []; + if (this.multiSelect) { + var rowVal = item[this.dataColumn] != null ? item[this.dataColumn] : index; + value += ``; + } + keys.forEach((key) => { header = head.find(`th[name='${key}']`); if (header.length > 0) {