Files
request-portal/provisioning/Provision-Lists.ps1
T
2026-08-05 13:53:26 +05:00

185 lines
8.8 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 скрипт:
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('&', '&amp;').Replace('<', '&lt;').Replace('>', '&gt;').Replace("'", '&apos;').Replace('"', '&quot;')
}
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) {
# Колонка уже есть. Для Choice — досетапливаем варианты (могли добавиться новые
# статусы): существующие значения сохраняются, добавляются недостающие.
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) {
# 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 = "<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) {
# 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 = "<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' {
# Список-источник должен уже существовать (см. про порядок в README).
$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)"
}
}
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