<# .SYNOPSIS Идемпотентно создаёт списки и библиотеки форм портала ct-forms-portal на целевом сайте (/sites/sed) по манифестам provisioning.json. .DESCRIPTION Читает ВСЕ манифесты provisioning/manifests/*.provisioning.json (снятые Export-ListSchema.ps1) и в ДВА прохода: Проход A — создаёт все списки/библиотеки (если ещё нет) по всем манифестам. Проход B — обеспечивает колонки на каждом списке. Два прохода нужны, чтобы к моменту создания Lookup-колонки её целевой список уже существовал (даже если он объявлен в другом манифесте — напр. общий «Поставщики»). Литеральные имена: имя списка берётся как есть из манифеста (никаких конвенций «— данные/— согласование», в отличие от request-portal). Поддержаны обычные списки (GenericList) и библиотеки документов (DocumentLibrary). Колонки создаются прямо НА СПИСКЕ (без общих столбцов сайта) — у форм CT/OID/KPS имена колонок совпадают между списками, а Choice-наборы могут отличаться, поэтому общий эталон сайта тут только мешал бы. Повторный запуск безопасен: существующие списки/колонки не пересоздаются; для Choice синхронизируются варианты. Вход — по сертификату «от имени приложения» (app-only, Sites.Selected на /sites/sed). .PARAMETER SiteUrl Целевой сайт, например https://tnsukz.sharepoint.com/sites/sed .PARAMETER ClientId Client Id приложения-провижинера (Sites.Selected). .PARAMETER Tenant Тенант, например tnsukz.onmicrosoft.com .PARAMETER Thumbprint Отпечаток сертификата в хранилище CurrentUser\My. .PARAMETER ManifestsDir Папка с манифестами. По умолчанию — provisioning/manifests рядом со скриптом. .PARAMETER Only Провижионить только манифесты указанных форм (по полю form). По умолчанию — все. .PARAMETER Lists Провижионить только списки с указанными названиями. Нужен для добавления нового списка на рабочий сайт: полный прогон перезаписывает варианты Choice у живых списков значениями из манифеста, а манифест мог отстать от живого списка. .EXAMPLE ./Provision-Lists.ps1 -SiteUrl "https://tnsukz.sharepoint.com/sites/sed" ` -ClientId "" -Tenant "tnsukz.onmicrosoft.com" -Thumbprint "" .EXAMPLE ./Provision-Lists.ps1 -SiteUrl "https://tnsukz.sharepoint.com/sites/sed" ` -ClientId "" -Tenant "tnsukz.onmicrosoft.com" -Thumbprint "" ` -Lists "Исключённые предложения", "Исключённые предложения ОИД" #> param( [Parameter(Mandatory = $true)][string]$SiteUrl, [Parameter(Mandatory = $true)][string]$ClientId, [Parameter(Mandatory = $true)][string]$Tenant, [Parameter(Mandatory = $true)][string]$Thumbprint, [string]$ManifestsDir = "$PSScriptRoot/manifests", [string[]]$Only, [string[]]$Lists ) $ErrorActionPreference = 'Stop' function ConvertTo-XmlSafe { param([string]$Value) return $Value.Replace('&', '&').Replace('<', '<').Replace('>', '>').Replace("'", ''').Replace('"', '"') } function Test-FieldExists { param([string]$List, [string]$InternalName) return $null -ne (Get-PnPField -List $List -Identity $InternalName -ErrorAction SilentlyContinue) } # Создаёт/обновляет колонку НА СПИСКЕ. Идемпотентно. Lookup/UserMulti/Number-с-decimals # — через XML (иначе PnP-cmdlet не покрывает нюансы), остальное — обычным Add-PnPField. function Add-ListColumn { param([string]$List, $Col) if (Test-FieldExists -List $List -InternalName $Col.internalName) { if ($Col.type -eq 'Choice' -and $Col.choices) { try { $vals = @{ Choices = [string[]]$Col.choices } if ($null -ne $Col.fillIn) { $vals.FillInChoice = [bool]$Col.fillIn } Set-PnPField -List $List -Identity $Col.internalName -Values $vals | Out-Null Write-Host " [~] варианты обновлены: $($Col.internalName)" -ForegroundColor DarkGray } catch { Write-Warning " [!] $($Col.internalName): не удалось обновить варианты — $($_.Exception.Message)" } } else { Write-Host " [=] есть: $($Col.internalName)" -ForegroundColor DarkGray } return } try { switch ($Col.type) { 'Text' { Add-PnPField -List $List -InternalName $Col.internalName -DisplayName $Col.title -Type Text -AddToDefaultView | Out-Null } 'Note' { Add-PnPField -List $List -InternalName $Col.internalName -DisplayName $Col.title -Type Note -AddToDefaultView | Out-Null } 'Boolean' { Add-PnPField -List $List -InternalName $Col.internalName -DisplayName $Col.title -Type Boolean -AddToDefaultView | Out-Null } 'DateTime' { Add-PnPField -List $List -InternalName $Col.internalName -DisplayName $Col.title -Type DateTime -AddToDefaultView | Out-Null if ($Col.dateOnly) { Set-PnPField -List $List -Identity $Col.internalName -Values @{ DisplayFormat = 0; FriendlyDisplayFormat = 1 } | Out-Null } } 'Choice' { $multi = [bool]$Col.multiChoice $type = if ($multi) { 'MultiChoice' } else { 'Choice' } Add-PnPField -List $List -InternalName $Col.internalName -DisplayName $Col.title -Type $type -Choices $Col.choices -AddToDefaultView | Out-Null if ($Col.fillIn) { Set-PnPField -List $List -Identity $Col.internalName -Values @{ FillInChoice = $true } | Out-Null } } 'Number' { if ($null -ne $Col.decimals) { $xml = "" Add-PnPFieldFromXml -List $List -FieldXml $xml | Out-Null } else { Add-PnPField -List $List -InternalName $Col.internalName -DisplayName $Col.title -Type Number -AddToDefaultView | Out-Null } } 'User' { if ($Col.allowMultiple) { $xml = "" Add-PnPFieldFromXml -List $List -FieldXml $xml | Out-Null } else { Add-PnPField -List $List -InternalName $Col.internalName -DisplayName $Col.title -Type User -AddToDefaultView | Out-Null } if ($Col.userSelectionMode) { $mode = if ($Col.userSelectionMode -eq 'PeopleAndGroups') { 1 } else { 0 } Set-PnPField -List $List -Identity $Col.internalName -Values @{ SelectionMode = $mode } | Out-Null } } 'Lookup' { $src = Get-PnPList -Identity $Col.lookupList -ErrorAction Stop $guid = $src.Id.ToString() $show = if ($Col.lookupShowField) { $Col.lookupShowField } else { 'Title' } $multAttr = if ($Col.allowMultiple) { " Mult='TRUE'" } else { '' } $ftype = if ($Col.allowMultiple) { 'LookupMulti' } else { 'Lookup' } $xml = "" Add-PnPFieldFromXml -List $List -FieldXml $xml | Out-Null } default { throw "Неизвестный тип колонки '$($Col.type)'" } } if ($Col.required -and $Col.type -ne 'Lookup') { Set-PnPField -List $List -Identity $Col.internalName -Values @{ Required = $true } | Out-Null } Write-Host " [+] $($Col.internalName) — «$($Col.title)» [$($Col.type)]" -ForegroundColor Green } catch { Write-Warning " [!] $($Col.internalName): $($_.Exception.Message)" } } Write-Host "Подключение к $SiteUrl (app-only) ..." -ForegroundColor Cyan Connect-PnPOnline -Url $SiteUrl -ClientId $ClientId -Tenant $Tenant -Thumbprint $Thumbprint $files = Get-ChildItem -Path $ManifestsDir -Filter '*.provisioning.json' -ErrorAction SilentlyContinue if (-not $files) { throw "Манифесты *.provisioning.json не найдены в $ManifestsDir" } $manifests = @() foreach ($file in $files) { $def = Get-Content -Path $file.FullName -Raw -Encoding UTF8 | ConvertFrom-Json if ($Only -and ($Only -notcontains $def.form)) { continue } if ($Lists) { $def.lists = @($def.lists | Where-Object { $Lists -contains $_.title }) if ($def.lists.Count -eq 0) { continue } } $manifests += $def } if (-not $manifests) { throw "Нет манифестов после фильтров -Only / -Lists" } if ($Lists) { $found = @($manifests | ForEach-Object { $_.lists } | ForEach-Object { $_.title }) $missing = @($Lists | Where-Object { $found -notcontains $_ }) if ($missing.Count -gt 0) { throw "Списков нет в манифестах: $($missing -join ', ')" } } # --- Проход A: создать все списки/библиотеки --- Write-Host "`n=== Проход A: списки ===" -ForegroundColor Cyan foreach ($def in $manifests) { Write-Host "Форма '$($def.form)':" -ForegroundColor White foreach ($ls in $def.lists) { $tmpl = if ($ls.template -eq 'DocumentLibrary') { 'DocumentLibrary' } else { 'GenericList' } if (Get-PnPList -Identity $ls.title -ErrorAction SilentlyContinue) { Write-Host " [=] есть: «$($ls.title)»" -ForegroundColor DarkGray } else { New-PnPList -Title $ls.title -Template $tmpl -EnableVersioning | Out-Null Write-Host " [+] создан ($tmpl): «$($ls.title)»" -ForegroundColor Green } } } # --- Проход B: колонки --- Write-Host "`n=== Проход B: колонки ===" -ForegroundColor Cyan foreach ($def in $manifests) { Write-Host "Форма '$($def.form)':" -ForegroundColor White foreach ($ls in $def.lists) { Write-Host " список «$($ls.title)»:" -ForegroundColor White foreach ($col in $ls.columns) { Add-ListColumn -List $ls.title -Col $col } } } Write-Host "`nГотово." -ForegroundColor Cyan Disconnect-PnPOnline