<# .SYNOPSIS Идемпотентно создаёт списки данных форм портала заявок в SharePoint Online по манифестам provisioning.json (единый источник правды по колонкам). .DESCRIPTION Для каждого файла forms/<Форма>/provisioning.json скрипт: 1) создаёт список данных (если его ещё нет); 2) при необходимости переименовывает колонку Title; 3) добавляет недостающие колонки нужных типов. Повторный запуск безопасен: существующие список и колонки не трогаются (перед созданием колонки проверяется, что её ещё нет). Вход — по сертификату «от имени приложения» (app-only, право Sites.Selected). Сертификат берётся из личного хранилища по отпечатку (Thumbprint). Списки согласования пока НЕ создаются — только данные (по договорённости). .PARAMETER SiteUrl URL сайта, например https://tnsukz.sharepoint.com/sites/requests .PARAMETER ClientId Client Id приложения-провижинера (Sites.Selected). .PARAMETER Tenant Тенант, например tnsukz.onmicrosoft.com .PARAMETER Thumbprint Отпечаток сертификата в хранилище CurrentUser\My. .PARAMETER FormsRoot Папка с формами. По умолчанию — src/webparts/requestPortal/forms. .EXAMPLE ./Provision-Lists.ps1 -SiteUrl "https://tnsukz.sharepoint.com/sites/requests" ` -ClientId "7459c965-5ed4-4673-8140-0ab8faf9db64" -Tenant "tnsukz.onmicrosoft.com" ` -Thumbprint "AABBCC...." #> param( [Parameter(Mandatory = $true)][string]$SiteUrl, [Parameter(Mandatory = $true)][string]$ClientId, [Parameter(Mandatory = $true)][string]$Tenant, [Parameter(Mandatory = $true)][string]$Thumbprint, [string]$FormsRoot = "$PSScriptRoot/../src/webparts/requestPortal/forms" ) $ErrorActionPreference = 'Stop' # Экранирование значений, попадающих в XML полей (Lookup/Number с точностью). 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) } # Создаёт одну колонку по описанию из манифеста (идемпотентно). function Add-Column { param([string]$List, $Col) if (Test-FieldExists -List $List -InternalName $Col.internalName) { 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 } 'DateTime' { Add-PnPField -List $List -InternalName $Col.internalName -DisplayName $Col.title -Type DateTime -AddToDefaultView | Out-Null if ($Col.dateOnly) { # 0 = только дата; 1 = отключить относительный формат («Завтра», «вчера») Set-PnPField -List $List -Identity $Col.internalName -Values @{ DisplayFormat = 0; FriendlyDisplayFormat = 1 } | Out-Null } } 'Boolean' { Add-PnPField -List $List -InternalName $Col.internalName -DisplayName $Col.title -Type Boolean -AddToDefaultView | Out-Null } 'User' { if ($Col.allowMultiple) { # Мульти-пользователь: только через XML (UserMulti нет в enum FieldType). $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) { # 0 = только люди, 1 = люди и группы $mode = if ($Col.userSelectionMode -eq 'PeopleAndGroups') { 1 } else { 0 } Set-PnPField -List $List -Identity $Col.internalName -Values @{ SelectionMode = $mode } | Out-Null } } 'Choice' { Add-PnPField -List $List -InternalName $Col.internalName -DisplayName $Col.title -Type Choice -Choices $Col.choices -AddToDefaultView | Out-Null } 'Number' { if ($null -ne $Col.decimals) { # Точность задаётся только через XML. $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 } } 'Lookup' { # Список-источник должен уже существовать (см. про порядок в README). $src = Get-PnPList -Identity $Col.lookupList -ErrorAction Stop $guid = $src.Id.ToString() $show = if ($Col.lookupShowField) { $Col.lookupShowField } else { 'Title' } $xml = "" Add-PnPFieldFromXml -List $List -FieldXml $xml | Out-Null } default { throw "Неизвестный тип колонки '$($Col.type)'" } } if ($Col.required) { 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 $manifests = Get-ChildItem -Path $FormsRoot -Recurse -Filter 'provisioning.json' if (-not $manifests) { Write-Warning "Манифесты provisioning.json не найдены в $FormsRoot" Disconnect-PnPOnline return } foreach ($manifest in $manifests) { $def = Get-Content -Path $manifest.FullName -Raw -Encoding UTF8 | ConvertFrom-Json $listTitle = $def.dataListTitle Write-Host "`n=== Форма: $($manifest.Directory.Name) → список «$listTitle» ===" -ForegroundColor Cyan # 1) Список (идемпотентно) if (-not (Get-PnPList -Identity $listTitle -ErrorAction SilentlyContinue)) { New-PnPList -Title $listTitle -Template GenericList -EnableVersioning | Out-Null Write-Host " [+] создан список" -ForegroundColor Green } else { Write-Host " [=] список уже существует" -ForegroundColor DarkGray } # 2) Переименование Title (если задано) if ($def.titleColumnName) { try { Set-PnPField -List $listTitle -Identity 'Title' -Values @{ Title = $def.titleColumnName } | Out-Null Write-Host " [~] Title → «$($def.titleColumnName)»" -ForegroundColor DarkGray } catch { Write-Warning " [!] не удалось переименовать Title: $($_.Exception.Message)" } } # 3) Колонки (идемпотентно) foreach ($col in $def.columns) { Add-Column -List $listTitle -Col $col } } Write-Host "`nГотово." -ForegroundColor Cyan Disconnect-PnPOnline