2 Commits

Author SHA1 Message Date
9dc85c8057 Added sample CSV files for user/sites importation.
All checks were successful
Release zip package / release (push) Successful in 1s
Fixed a few bugs.
2026-03-16 13:45:00 +01:00
9bcbad5d5b Ajoute de barres de recherches dans les rapports HTML de permissions et stockage
All checks were successful
Release zip package / release (push) Successful in 1s
2026-03-16 11:44:17 +01:00
4 changed files with 134 additions and 56 deletions

View File

@@ -1482,6 +1482,10 @@ h1{font-size:21px;font-weight:600;margin-bottom:6px}
.hdr{background:#0078d4;color:#fff;padding:22px 28px;border-radius:10px;margin-bottom:22px} .hdr{background:#0078d4;color:#fff;padding:22px 28px;border-radius:10px;margin-bottom:22px}
.hdr .sub{font-size:13px;opacity:.85;margin-top:4px} .hdr .sub{font-size:13px;opacity:.85;margin-top:4px}
.hdr a{color:#cce4ff} .hdr a{color:#cce4ff}
.srch{background:#fff;border-radius:8px;padding:10px 14px;margin-bottom:14px;box-shadow:0 1px 4px rgba(0,0,0,.08)}
.srch input{width:100%;padding:6px 10px;border:1px solid #ccc;border-radius:4px;font-size:13px;outline:none}
.srch input:focus{border-color:#0078d4}
.hidden{display:none}
.cards{display:flex;gap:14px;margin-bottom:22px} .cards{display:flex;gap:14px;margin-bottom:22px}
.card{background:#fff;border-radius:8px;padding:16px 20px;flex:1;box-shadow:0 1px 4px rgba(0,0,0,.08);text-align:center} .card{background:#fff;border-radius:8px;padding:16px 20px;flex:1;box-shadow:0 1px 4px rgba(0,0,0,.08);text-align:center}
.card .v{font-size:26px;font-weight:700;color:#0078d4} .card .v{font-size:26px;font-weight:700;color:#0078d4}
@@ -1534,9 +1538,11 @@ a:hover{text-decoration:underline}
<div class="card"><div class="v">$uniqueCount</div><div class="l">Unique Permission Sets</div></div> <div class="card"><div class="v">$uniqueCount</div><div class="l">Unique Permission Sets</div></div>
<div class="card"><div class="v">$userCount</div><div class="l">Distinct Users / Groups</div></div> <div class="card"><div class="v">$userCount</div><div class="l">Distinct Users / Groups</div></div>
</div> </div>
<div class="wrap"><table> <div class="wrap">
<div class="srch"><input type="text" id="q" placeholder="Filter results..." onkeyup="filterTable()"></div>
<table>
<thead><tr><th>Type</th><th>Name</th><th>Users / Members</th><th>Permission Level</th><th>Granted Through</th><th>Unique Permissions</th></tr></thead> <thead><tr><th>Type</th><th>Name</th><th>Users / Members</th><th>Permission Level</th><th>Granted Through</th><th>Unique Permissions</th></tr></thead>
<tbody> <tbody id="tbody">
$rows $rows
</tbody></table></div> </tbody></table></div>
<div class="foot">Generated by SharePoint Toolbox</div> <div class="foot">Generated by SharePoint Toolbox</div>
@@ -1595,6 +1601,12 @@ function fallbackCopy(text) {
try { document.execCommand('copy'); } catch(e) {} try { document.execCommand('copy'); } catch(e) {}
document.body.removeChild(ta); document.body.removeChild(ta);
} }
function filterTable(){
var q=document.getElementById('q').value.toLowerCase();
Array.from(document.getElementById('tbody').rows).forEach(function(r){
r.classList.toggle('hidden', q && !r.innerText.toLowerCase().includes(q));
});
}
</script> </script>
</body></html> </body></html>
"@ "@
@@ -1720,6 +1732,10 @@ a:hover{text-decoration:underline}
.sf-tbl tr:hover td{background:#eaf7ea} .sf-tbl tr:hover td{background:#eaf7ea}
.sf-tbl a{color:#2e7d32} .sf-tbl a{color:#2e7d32}
.foot{margin-top:18px;text-align:center;font-size:12px;color:#bbb} .foot{margin-top:18px;text-align:center;font-size:12px;color:#bbb}
.srch{background:#fff;border-radius:8px;padding:10px 14px;margin-bottom:14px;box-shadow:0 1px 4px rgba(0,0,0,.08)}
.srch input{width:100%;padding:6px 10px;border:1px solid #ccc;border-radius:4px;font-size:13px;outline:none}
.srch input:focus{border-color:#107c10}
.hidden{display:none}
</style> </style>
<script> <script>
function toggle(i){ function toggle(i){
@@ -1741,12 +1757,22 @@ function toggle(i){
<div class="card"><div class="v">$totalFiles</div><div class="l">Total Files</div></div> <div class="card"><div class="v">$totalFiles</div><div class="l">Total Files</div></div>
<div class="card"><div class="v">$libCount</div><div class="l">Libraries / Sites Scanned</div></div> <div class="card"><div class="v">$libCount</div><div class="l">Libraries / Sites Scanned</div></div>
</div> </div>
<div class="wrap"><table> <div class="wrap">
<div class="srch"><input type="text" id="q" placeholder="Filter results..." onkeyup="filterTable()"></div>
<table>
<thead><tr><th>Library</th><th>Site</th><th style="text-align:right">Files</th><th style="text-align:right">Size</th><th style="text-align:right">Versions</th><th>Share of Total</th><th style="text-align:right">Last Modified</th></tr></thead> <thead><tr><th>Library</th><th>Site</th><th style="text-align:right">Files</th><th style="text-align:right">Size</th><th style="text-align:right">Versions</th><th>Share of Total</th><th style="text-align:right">Last Modified</th></tr></thead>
<tbody> <tbody id="tbody">
$rows $rows
</tbody></table></div> </tbody></table></div>
<div class="foot">Generated by SharePoint Toolbox</div> <div class="foot">Generated by SharePoint Toolbox</div>
<script>
function filterTable(){
var q=document.getElementById('q').value.toLowerCase();
Array.from(document.getElementById('tbody').rows).forEach(function(r){
r.classList.toggle('hidden', q && !r.innerText.toLowerCase().includes(q));
});
}
</script>
</body></html> </body></html>
"@ "@
$html | Out-File -FilePath $OutputPath -Encoding UTF8 $html | Out-File -FilePath $OutputPath -Encoding UTF8
@@ -5203,30 +5229,41 @@ $btnBulkCsv.Add_Click({
$ofd = New-Object System.Windows.Forms.OpenFileDialog $ofd = New-Object System.Windows.Forms.OpenFileDialog
$ofd.Filter = "CSV (*.csv)|*.csv|All (*.*)|*.*" $ofd.Filter = "CSV (*.csv)|*.csv|All (*.*)|*.*"
if ($ofd.ShowDialog($form) -ne "OK") { return } if ($ofd.ShowDialog($form) -ne "OK") { return }
# Try semicolon first (handles commas inside fields), fall back to comma
$content = Get-Content $ofd.FileName -Raw
if ($content -match ';') {
$rows = Import-Csv $ofd.FileName -Delimiter ';'
} else {
$rows = Import-Csv $ofd.FileName $rows = Import-Csv $ofd.FileName
}
$count = 0 $count = 0
foreach ($r in $rows) { foreach ($r in $rows) {
# Accepted column names (case-insensitive via PSObject) # Read columns via PSObject properties (case-insensitive)
$name = if ($r.Name) { $r.Name } elseif ($r.name) { $r.name } $props = @{}
elseif ($r.Title) { $r.Title } elseif ($r.title) { $r.title } else { "" } foreach ($p in $r.PSObject.Properties) { $props[$p.Name.ToLower()] = "$($p.Value)".Trim() }
$alias = if ($r.Alias) { $r.Alias } elseif ($r.alias) { $r.alias }
elseif ($r.URL) { $r.URL } elseif ($r.url) { $r.url } else { "" }
$type = if ($r.Type) { $r.Type } elseif ($r.type) { $r.type } else { "Team" }
$tpl = if ($r.Template) { $r.Template } elseif ($r.template) { $r.template } else { "" }
$own = if ($r.Owners) { $r.Owners } elseif ($r.owners) { $r.owners }
elseif ($r.Owner) { $r.Owner } elseif ($r.owner) { $r.owner } else { "" }
$mem = if ($r.Members) { $r.Members } elseif ($r.members) { $r.members } else { "" }
if (-not $name -or -not $alias) { continue } $name = if ($props['name']) { $props['name'] } elseif ($props['title']) { $props['title'] } else { "" }
$alias = if ($props['alias']) { $props['alias'] } elseif ($props['url']) { $props['url'] } else { "" }
$type = if ($props['type']) { $props['type'] } else { "Team" }
$tpl = if ($props['template']) { $props['template'] } else { "" }
$own = if ($props['owners']) { $props['owners'] } elseif ($props['owner']) { $props['owner'] } else { "" }
$mem = if ($props['members']) { $props['members'] } else { "" }
# Name is required; skip empty rows
if (-not $name) { continue }
# Auto-generate alias from name if not provided
if (-not $alias) {
$alias = $name.ToLower() -replace '[^a-z0-9\-]', '-' -replace '-+', '-' -replace '^-|-$', ''
}
# Normalize type # Normalize type
if ($type -match '^[Cc]omm') { $type = "Communication" } else { $type = "Team" } if ($type -match '^[Cc]omm') { $type = "Communication" } else { $type = "Team" }
Add-BulkListItem @{ Add-BulkListItem @{
Name = $name.Trim() Name = $name
Alias = $alias.Trim() Alias = $alias
Type = $type Type = $type
Template = $tpl.Trim() Template = $tpl
Owners = $own.Trim() Owners = $own
Members = $mem.Trim() Members = $mem
} }
$count++ $count++
} }
@@ -5314,32 +5351,83 @@ $btnBulkCreate.Add_Click({
$name = $entry.Name $name = $entry.Name
$alias = $entry.Alias $alias = $entry.Alias
$isTeam = $entry.Type -ne "Communication" $isTeam = $entry.Type -ne "Communication"
$owners = @($entry.Owners -split '[,;]' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) $ownerRaw = "$($entry.Owners)"
$members = @($entry.Members -split '[,;]' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) $memberRaw = "$($entry.Members)"
$owners = [string[]]@($ownerRaw -split '[,;\s]+' | Where-Object { $_ -match '\S' })
$members = [string[]]@($memberRaw -split '[,;\s]+' | Where-Object { $_ -match '\S' })
$tplName = $entry.Template $tplName = $entry.Template
BgLog "[$idx/$total] Creating '$name' (alias: $alias, type: $($entry.Type))..." "White" BgLog "[$idx/$total] Creating '$name' (alias: $alias, type: $($entry.Type))..." "White"
BgLog " DEBUG owners raw='$ownerRaw' parsed=[$($owners -join '|')] count=$($owners.Count)" "Gray"
BgLog " DEBUG members raw='$memberRaw' parsed=[$($members -join '|')] count=$($members.Count)" "Gray"
# TeamSite requires at least one owner
if ($isTeam -and $owners.Count -eq 0) {
BgLog " ERREUR : TeamSite requires at least one owner — skipping '$name'" "Red"
$Sync.Queue.Enqueue(@{ Text = "##STATUS##"; Index = ($idx - 1); Value = "Error: no owner" })
$Sync.ErrCount++
continue
}
# Update status # Update status
$Sync.Queue.Enqueue(@{ Text = "##STATUS##"; Index = ($idx - 1); Value = "Creating..." }) $Sync.Queue.Enqueue(@{ Text = "##STATUS##"; Index = ($idx - 1); Value = "Creating..." })
try { try {
# Create the site # Create the site WITHOUT owners/members (PnP bug: odata.bind empty array)
# Current user becomes default owner; we add owners/members after creation
Connect-PnPOnline -Url $adminUrl -Interactive -ClientId $Params.ClientId Connect-PnPOnline -Url $adminUrl -Interactive -ClientId $Params.ClientId
$newUrl = if ($isTeam) { if ($isTeam) {
if ($owners.Count -gt 0) { BgLog " Creating TeamSite '$alias' (owners/members added after)..." "DarkGray"
New-PnPSite -Type TeamSite -Title $name -Alias $alias -Owners $owners -Wait $newUrl = New-PnPSite -Type TeamSite -Title $name -Alias $alias -Wait
} else { } else {
New-PnPSite -Type TeamSite -Title $name -Alias $alias -Wait BgLog " Creating CommunicationSite '$alias'..." "DarkGray"
} $newUrl = New-PnPSite -Type CommunicationSite -Title $name -Url "$base/sites/$alias" -Wait
} else {
New-PnPSite -Type CommunicationSite -Title $name -Url "$base/sites/$alias" -Wait
} }
BgLog " Site cree : $newUrl" "LightGreen" BgLog " Site cree : $newUrl" "LightGreen"
# Connect to the new site for template + members # Connect to the new site for owners/members/template
Connect-PnPOnline -Url $newUrl -Interactive -ClientId $Params.ClientId Connect-PnPOnline -Url $newUrl -Interactive -ClientId $Params.ClientId
# Assign owners & members post-creation
if ($isTeam) {
$groupId = $null
try { $groupId = (Get-PnPSite -Includes GroupId).GroupId.Guid } catch {}
if ($groupId) {
foreach ($o in $owners) {
try {
Add-PnPMicrosoft365GroupOwner -Identity $groupId -Users $o -ErrorAction Stop
BgLog " Owner added: $o" "Cyan"
} catch { BgLog " Warn owner '$o': $($_.Exception.Message)" "DarkYellow" }
}
foreach ($m in $members) {
try {
Add-PnPMicrosoft365GroupMember -Identity $groupId -Users $m -ErrorAction Stop
BgLog " Member added: $m" "Cyan"
} catch { BgLog " Warn member '$m': $($_.Exception.Message)" "DarkYellow" }
}
} else {
BgLog " Could not get M365 GroupId — owners/members not assigned" "DarkYellow"
}
} else {
# CommunicationSite — classic SharePoint groups
if ($owners.Count -gt 0) {
$ownerGrp = Get-PnPGroup | Where-Object { $_.Title -like "*Propri*" -or $_.Title -like "*Owner*" } | Select-Object -First 1
if ($ownerGrp) {
foreach ($o in $owners) {
try { Add-PnPGroupMember -LoginName $o -Group $ownerGrp.Title -ErrorAction SilentlyContinue } catch {}
}
}
}
if ($members.Count -gt 0) {
$memberGrp = Get-PnPGroup | Where-Object { $_.Title -like "*Membre*" -or $_.Title -like "*Member*" } | Select-Object -First 1
if ($memberGrp) {
foreach ($m in $members) {
try { Add-PnPGroupMember -LoginName $m -Group $memberGrp.Title -ErrorAction SilentlyContinue } catch {}
}
}
}
}
# Apply template if specified # Apply template if specified
if ($tplName -and $Params.Templates.ContainsKey($tplName)) { if ($tplName -and $Params.Templates.ContainsKey($tplName)) {
$tpl = $Params.Templates[$tplName] $tpl = $Params.Templates[$tplName]
@@ -5380,30 +5468,7 @@ $btnBulkCreate.Add_Click({
} }
} }
# Add members
if ($members.Count -gt 0) {
$memberGroup = Get-PnPGroup | Where-Object { $_.Title -like "*Membres*" -or $_.Title -like "*Members*" } | Select-Object -First 1
if ($memberGroup) {
foreach ($m in $members) {
try {
Add-PnPGroupMember -LoginName $m -Group $memberGroup.Title -ErrorAction SilentlyContinue
} catch {}
}
BgLog " $($members.Count) member(s) added." "Cyan"
}
}
# Add owners for Communication sites (TeamSite owners set at creation)
if (-not $isTeam -and $owners.Count -gt 0) {
$ownerGroup = Get-PnPGroup | Where-Object { $_.Title -like "*Propri*" -or $_.Title -like "*Owner*" } | Select-Object -First 1
if ($ownerGroup) {
foreach ($o in $owners) {
try {
Add-PnPGroupMember -LoginName $o -Group $ownerGroup.Title -ErrorAction SilentlyContinue
} catch {}
}
}
}
$Sync.Queue.Enqueue(@{ Text = "##STATUS##"; Index = ($idx - 1); Value = "OK" }) $Sync.Queue.Enqueue(@{ Text = "##STATUS##"; Index = ($idx - 1); Value = "OK" })
$Sync.CreatedSites.Add([PSCustomObject]@{ $Sync.CreatedSites.Add([PSCustomObject]@{

View File

@@ -1,5 +1,4 @@
# Features à ajouter : # Features à ajouter :
- Sauvegarde du contexte d'authentification en plus des profils - Sauvegarde du contexte d'authentification en plus des profils
- Possibilité de demander la liste de site auquels un user precis a acces - Possibilité de demander la liste de site auquels un user precis a acces
- Copie de site à site
- Barre de recherche dans les fichiers HTML exportés - Barre de recherche dans les fichiers HTML exportés

View File

@@ -0,0 +1,8 @@
Email
user1@contoso.com
user2@contoso.com
user3@contoso.com
manager1@contoso.com
designer@contoso.com
analyste@contoso.com
stagiaire@contoso.com
1 Email
2 user1@contoso.com
3 user2@contoso.com
4 user3@contoso.com
5 manager1@contoso.com
6 designer@contoso.com
7 analyste@contoso.com
8 stagiaire@contoso.com

View File

@@ -0,0 +1,6 @@
Name;Alias;Type;Template;Owners;Members
Projet Alpha;projet-alpha;Team;;admin@contoso.com;user1@contoso.com, user2@contoso.com
Projet Beta;;Team;;admin@contoso.com;user3@contoso.com, user4@contoso.com
Communication RH;;Communication;;rh-admin@contoso.com;manager1@contoso.com, manager2@contoso.com
Equipe Marketing;equipe-marketing;Team;;marketing-lead@contoso.com;designer@contoso.com, redacteur@contoso.com
Portail Intranet;;Communication;;it-admin@contoso.com;
1 Name Alias Type Template Owners Members
2 Projet Alpha projet-alpha Team admin@contoso.com user1@contoso.com, user2@contoso.com
3 Projet Beta Team admin@contoso.com user3@contoso.com, user4@contoso.com
4 Communication RH Communication rh-admin@contoso.com manager1@contoso.com, manager2@contoso.com
5 Equipe Marketing equipe-marketing Team marketing-lead@contoso.com designer@contoso.com, redacteur@contoso.com
6 Portail Intranet Communication it-admin@contoso.com