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.
This commit is contained in:
@@ -5,100 +5,190 @@ param(
|
||||
|
||||
[string]$CssPath = '',
|
||||
|
||||
[string[]]$AllowedCssHost = @(),
|
||||
|
||||
[switch]$EnableDebug,
|
||||
|
||||
[switch]$Disable,
|
||||
|
||||
[switch]$ResetConfiguration,
|
||||
|
||||
[string]$ClassicScriptUrl = '',
|
||||
|
||||
[switch]$RemoveClassicScriptLink,
|
||||
|
||||
[string]$Description = 'MSFT-Custom-Solution:CustomBranding'
|
||||
)
|
||||
|
||||
Add-PSSnapin Microsoft.SharePoint.PowerShell -ErrorAction SilentlyContinue | Out-Null
|
||||
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 = '035ba968-6488-4d42-86b3-0470ffcc95b9'
|
||||
$location = 'ClientSideExtension.ApplicationCustomizer'
|
||||
$name = 'CustomBranding'
|
||||
$title = 'Custom Branding'
|
||||
$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 Get-CustomBrandingPropertiesJson {
|
||||
param(
|
||||
[string]$Path
|
||||
)
|
||||
|
||||
$config = @{
|
||||
function New-DefaultCustomBrandingConfiguration {
|
||||
return @{
|
||||
schemaVersion = 2
|
||||
enabled = $true
|
||||
debug = $false
|
||||
allowedCssHosts = @()
|
||||
cssfiles = @()
|
||||
placeholdertop = @{ elements = @() }
|
||||
placeholderbottom = @{ elements = @() }
|
||||
}
|
||||
}
|
||||
|
||||
if ($Path -and $Path.Trim().Length -gt 0) {
|
||||
$config.cssfiles += @{ path = $Path.Trim() }
|
||||
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
|
||||
}
|
||||
|
||||
return ($config | ConvertTo-Json -Depth 10 -Compress)
|
||||
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,
|
||||
[string]$CurrentComponentId,
|
||||
[string]$CurrentLocation
|
||||
)
|
||||
|
||||
$removedCount = 0
|
||||
|
||||
param([Microsoft.SharePoint.SPSite]$CurrentSite)
|
||||
$removed = 0
|
||||
foreach ($web in $CurrentSite.AllWebs) {
|
||||
try {
|
||||
$webActions = @($web.UserCustomActions | Where-Object {
|
||||
$_.Location -eq $CurrentLocation -and
|
||||
$actions = @($web.UserCustomActions | Where-Object {
|
||||
$_.Location -eq $componentLocation -and
|
||||
$_.ClientSideComponentId -and
|
||||
$_.ClientSideComponentId.ToString().ToLower() -eq $CurrentComponentId
|
||||
$_.ClientSideComponentId.ToString().ToLowerInvariant() -eq $componentIdText
|
||||
})
|
||||
|
||||
foreach ($webAction in $webActions) {
|
||||
$web.UserCustomActions.Delete($webAction.Id)
|
||||
$removedCount++
|
||||
}
|
||||
|
||||
if ($webActions.Count -gt 0) {
|
||||
$web.Update()
|
||||
foreach ($action in $actions) {
|
||||
$action.Delete()
|
||||
$removed++
|
||||
}
|
||||
if ($actions.Count -gt 0) { $web.Update() }
|
||||
}
|
||||
finally {
|
||||
$web.Dispose()
|
||||
}
|
||||
}
|
||||
|
||||
return $removedCount
|
||||
return $removed
|
||||
}
|
||||
|
||||
$site = Get-SPSite -Identity $SiteUrl
|
||||
try {
|
||||
$removedWebScopedActions = Remove-WebScopedComponentActions -CurrentSite $site -CurrentComponentId $componentId -CurrentLocation $location
|
||||
$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
|
||||
|
||||
$existingAction = $site.UserCustomActions | Where-Object {
|
||||
$_.Location -eq $location -and $_.ClientSideComponentId -and $_.ClientSideComponentId.ToString().ToLower() -eq $componentId
|
||||
} | Select-Object -First 1
|
||||
$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 ($existingAction) {
|
||||
$action = $existingAction
|
||||
Write-Host 'Aktualisiere vorhandene site-scoped Custom Branding UserCustomAction...' -ForegroundColor Yellow
|
||||
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 {
|
||||
$action = $site.UserCustomActions.Add()
|
||||
Write-Host 'Erstelle neue site-scoped Custom Branding UserCustomAction...' -ForegroundColor Yellow
|
||||
Write-Host 'Aktualisiere zentrale CustomBranding Site-Collection-Action...' -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
$action.Name = $name
|
||||
$action.Title = $title
|
||||
$action.Name = $componentName
|
||||
$action.Title = $componentTitle
|
||||
$action.Description = $Description
|
||||
$action.Location = $location
|
||||
$action.ClientSideComponentId = [Guid]$componentId
|
||||
$action.ClientSideComponentProperties = Get-CustomBrandingPropertiesJson -Path $CssPath
|
||||
$action.Location = $componentLocation
|
||||
$action.Sequence = 100
|
||||
$action.ClientSideComponentId = $componentId
|
||||
$action.ClientSideComponentProperties = [string]($configuration | ConvertTo-Json -Depth 20 -Compress)
|
||||
$action.Update()
|
||||
|
||||
Write-Host 'Custom Branding wurde site-scoped registriert.' -ForegroundColor Green
|
||||
Write-Host ('Entfernte web-scoped Eintraege: ' + $removedWebScopedActions)
|
||||
Write-Host ('Beschreibung: ' + $Description)
|
||||
@($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()
|
||||
}
|
||||
}
|
||||
if ($site) { $site.Dispose() }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user