185 lines
8.8 KiB
PowerShell
185 lines
8.8 KiB
PowerShell
<#
|
||
.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) {
|
||
# Колонка уже есть. Для 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
|