Files
request-portal/provisioning/Provision-Lists.ps1
T
2026-08-06 15:00:06 +05:00

228 lines
11 KiB
PowerShell
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<#
.SYNOPSIS
Идемпотентно создаёт списки данных форм портала заявок в SharePoint Online
по манифестам provisioning.json (единый источник правды по колонкам).
.DESCRIPTION
Обходит ВСЕ манифесты forms/**/provisioning.json (в т.ч. подпапки approval/) и для
каждого:
1) создаёт список (если его ещё нет);
2) при необходимости переименовывает колонку Title;
3) обеспечивает колонки.
Колонки — через СТОЛБЦЫ САЙТА (эталоны):
- не-Lookup колонка сначала создаётся как столбец сайта (группа «Портал заявок»),
один раз на весь сайт, затем ПОДКЛЮЧАЕТСЯ к списку по ссылке. Так одинаковые
колонки (Status, Phase, StepOrder …) определены в одном месте и единообразны;
менять их (напр. варианты Choice) можно в эталоне. Choice при повторе объединяет
варианты (union) — наборы разных форм не затирают друг друга.
- Lookup остаётся столбцом СПИСКА: эталон-lookup привязан к одному списку, а у нас
Ticket у каждого списка согласования ссылается на свой список данных.
Повторный запуск безопасен (идемпотентно): существующие эталоны/колонки не
пересоздаются. Имена колонок (internalName) — как в манифестах, к ним обращается код.
Вход — по сертификату «от имени приложения» (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('&', '&amp;').Replace('<', '&lt;').Replace('>', '&gt;').Replace("'", '&apos;').Replace('"', '&quot;')
}
# Есть ли колонка НА СПИСКЕ (list-scope).
function Test-FieldExists {
param([string]$List, [string]$InternalName)
return $null -ne (Get-PnPField -List $List -Identity $InternalName -ErrorAction SilentlyContinue)
}
# Есть ли СТОЛБЕЦ САЙТА (site-scope, галерея столбцов сайта).
function Test-SiteFieldExists {
param([string]$InternalName)
return $null -ne (Get-PnPField -Identity $InternalName -ErrorAction SilentlyContinue)
}
$SITE_COLUMN_GROUP = 'Портал заявок'
# Создаёт/обновляет СТОЛБЕЦ САЙТА (эталон) — один раз на весь сайт. Для Choice при
# повторе объединяет варианты (union), чтобы наборы разных форм не затирали друг друга.
# Lookup сюда НЕ попадает (эталоном быть не может — привязка к конкретному списку).
function Ensure-SiteColumn {
param($Col)
if (Test-SiteFieldExists -InternalName $Col.internalName) {
if ($Col.type -eq 'Choice' -and $Col.choices) {
$existing = (Get-PnPField -Identity $Col.internalName).Choices
$merged = @($existing) + @($Col.choices) | Select-Object -Unique
Set-PnPField -Identity $Col.internalName -Values @{ Choices = [string[]]$merged } | Out-Null
}
return
}
$g = $SITE_COLUMN_GROUP
switch ($Col.type) {
'Text' { Add-PnPField -InternalName $Col.internalName -DisplayName $Col.title -Type Text -Group $g | Out-Null }
'Note' { Add-PnPField -InternalName $Col.internalName -DisplayName $Col.title -Type Note -Group $g | Out-Null }
'DateTime' {
Add-PnPField -InternalName $Col.internalName -DisplayName $Col.title -Type DateTime -Group $g | Out-Null
if ($Col.dateOnly) {
Set-PnPField -Identity $Col.internalName -Values @{ DisplayFormat = 0; FriendlyDisplayFormat = 1 } | Out-Null
}
}
'Boolean' { Add-PnPField -InternalName $Col.internalName -DisplayName $Col.title -Type Boolean -Group $g | Out-Null }
'User' {
if ($Col.allowMultiple) {
$xml = "<Field Type='UserMulti' DisplayName='$(ConvertTo-XmlSafe $Col.title)' Name='$($Col.internalName)' Mult='TRUE' Group='$(ConvertTo-XmlSafe $g)' />"
Add-PnPFieldFromXml -FieldXml $xml | Out-Null
}
else {
Add-PnPField -InternalName $Col.internalName -DisplayName $Col.title -Type User -Group $g | Out-Null
}
if ($Col.userSelectionMode) {
$mode = if ($Col.userSelectionMode -eq 'PeopleAndGroups') { 1 } else { 0 }
Set-PnPField -Identity $Col.internalName -Values @{ SelectionMode = $mode } | Out-Null
}
}
'Choice' { Add-PnPField -InternalName $Col.internalName -DisplayName $Col.title -Type Choice -Choices $Col.choices -Group $g | Out-Null }
'Number' {
if ($null -ne $Col.decimals) {
$xml = "<Field Type='Number' DisplayName='$(ConvertTo-XmlSafe $Col.title)' Name='$($Col.internalName)' Decimals='$($Col.decimals)' Group='$(ConvertTo-XmlSafe $g)' />"
Add-PnPFieldFromXml -FieldXml $xml | Out-Null
}
else {
Add-PnPField -InternalName $Col.internalName -DisplayName $Col.title -Type Number -Group $g | Out-Null
}
}
default { throw "Неизвестный тип столбца сайта '$($Col.type)'" }
}
Write-Host " [+эталон] $($Col.internalName) — «$($Col.title)» [$($Col.type)]" -ForegroundColor Green
}
# Обеспечивает колонку НА СПИСКЕ. Не-Lookup — из эталона (столбец сайта → подключить).
# Lookup — обычным столбцом списка (эталоном быть не может).
function Add-ColumnToList {
param([string]$List, $Col)
if ($Col.type -eq 'Lookup') {
if (Test-FieldExists -List $List -InternalName $Col.internalName) {
Write-Host " [=] lookup есть: $($Col.internalName)" -ForegroundColor DarkGray
return
}
try {
$src = Get-PnPList -Identity $Col.lookupList -ErrorAction Stop
$guid = $src.Id.ToString()
$show = if ($Col.lookupShowField) { $Col.lookupShowField } else { 'Title' }
$xml = "<Field Type='Lookup' DisplayName='$(ConvertTo-XmlSafe $Col.title)' Name='$($Col.internalName)' ShowField='$show' List='{$guid}' />"
Add-PnPFieldFromXml -List $List -FieldXml $xml | Out-Null
Write-Host " [+] lookup (столбец списка): $($Col.internalName)" -ForegroundColor Green
}
catch {
Write-Warning " [!] $($Col.internalName): $($_.Exception.Message)"
}
return
}
# 1) эталон на сайте (создать/обновить)
try {
Ensure-SiteColumn -Col $Col
}
catch {
Write-Warning " [!] эталон $($Col.internalName): $($_.Exception.Message)"
return
}
# 2) подключить эталон к списку (если ещё не подключён)
if (Test-FieldExists -List $List -InternalName $Col.internalName) {
Write-Host " [=] уже на списке: $($Col.internalName)" -ForegroundColor DarkGray
}
else {
try {
Add-PnPField -List $List -Field $Col.internalName -AddToDefaultView | Out-Null
Write-Host " [+] подключён эталон: $($Col.internalName)" -ForegroundColor Green
}
catch {
Write-Warning " [!] подключение $($Col.internalName): $($_.Exception.Message)"
}
}
# 3) обязательность — настройка уровня списка (у поля может отличаться по спискам)
if ($Col.required) {
try { Set-PnPField -List $List -Identity $Col.internalName -Values @{ Required = $true } | Out-Null } catch {}
}
}
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) Колонки (идемпотентно): не-Lookup — через эталоны (столбцы сайта), Lookup — на список.
foreach ($col in $def.columns) {
Add-ColumnToList -List $listTitle -Col $col
}
}
Write-Host "`nГотово." -ForegroundColor Cyan
Disconnect-PnPOnline