From 086804edf93526ae6d6fd2cc349ca21d7f5a4b74 Mon Sep 17 00:00:00 2001 From: Kawa Date: Mon, 16 Mar 2026 16:39:37 +0100 Subject: [PATCH] Added functionnality : you can vcreate a whole folder tree by importing a CSV (see examples fodler) --- Sharepoint_ToolBox.ps1 | 199 +++++++++++++++++++++++++++++++++- examples/folder_structure.csv | 20 ++++ lang/fr.json | 15 ++- 3 files changed, 232 insertions(+), 2 deletions(-) create mode 100644 examples/folder_structure.csv diff --git a/Sharepoint_ToolBox.ps1 b/Sharepoint_ToolBox.ps1 index f641f8b..f6ddb10 100644 --- a/Sharepoint_ToolBox.ps1 +++ b/Sharepoint_ToolBox.ps1 @@ -2854,6 +2854,18 @@ $script:LangDefault = @{ "bulk.status.creating" = "Creating..." "bulk.status.ok" = "OK" "bulk.status.error" = "Error" + "tab.structure" = " Structure " + "grp.struct.csv" = "CSV Import" + "lbl.struct.desc" = "Import a CSV to create a folder tree. Each column represents a depth level." + "btn.struct.csv" = "Load CSV..." + "grp.struct.preview" = "Preview" + "grp.struct.target" = "Target" + "lbl.struct.library" = "Target library:" + "ph.struct.library" = "Shared Documents" + "btn.struct.create" = "Create Structure" + "btn.struct.clear" = "Clear" + "struct.col.path" = "Full Path" + "struct.col.depth" = "Depth" } $script:Lang = $null # null = use LangDefault @@ -3606,7 +3618,66 @@ $btnBulkCreate.Size = New-Object System.Drawing.Size(200, 34) $tabBulk.Controls.AddRange(@($grpBulkList, $btnBulkCreate)) -$tabs.TabPages.AddRange(@($tabPerms, $tabStorage, $tabTemplates, $tabSearch, $tabDupes, $tabTransfer, $tabBulk)) +# ══════════════════════════════════════════════════════════════════════════════ +# Tab 8 – Structure (folder tree from CSV) +# ══════════════════════════════════════════════════════════════════════════════ +$tabStruct = New-Object System.Windows.Forms.TabPage +$tabStruct.Text = T "tab.structure" + +# ── CSV import ───────────────────────────────────────────────────────────── +$grpStructCsv = New-Group (T "grp.struct.csv") 10 4 620 60 + +$lblStructDesc = New-Object System.Windows.Forms.Label +$lblStructDesc.Text = T "lbl.struct.desc" +$lblStructDesc.Location = New-Object System.Drawing.Point(10, 18) +$lblStructDesc.Size = New-Object System.Drawing.Size(460, 32) + +$btnStructCsv = New-Object System.Windows.Forms.Button +$btnStructCsv.Text = T "btn.struct.csv" +$btnStructCsv.Location = New-Object System.Drawing.Point(490, 22) +$btnStructCsv.Size = New-Object System.Drawing.Size(118, 28) + +$grpStructCsv.Controls.AddRange(@($lblStructDesc, $btnStructCsv)) + +# ── Preview ──────────────────────────────────────────────────────────────── +$grpStructPreview = New-Group (T "grp.struct.preview") 10 68 620 140 + +$tvStruct = New-Object System.Windows.Forms.TreeView +$tvStruct.Location = New-Object System.Drawing.Point(10, 18) +$tvStruct.Size = New-Object System.Drawing.Size(598, 112) +$tvStruct.Font = New-Object System.Drawing.Font("Segoe UI", 9) +$tvStruct.ShowLines = $true +$tvStruct.ShowPlusMinus = $true + +$grpStructPreview.Controls.Add($tvStruct) + +# ── Target ───────────────────────────────────────────────────────────────── +$grpStructTarget = New-Group (T "grp.struct.target") 10 212 620 52 + +$lblStructLib = New-Object System.Windows.Forms.Label +$lblStructLib.Text = T "lbl.struct.library" +$lblStructLib.Location = New-Object System.Drawing.Point(10, 22) +$lblStructLib.Size = New-Object System.Drawing.Size(120, 20) + +$txtStructLib = New-Object System.Windows.Forms.TextBox +$txtStructLib.Location = New-Object System.Drawing.Point(134, 20) +$txtStructLib.Size = New-Object System.Drawing.Size(300, 22) +$txtStructLib.PlaceholderText = T "ph.struct.library" + +$grpStructTarget.Controls.AddRange(@($lblStructLib, $txtStructLib)) + +# ── Buttons ──────────────────────────────────────────────────────────────── +$btnStructCreate = New-ActionBtn (T "btn.struct.create") 10 270 ([System.Drawing.Color]::FromArgb(0, 120, 212)) +$btnStructCreate.Size = New-Object System.Drawing.Size(200, 34) + +$btnStructClear = New-Object System.Windows.Forms.Button +$btnStructClear.Text = T "btn.struct.clear" +$btnStructClear.Location = New-Object System.Drawing.Point(220, 270) +$btnStructClear.Size = New-Object System.Drawing.Size(90, 34) + +$tabStruct.Controls.AddRange(@($grpStructCsv, $grpStructPreview, $grpStructTarget, $btnStructCreate, $btnStructClear)) + +$tabs.TabPages.AddRange(@($tabPerms, $tabStorage, $tabTemplates, $tabSearch, $tabDupes, $tabTransfer, $tabBulk, $tabStruct)) # ── Progress bar ─────────────────────────────────────────────────────────────── $progressBar = New-Object System.Windows.Forms.ProgressBar @@ -3793,6 +3864,11 @@ $_reg = { & $_reg $script:i18nMap $btnBulkRemove "btn.bulk.remove" & $_reg $script:i18nMap $btnBulkClear "btn.bulk.clear" & $_reg $script:i18nMap $btnBulkCreate "btn.bulk.create" +& $_reg $script:i18nMap $lblStructDesc "lbl.struct.desc" +& $_reg $script:i18nMap $btnStructCsv "btn.struct.csv" +& $_reg $script:i18nMap $lblStructLib "lbl.struct.library" +& $_reg $script:i18nMap $btnStructCreate "btn.struct.create" +& $_reg $script:i18nMap $btnStructClear "btn.struct.clear" # Tab pages & $_reg $script:i18nTabs $tabPerms "tab.perms" @@ -3802,6 +3878,7 @@ $_reg = { & $_reg $script:i18nTabs $tabDupes "tab.dupes" & $_reg $script:i18nTabs $tabTransfer "tab.transfer" & $_reg $script:i18nTabs $tabBulk "tab.bulk" +& $_reg $script:i18nTabs $tabStruct "tab.structure" # Menu items & $_reg $script:i18nMenus $menuSettings "menu.settings" @@ -3816,6 +3893,7 @@ $script:i18nPlaceholders = [System.Collections.Generic.Dictionary[string,object] & $_reg $script:i18nPlaceholders $txtSrchModBy "ph.modified.by" & $_reg $script:i18nPlaceholders $txtSrchLib "ph.library" & $_reg $script:i18nPlaceholders $txtDupLib "ph.dup.lib" +& $_reg $script:i18nPlaceholders $txtStructLib "ph.struct.library" & $_reg $script:i18nPlaceholders $txtXferSrcSite "ph.xfer.site" & $_reg $script:i18nPlaceholders $txtXferSrcLib "ph.xfer.library" & $_reg $script:i18nPlaceholders $txtXferDstSite "ph.xfer.site" @@ -5578,6 +5656,125 @@ $btnBulkCreate.Add_Click({ #endregion +#region ===== Structure (folder tree from CSV) ===== + +# Store the parsed folder paths +$script:_StructPaths = @() + +function Build-StructTree([string]$csvPath) { + # Auto-detect delimiter + $raw = Get-Content $csvPath -Raw + $delim = if ($raw -match ';') { ';' } else { ',' } + $rows = Import-Csv $csvPath -Delimiter $delim + + $paths = [System.Collections.Generic.List[string]]::new() + foreach ($r in $rows) { + $cols = @($r.PSObject.Properties | ForEach-Object { "$($_.Value)".Trim() }) + # Build path from non-empty columns + $parts = @($cols | Where-Object { $_ -ne '' }) + if ($parts.Count -gt 0) { + # Add all intermediate paths to ensure parents exist + for ($i = 1; $i -le $parts.Count; $i++) { + $p = ($parts[0..($i-1)] -join '/') + if (-not $paths.Contains($p)) { $paths.Add($p) } + } + } + } + $script:_StructPaths = @($paths | Sort-Object) + return $script:_StructPaths +} + +function Populate-StructTreeView([string[]]$paths) { + $tvStruct.Nodes.Clear() + $nodeMap = @{} + foreach ($p in $paths) { + $parts = $p -split '/' + $parentKey = if ($parts.Count -gt 1) { ($parts[0..($parts.Count - 2)] -join '/') } else { '' } + $name = $parts[-1] + $node = New-Object System.Windows.Forms.TreeNode($name) + $node.Tag = $p + if ($parentKey -and $nodeMap.ContainsKey($parentKey)) { + $nodeMap[$parentKey].Nodes.Add($node) | Out-Null + } else { + $tvStruct.Nodes.Add($node) | Out-Null + } + $nodeMap[$p] = $node + } + $tvStruct.ExpandAll() +} + +$btnStructCsv.Add_Click({ + $ofd = New-Object System.Windows.Forms.OpenFileDialog + $ofd.Filter = "CSV (*.csv)|*.csv|All (*.*)|*.*" + if ($ofd.ShowDialog($form) -ne "OK") { return } + try { + $paths = Build-StructTree $ofd.FileName + Populate-StructTreeView $paths + Write-Log "$($paths.Count) folder(s) loaded from CSV." "LightGreen" + } catch { + Write-Log "CSV error: $($_.Exception.Message)" "Red" + } +}) + +$btnStructClear.Add_Click({ + $tvStruct.Nodes.Clear() + $script:_StructPaths = @() + Write-Log "Structure cleared." "Gray" +}) + +$btnStructCreate.Add_Click({ + $siteUrl = $txtSiteUrl.Text.Trim() + $clientId = $txtClientId.Text.Trim() + $library = $txtStructLib.Text.Trim() + + if (-not $siteUrl) { Write-Log "Site URL required." "Red"; return } + if (-not $clientId) { Write-Log "Client ID required." "Red"; return } + if (-not $library) { Write-Log "Target library required." "Red"; return } + if ($script:_StructPaths.Count -eq 0) { Write-Log "No structure loaded. Load a CSV first." "Red"; return } + + $btnStructCreate.Enabled = $false + $btnStructCsv.Enabled = $false + Start-ProgressAnim + Write-Log "=== CREATING FOLDER STRUCTURE ===" "White" + Write-Log "Target: $siteUrl / $library" "Gray" + Write-Log "Folders to create: $($script:_StructPaths.Count)" "Gray" + Write-Log ("-" * 52) "DarkGray" + + try { + Connect-PnPOnline -Url $siteUrl -Interactive -ClientId $clientId + + # Get the library root + $list = Get-PnPList -Identity $library -ErrorAction Stop + $rf = Get-PnPProperty -ClientObject $list -Property RootFolder + $base = $rf.ServerRelativeUrl.TrimEnd('/') + + $ok = 0 + $err = 0 + $total = $script:_StructPaths.Count + foreach ($p in $script:_StructPaths) { + $folderPath = "$base/$p" + try { + # Resolve-PnPFolder creates the full path recursively + Resolve-PnPFolder -SiteRelativePath "$library/$p" -ErrorAction Stop | Out-Null + $ok++ + Write-Log " OK: $p" "LightGreen" + } catch { + $err++ + Write-Log " FAIL: $p — $($_.Exception.Message)" "Red" + } + } + Write-Log "=== STRUCTURE COMPLETE: $ok OK, $err error(s) ===" "White" + } catch { + Write-Log "Error: $($_.Exception.Message)" "Red" + } finally { + $btnStructCreate.Enabled = $true + $btnStructCsv.Enabled = $true + Stop-ProgressAnim + } +}) + +#endregion + # ── Initialisation : chargement des settings ─────────────────────────────── $_settings = Load-Settings $script:DataFolder = if ($_settings.dataFolder -and (Test-Path $_settings.dataFolder)) { diff --git a/examples/folder_structure.csv b/examples/folder_structure.csv new file mode 100644 index 0000000..0d09f19 --- /dev/null +++ b/examples/folder_structure.csv @@ -0,0 +1,20 @@ +Level1;Level2;Level3;Level4 +Administration;;; +Administration;Comptabilite;; +Administration;Comptabilite;Factures; +Administration;Comptabilite;Bilans; +Administration;Ressources Humaines;; +Administration;Ressources Humaines;Contrats; +Administration;Ressources Humaines;Fiches de paie; +Projets;;; +Projets;Projet Alpha;; +Projets;Projet Alpha;Documents; +Projets;Projet Alpha;Livrables; +Projets;Projet Beta;; +Projets;Projet Beta;Documents; +Communication;;; +Communication;Interne;; +Communication;Interne;Notes de service; +Communication;Externe;; +Communication;Externe;Communiques de presse; +Communication;Externe;Newsletter; diff --git a/lang/fr.json b/lang/fr.json index 734147c..c0347a6 100644 --- a/lang/fr.json +++ b/lang/fr.json @@ -137,5 +137,18 @@ "bulk.status.pending": "En attente", "bulk.status.creating": "Création...", "bulk.status.ok": "OK", - "bulk.status.error": "Erreur" + "bulk.status.error": "Erreur", + + "tab.structure": " Structure ", + "grp.struct.csv": "Import CSV", + "lbl.struct.desc": "Importez un CSV pour créer une arborescence. Chaque colonne représente un niveau de profondeur.", + "btn.struct.csv": "Charger CSV...", + "grp.struct.preview": "Aperçu", + "grp.struct.target": "Cible", + "lbl.struct.library": "Bibliothèque cible :", + "ph.struct.library": "Documents partagés", + "btn.struct.create": "Créer l'arborescence", + "btn.struct.clear": "Effacer", + "struct.col.path": "Chemin complet", + "struct.col.depth": "Profondeur" }