fix: changed script to crete cells from list to site

This commit is contained in:
Тимур Городецкий
2026-08-06 15:00:06 +05:00
parent 571614c031
commit 05f294d732
11 changed files with 435 additions and 99 deletions
+122 -79
View File
@@ -4,18 +4,27 @@
по манифестам provisioning.json (единый источник правды по колонкам).
.DESCRIPTION
Для каждого файла forms/<Форма>/provisioning.json скрипт:
1) создаёт список данных (если его ещё нет);
Обходит ВСЕ манифесты forms/**/provisioning.json (в т.ч. подпапки approval/) и для
каждого:
1) создаёт список (если его ещё нет);
2) при необходимости переименовывает колонку Title;
3) добавляет недостающие колонки нужных типов.
Повторный запуск безопасен: существующие список и колонки не трогаются
(перед созданием колонки проверяется, что её ещё нет).
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
@@ -52,90 +61,124 @@ function ConvertTo-XmlSafe {
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)
}
# Создаёт одну колонку по описанию из манифеста (идемпотентно).
function Add-Column {
param([string]$List, $Col)
# Есть ли СТОЛБЕЦ САЙТА (site-scope, галерея столбцов сайта).
function Test-SiteFieldExists {
param([string]$InternalName)
return $null -ne (Get-PnPField -Identity $InternalName -ErrorAction SilentlyContinue)
}
if (Test-FieldExists -List $List -InternalName $Col.internalName) {
# Колонка уже есть. Для Choice — досетапливаем варианты (могли добавиться новые
# статусы): существующие значения сохраняются, добавляются недостающие.
$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) {
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
$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 {
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
Ensure-SiteColumn -Col $Col
}
catch {
Write-Warning " [!] $($Col.internalName): $($_.Exception.Message)"
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 {}
}
}
@@ -174,9 +217,9 @@ foreach ($manifest in $manifests) {
}
}
# 3) Колонки (идемпотентно)
# 3) Колонки (идемпотентно): не-Lookup — через эталоны (столбцы сайта), Lookup — на список.
foreach ($col in $def.columns) {
Add-Column -List $listTitle -Col $col
Add-ColumnToList -List $listTitle -Col $col
}
}