Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
222 changes: 170 additions & 52 deletions scripts/spo-cleanup-site-column-usage/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,72 +102,190 @@ if (!$reportOnly) {

# [CLI for Microsoft 365](#tab/cli-m365-ps)
```powershell
function Invoke-SiteColumnCleanup {
[CmdletBinding(SupportsShouldProcess)]
param(
[Parameter(Mandatory, HelpMessage = "SharePoint site URL hosting the content types and lists")]
[ValidateNotNullOrEmpty()][string] $SiteUrl,

[Parameter(Mandatory, HelpMessage = "Names of content types that should be scanned for the site column")]
[ValidateNotNullOrEmpty()][string[]] $ContentTypeNames,

[Parameter(Mandatory, HelpMessage = "Display name of the site column to remove")]
[ValidateNotNullOrEmpty()][string] $SiteColumnName,

[Parameter(HelpMessage = "When set, only report usage without removing the column")]
[switch] $ReportOnly
)

begin {
Write-Verbose "Ensuring CLI session is authenticated."
$loginOutput = m365 login --ensure 2>&1
if ($LASTEXITCODE -ne 0) {
throw "Failed to ensure CLI login. CLI output: $loginOutput"
}

# Base variables
$siteURL = "https://tenant.sharepoint.com/sites/sitename"
$contentTypeArray = @('testCT1','CustomContentType1')
$siteColumn = "EffectiveDate"
$reportOnly = $true # If $true, just report. If $false, take action.

$m365Status = m365 status
if ($m365Status -match "Logged Out") {
m365 login
}

# Remove the Site Column from all Content Types which have it
Write-Host -BackgroundColor Blue "Checking Content Types"

foreach ($contentTypeName in $contentTypeArray) {

Write-Host "Checking Content Type $contentTypeName"

$contentType = m365 spo contenttype get --webUrl $siteURL --name $contentTypeName
$contentType = $contentType | ConvertFrom-Json
$schemaXml = $contentType.SchemaXml
$schemaXml = [xml]"<xml>$schemaXml</xml>"
$field = $schemaXml.xml.ContentType.Fields.Field | ? { $_.Name -eq $siteColumn }

if ($field) {
Write-Host -ForegroundColor Green "Found column $($siteColumn) in $($contentTypeName)"
if (!$reportOnly) {
Write-Host -ForegroundColor Yellow "Removing column $($siteColumn) in $($contentTypeName)"
$contentTypeId = $contentType.Id.StringValue
$fieldLinkId = $field.ID.Replace("{", "").Replace("}", "")
m365 spo contenttype field remove --contentTypeId $contentTypeId --fieldLinkId $fieldLinkId --webUrl $siteURL --confirm
$script:Summary = [ordered]@{
ContentTypesChecked = 0
ContentTypesUpdated = 0
ListsChecked = 0
ListsUpdated = 0
SiteColumnRemoved = 0
Failures = 0
}
}
}


# Remove the orphaned Site Column from all lists/libraries which have it
Write-Host -BackgroundColor Blue "Checking Lists"

$lists = m365 spo list list --webUrl $siteURL
$lists = $lists | ConvertFrom-Json

foreach ($list in $lists) {
process {
Write-Host "Checking content types for column '$SiteColumnName'."

foreach ($ctName in $ContentTypeNames) {
$script:Summary.ContentTypesChecked++
Write-Host "Examining content type '$ctName'."

$ctOutput = m365 spo contenttype get --webUrl $SiteUrl --name $ctName --output json 2>&1
if ($LASTEXITCODE -ne 0) {
$script:Summary.Failures++
Write-Warning "Failed to retrieve content type '$ctName'. CLI output: $ctOutput"
continue
}

try {
$ct = $ctOutput | ConvertFrom-Json -ErrorAction Stop
}
catch {
$script:Summary.Failures++
Write-Warning "Unable to parse content type '$ctName'. $($_.Exception.Message)"
continue
}

$query = "[?Title=='$SiteColumnName' || InternalName=='$SiteColumnName']"
$fieldsOutput = m365 spo contenttype field list --webUrl $SiteUrl --contentTypeName $ctName --properties "Title,Id,InternalName" --query $query --output json 2>&1
if ($LASTEXITCODE -ne 0) {
$script:Summary.Failures++
Write-Warning "Failed to list fields for content type '$ctName'. CLI output: $fieldsOutput"
continue
}

try {
$ctFields = $fieldsOutput | ConvertFrom-Json -ErrorAction Stop
}
catch {
$script:Summary.Failures++
Write-Warning "Unable to parse field list for '$ctName'. $($_.Exception.Message)"
continue
}

$fieldLink = $ctFields | Select-Object -First 1
if ($fieldLink) {
Write-Host -ForegroundColor Green "Found field '$SiteColumnName' in content type '$ctName'."
if (-not $ReportOnly -and $PSCmdlet.ShouldProcess("Content type '$ctName'", "Remove field link")) {
$removeOutput = m365 spo contenttype field remove --webUrl $SiteUrl --contentTypeId $ct.Id.StringValue --id $fieldLink.Id --force 2>&1
if ($LASTEXITCODE -ne 0) {
$script:Summary.Failures++
Write-Warning "Failed to remove field '$SiteColumnName' from '$ctName'. CLI output: $removeOutput"
}
else {
$script:Summary.ContentTypesUpdated++
}
}
}
}

$listTitle = $list.Title
Write-Host "Checking list $($listTitle)"
Write-Host "Checking lists for orphaned column '$SiteColumnName'."
$listOutput = m365 spo list list --webUrl $SiteUrl --output json 2>&1
if ($LASTEXITCODE -ne 0) {
$script:Summary.Failures++
throw "Failed to retrieve lists. CLI output: $listOutput"
}

$field = m365 spo field get --webUrl $siteURL --listTitle $listTitle --fieldTitle $siteColumn
try {
$lists = $listOutput | ConvertFrom-Json -ErrorAction Stop
}
catch {
throw "Unable to parse lists response. $($_.Exception.Message)"
}

if ($field) {
Write-Host -ForegroundColor Green "Found column $($siteColumn) in $($listTitle)"
foreach ($list in $lists) {
$script:Summary.ListsChecked++
$listTitle = $list.Title
Write-Host "Examining list '$listTitle'."

$listQuery = "[?Title=='$SiteColumnName' || InternalName=='$SiteColumnName']"
$listFieldsOutput = m365 spo field list --webUrl $SiteUrl --listTitle $listTitle --query $listQuery --output json 2>&1
if ($LASTEXITCODE -ne 0) {
$script:Summary.Failures++
Write-Warning "Failed to list fields for list '$listTitle'. CLI output: $listFieldsOutput"
continue
}

try {
$listFields = $listFieldsOutput | ConvertFrom-Json -ErrorAction Stop
}
catch {
$script:Summary.Failures++
Write-Warning "Unable to parse field list for '$listTitle'. $($_.Exception.Message)"
continue
}

$listField = $listFields | Select-Object -First 1
if (-not $listField) {
continue
}

Write-Host -ForegroundColor Green "Found field '$SiteColumnName' in list '$listTitle'."
if (-not $ReportOnly -and $PSCmdlet.ShouldProcess("List '$listTitle'", "Remove field")) {
$removeFieldOutput = m365 spo field remove --webUrl $SiteUrl --listTitle $listTitle --id $listField.Id --force 2>&1
if ($LASTEXITCODE -ne 0) {
$script:Summary.Failures++
Write-Warning "Failed to remove field from list '$listTitle'. CLI output: $removeFieldOutput"
}
else {
$script:Summary.ListsUpdated++
}
}
}

if (!$reportOnly) {
Write-Host -ForegroundColor Yellow "Removing column $($siteColumn) in $($listTitle)"
m365 spo field remove --webUrl $siteURL --listTitle $listTitle --fieldTitle $siteColumn --confirm
if (-not $ReportOnly -and $PSCmdlet.ShouldProcess("Site '$SiteUrl'", "Remove site column '$SiteColumnName'")) {
$siteFieldOutput = m365 spo field get --webUrl $SiteUrl --title $SiteColumnName --output json 2>&1
if ($LASTEXITCODE -eq 0) {
try {
$siteField = $siteFieldOutput | ConvertFrom-Json -ErrorAction Stop
}
catch {
$script:Summary.Failures++
throw "Unable to parse site column details for '$SiteColumnName'. $($_.Exception.Message)"
}

$removeSiteField = m365 spo field remove --webUrl $SiteUrl --id $siteField.Id --force 2>&1
if ($LASTEXITCODE -ne 0) {
$script:Summary.Failures++
Write-Warning "Failed to remove site column '$SiteColumnName'. CLI output: $removeSiteField"
}
else {
$script:Summary.SiteColumnRemoved++
}
}
else {
Write-Verbose "Site column '$SiteColumnName' was not found at the site level."
}
}
}
}

# Remove the Site Column itself
if (!$reportOnly) {
m365 spo field remove --webUrl $siteURL --fieldTitle $siteColumn --confirm
end {
Write-Host "`nCleanup summary:" -ForegroundColor Cyan
Write-Host " Content types checked : $($script:Summary.ContentTypesChecked)"
Write-Host " Content types updated : $($script:Summary.ContentTypesUpdated)"
Write-Host " Lists checked : $($script:Summary.ListsChecked)"
Write-Host " Lists updated : $($script:Summary.ListsUpdated)"
Write-Host " Site columns removed : $($script:Summary.SiteColumnRemoved)"
Write-Host " Failures : $($script:Summary.Failures)"
}
}

