Files
CustomBranding/deployment/add-custombranding.ps1
Torsten Brendgen 490e9adbd8 feat: Add custom branding functionality with CSS and JSON configuration
- Introduced custom branding CSS styles in `custom-branding.css`.
- Created example JSON configuration for custom branding in `custom-branding.example.json`.
- Implemented branding configuration logic in `BrandingConfig.ts` to normalize and validate branding settings.
- Developed CSS loader to manage loading and unloading of custom stylesheets in `BrandingCssLoader.ts`.
- Added DOM rendering capabilities for branding elements in `BrandingDomRenderer.ts`.
- Defined types and interfaces for branding elements and configurations in `BrandingTypes.ts`.
- Included localization support for German in `de-de.js`.
- Added unit tests for branding configuration, CSS loader, and DOM renderer.
- Validated project structure and static assets with new validation scripts.
2026-07-20 22:55:47 +02:00

195 lines
7.8 KiB
PowerShell

param(
[Parameter(Mandatory = $true)]
[Alias('WebUrl')]
[string]$SiteUrl,
[string]$CssPath = '',
[string[]]$AllowedCssHost = @(),
[switch]$EnableDebug,
[switch]$Disable,
[switch]$ResetConfiguration,
[string]$ClassicScriptUrl = '',
[switch]$RemoveClassicScriptLink,
[string]$Description = 'MSFT-Custom-Solution:CustomBranding'
)
if (-not (Get-Command -Name Get-SPSite -ErrorAction SilentlyContinue)) {
if (Get-Module -ListAvailable -Name Microsoft.SharePoint.PowerShell) {
Import-Module Microsoft.SharePoint.PowerShell -DisableNameChecking
}
elseif (Get-PSSnapin -Registered -Name Microsoft.SharePoint.PowerShell -ErrorAction SilentlyContinue) {
Add-PSSnapin Microsoft.SharePoint.PowerShell
}
else {
throw 'Die SharePoint PowerShell-Komponenten wurden nicht gefunden. Bitte in der SharePoint Management Shell ausfuehren.'
}
}
$componentId = [Guid]'035ba968-6488-4d42-86b3-0470ffcc95b9'
$componentIdText = $componentId.ToString().ToLowerInvariant()
$componentLocation = 'ClientSideExtension.ApplicationCustomizer'
$componentName = 'CustomBranding'
$componentTitle = 'Custom Branding Application Customizer'
$classicActionName = 'CustomBranding.Classic.ScriptLink'
function New-DefaultCustomBrandingConfiguration {
return @{
schemaVersion = 2
enabled = $true
debug = $false
allowedCssHosts = @()
cssfiles = @()
placeholdertop = @{ elements = @() }
placeholderbottom = @{ elements = @() }
}
}
function ConvertTo-Hashtable {
param([object]$InputObject)
if ($null -eq $InputObject) { return $null }
if ($InputObject -is [System.Collections.IDictionary]) {
$result = @{}
foreach ($key in $InputObject.Keys) { $result[$key] = ConvertTo-Hashtable $InputObject[$key] }
return $result
}
if ($InputObject -is [System.Collections.IEnumerable] -and -not ($InputObject -is [string])) {
return @($InputObject | ForEach-Object { ConvertTo-Hashtable $_ })
}
if ($InputObject.PSObject -and $InputObject.PSObject.Properties.Count -gt 0 -and
-not ($InputObject -is [string]) -and -not ($InputObject.GetType().IsPrimitive)) {
$result = @{}
foreach ($property in $InputObject.PSObject.Properties) { $result[$property.Name] = ConvertTo-Hashtable $property.Value }
return $result
}
return $InputObject
}
function Get-ExistingConfiguration {
param([Microsoft.SharePoint.SPUserCustomAction]$Action)
if ($ResetConfiguration -or -not $Action -or [string]::IsNullOrWhiteSpace($Action.ClientSideComponentProperties)) {
return New-DefaultCustomBrandingConfiguration
}
try {
$parsed = $Action.ClientSideComponentProperties | ConvertFrom-Json
$configuration = ConvertTo-Hashtable $parsed
if (-not $configuration) { return New-DefaultCustomBrandingConfiguration }
return $configuration
}
catch {
Write-Warning 'Vorhandene ClientSideComponentProperties waren ungueltig und werden durch Standardwerte ersetzt.'
return New-DefaultCustomBrandingConfiguration
}
}
function Remove-WebScopedComponentActions {
param([Microsoft.SharePoint.SPSite]$CurrentSite)
$removed = 0
foreach ($web in $CurrentSite.AllWebs) {
try {
$actions = @($web.UserCustomActions | Where-Object {
$_.Location -eq $componentLocation -and
$_.ClientSideComponentId -and
$_.ClientSideComponentId.ToString().ToLowerInvariant() -eq $componentIdText
})
foreach ($action in $actions) {
$action.Delete()
$removed++
}
if ($actions.Count -gt 0) { $web.Update() }
}
finally {
$web.Dispose()
}
}
return $removed
}
$site = Get-SPSite -Identity $SiteUrl
try {
$siteActions = @($site.UserCustomActions | Where-Object {
$_.Location -eq $componentLocation -and
$_.ClientSideComponentId -and
$_.ClientSideComponentId.ToString().ToLowerInvariant() -eq $componentIdText
})
$action = $siteActions | Select-Object -First 1
$configuration = Get-ExistingConfiguration -Action $action
$configuration.schemaVersion = 2
if (-not $configuration.ContainsKey('placeholdertop')) { $configuration.placeholdertop = @{ elements = @() } }
if (-not $configuration.ContainsKey('placeholderbottom')) { $configuration.placeholderbottom = @{ elements = @() } }
if (-not $configuration.ContainsKey('cssfiles')) { $configuration.cssfiles = @() }
if (-not $configuration.ContainsKey('allowedCssHosts')) { $configuration.allowedCssHosts = @() }
if ($PSBoundParameters.ContainsKey('EnableDebug')) { $configuration.debug = [bool]$EnableDebug.IsPresent }
if ($PSBoundParameters.ContainsKey('Disable')) { $configuration.enabled = -not [bool]$Disable.IsPresent }
if (-not [string]::IsNullOrWhiteSpace($CssPath)) {
$trimmedPath = $CssPath.Trim()
$existingPaths = @($configuration.cssfiles | ForEach-Object {
if ($_ -is [System.Collections.IDictionary]) { [string]$_['path'] } else { [string]$_.path }
})
if ($existingPaths -notcontains $trimmedPath) {
$configuration.cssfiles = @($configuration.cssfiles) + @(@{ path = $trimmedPath; media = 'all' })
}
}
if ($AllowedCssHost.Count -gt 0) {
$configuration.allowedCssHosts = @($AllowedCssHost | ForEach-Object { $_.Trim().ToLowerInvariant() } | Where-Object { $_ } | Select-Object -Unique)
}
if (-not $action) {
$action = $site.UserCustomActions.Add()
Write-Host 'Erstelle zentrale CustomBranding Site-Collection-Action...' -ForegroundColor Yellow
}
else {
Write-Host 'Aktualisiere zentrale CustomBranding Site-Collection-Action...' -ForegroundColor Yellow
}
$action.Name = $componentName
$action.Title = $componentTitle
$action.Description = $Description
$action.Location = $componentLocation
$action.Sequence = 100
$action.ClientSideComponentId = $componentId
$action.ClientSideComponentProperties = [string]($configuration | ConvertTo-Json -Depth 20 -Compress)
$action.Update()
@($siteActions | Select-Object -Skip 1) | ForEach-Object {
Write-Host ('Entferne doppelte Site-Collection-Action: ' + $_.Id) -ForegroundColor Yellow
$_.Delete()
}
$removedWebActions = Remove-WebScopedComponentActions -CurrentSite $site
$classicActions = @($site.UserCustomActions | Where-Object {
$_.Location -eq 'ScriptLink' -and $_.Name -eq $classicActionName
})
if ($RemoveClassicScriptLink) {
foreach ($classicAction in $classicActions) { $classicAction.Delete() }
Write-Host 'Classic ScriptLink wurde entfernt.' -ForegroundColor Yellow
}
elseif (-not [string]::IsNullOrWhiteSpace($ClassicScriptUrl)) {
$classicAction = $classicActions | Select-Object -First 1
if (-not $classicAction) { $classicAction = $site.UserCustomActions.Add() }
$classicAction.Name = $classicActionName
$classicAction.Title = 'CustomBranding Classic Runtime'
$classicAction.Description = $Description
$classicAction.Location = 'ScriptLink'
$classicAction.Sequence = 101
$classicAction.ScriptSrc = $ClassicScriptUrl.Trim()
$classicAction.Update()
@($classicActions | Select-Object -Skip 1) | ForEach-Object { $_.Delete() }
Write-Host ('Classic ScriptLink: ' + $classicAction.ScriptSrc) -ForegroundColor Green
}
Write-Host 'CustomBranding wurde zentral fuer die Site Collection registriert.' -ForegroundColor Green
Write-Host ('Entfernte web-scoped Actions: ' + $removedWebActions)
Write-Host ('Properties: ' + $action.ClientSideComponentProperties)
}
finally {
if ($site) { $site.Dispose() }
}