Files
request-portal/provisioning/Provision-Lists.ps1
T
Тимур Городецкий 40f9ac6c03 feat: changed lists columns
2026-08-06 15:36:22 +05:00

275 lines
14 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 = 'Портал заявок'
# Типы, которые НЕ делаем эталоном (столбцом сайта), а создаём столбцом списка:
# - Lookup (эталон привязан к одному списку, а Ticket у каждого свой);
# - Number с decimals и UserMulti (создаются через XML; web-level XML в PnP падает
# NRE, и это одноразовые поля конкретной формы — переиспользовать нечего).
function Test-NeedsListColumn {
param($Col)
return ($Col.type -eq 'Lookup') `
-or ($Col.type -eq 'Number' -and $null -ne $Col.decimals) `
-or ($Col.type -eq 'User' -and $Col.allowMultiple)
}
# Создаёт/обновляет СТОЛБЕЦ САЙТА (эталон) — один раз на весь сайт. Только простые
# типы (без XML). Для Choice при повторе объединяет варианты (union), чтобы наборы
# разных форм не затирали друг друга.
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' {
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' { 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 / XML-типов). Идемпотентно.
function Add-ListColumn {
param([string]$List, $Col)
if (Test-FieldExists -List $List -InternalName $Col.internalName) {
if ($Col.type -eq 'Choice' -and $Col.choices) {
try {
Set-PnPField -List $List -Identity $Col.internalName -Values @{ Choices = [string[]]$Col.choices } | 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 }
'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 }
}
'Boolean' { Add-PnPField -List $List -InternalName $Col.internalName -DisplayName $Col.title -Type Boolean -AddToDefaultView | Out-Null }
'User' {
if ($Col.allowMultiple) {
$xml = "<Field Type='UserMulti' DisplayName='$(ConvertTo-XmlSafe $Col.title)' Name='$($Col.internalName)' Mult='TRUE' />"
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
}
}
'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 = "<Field Type='Number' DisplayName='$(ConvertTo-XmlSafe $Col.title)' Name='$($Col.internalName)' Decimals='$($Col.decimals)' />"
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' {
$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
}
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)" }
}
# Обеспечивает колонку НА СПИСКЕ: простые типы — из эталона (столбец сайта →
# подключить), Lookup/XML-типы — обычным столбцом списка.
function Add-ColumnToList {
param([string]$List, $Col)
if (Test-NeedsListColumn -Col $Col) {
Add-ListColumn -List $List -Col $Col
return
}
# 1) эталон на сайте (создать/обновить)
try {
Ensure-SiteColumn -Col $Col
}
catch {
Write-Warning " [!] эталон $($Col.internalName): $($_.Exception.Message)"
return
}
# 2) подключить эталон к списку (если ещё не подключён). Без -AddToDefaultView:
# с параметром -Field он несовместим (другой набор параметров cmdlet).
if (Test-FieldExists -List $List -InternalName $Col.internalName) {
Write-Host " [=] уже на списке: $($Col.internalName)" -ForegroundColor DarkGray
}
else {
try {
Add-PnPField -List $List -Field $Col.internalName | 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