# Example usage:
# Invoke-SiteColumnCleanup -SiteUrl "https://tenant.sharepoint.com/sites/sitename" -ContentTypeNames 'testCT1','CustomContentType1' -SiteColumnName 'EffectiveDate' -ReportOnly

Invoke-SiteColumnCleanup -SiteUrl "https://tenanttocheck.sharepoint.com/sites/PnPDemo2" -ContentTypeNames 'testContentTypeA','testContentTypeB','testContentTypeC' -SiteColumnName 'testColumn1' -ReportOnly
```
[!INCLUDE [More about CLI for Microsoft 365](../../docfx/includes/MORE-CLIM365.md)]
***
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 2 additions & 3 deletions scripts/spo-cleanup-site-column-usage/assets/sample.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"Sometimes when we iteratively build out our information architecture, we're over-zealous. It seems like we need a set of Site Columns to maintain metadata on lists or libraries, but in the end, we decide we want to trim away a few of the Site Columns we’ve created."
],
"creationDateTime": "2021-10-15",
"updateDateTime": "2021-10-25",
"updateDateTime": "2025-11-18",
"products": [
"SharePoint"
],
Expand All @@ -20,7 +20,7 @@
},
{
"key": "CLI-FOR-MICROSOFT365",
"value": "3.7.0"
"value": "11.0.0"
}
],
"categories": [
Expand All @@ -35,7 +35,6 @@
"Get-PnPList",
"Get-PnPField",
"Remove-PnPField",
"m365 status",
"m365 login",
"m365 spo contenttype get",
"m365 spo contenttype field remove",
Expand Down