diff --git a/SharepointToolbox/Localization/Strings.Designer.cs b/SharepointToolbox/Localization/Strings.Designer.cs
index ffac79b..cd99ebc 100644
--- a/SharepointToolbox/Localization/Strings.Designer.cs
+++ b/SharepointToolbox/Localization/Strings.Designer.cs
@@ -136,5 +136,96 @@ namespace SharepointToolbox.Localization {
public static string ph_dup_lib => ResourceManager.GetString("ph.dup.lib", resourceCulture) ?? string.Empty;
public static string btn_run_scan => ResourceManager.GetString("btn.run.scan", resourceCulture) ?? string.Empty;
public static string btn_open_results => ResourceManager.GetString("btn.open.results", resourceCulture) ?? string.Empty;
+
+ // Phase 4: Tab headers
+ public static string tab_transfer => ResourceManager.GetString("tab.transfer", resourceCulture) ?? string.Empty;
+ public static string tab_bulkMembers => ResourceManager.GetString("tab.bulkMembers", resourceCulture) ?? string.Empty;
+ public static string tab_bulkSites => ResourceManager.GetString("tab.bulkSites", resourceCulture) ?? string.Empty;
+ public static string tab_folderStructure => ResourceManager.GetString("tab.folderStructure", resourceCulture) ?? string.Empty;
+
+ // Phase 4: Transfer tab
+ public static string transfer_sourcesite => ResourceManager.GetString("transfer.sourcesite", resourceCulture) ?? string.Empty;
+ public static string transfer_destsite => ResourceManager.GetString("transfer.destsite", resourceCulture) ?? string.Empty;
+ public static string transfer_sourcelibrary => ResourceManager.GetString("transfer.sourcelibrary", resourceCulture) ?? string.Empty;
+ public static string transfer_destlibrary => ResourceManager.GetString("transfer.destlibrary", resourceCulture) ?? string.Empty;
+ public static string transfer_sourcefolder => ResourceManager.GetString("transfer.sourcefolder", resourceCulture) ?? string.Empty;
+ public static string transfer_destfolder => ResourceManager.GetString("transfer.destfolder", resourceCulture) ?? string.Empty;
+ public static string transfer_mode => ResourceManager.GetString("transfer.mode", resourceCulture) ?? string.Empty;
+ public static string transfer_mode_copy => ResourceManager.GetString("transfer.mode.copy", resourceCulture) ?? string.Empty;
+ public static string transfer_mode_move => ResourceManager.GetString("transfer.mode.move", resourceCulture) ?? string.Empty;
+ public static string transfer_conflict => ResourceManager.GetString("transfer.conflict", resourceCulture) ?? string.Empty;
+ public static string transfer_conflict_skip => ResourceManager.GetString("transfer.conflict.skip", resourceCulture) ?? string.Empty;
+ public static string transfer_conflict_overwrite => ResourceManager.GetString("transfer.conflict.overwrite", resourceCulture) ?? string.Empty;
+ public static string transfer_conflict_rename => ResourceManager.GetString("transfer.conflict.rename", resourceCulture) ?? string.Empty;
+ public static string transfer_browse => ResourceManager.GetString("transfer.browse", resourceCulture) ?? string.Empty;
+ public static string transfer_start => ResourceManager.GetString("transfer.start", resourceCulture) ?? string.Empty;
+ public static string transfer_nofiles => ResourceManager.GetString("transfer.nofiles", resourceCulture) ?? string.Empty;
+
+ // Phase 4: Bulk Members tab
+ public static string bulkmembers_import => ResourceManager.GetString("bulkmembers.import", resourceCulture) ?? string.Empty;
+ public static string bulkmembers_example => ResourceManager.GetString("bulkmembers.example", resourceCulture) ?? string.Empty;
+ public static string bulkmembers_execute => ResourceManager.GetString("bulkmembers.execute", resourceCulture) ?? string.Empty;
+ public static string bulkmembers_preview => ResourceManager.GetString("bulkmembers.preview", resourceCulture) ?? string.Empty;
+ public static string bulkmembers_groupname => ResourceManager.GetString("bulkmembers.groupname", resourceCulture) ?? string.Empty;
+ public static string bulkmembers_groupurl => ResourceManager.GetString("bulkmembers.groupurl", resourceCulture) ?? string.Empty;
+ public static string bulkmembers_email => ResourceManager.GetString("bulkmembers.email", resourceCulture) ?? string.Empty;
+ public static string bulkmembers_role => ResourceManager.GetString("bulkmembers.role", resourceCulture) ?? string.Empty;
+
+ // Phase 4: Bulk Sites tab
+ public static string bulksites_import => ResourceManager.GetString("bulksites.import", resourceCulture) ?? string.Empty;
+ public static string bulksites_example => ResourceManager.GetString("bulksites.example", resourceCulture) ?? string.Empty;
+ public static string bulksites_execute => ResourceManager.GetString("bulksites.execute", resourceCulture) ?? string.Empty;
+ public static string bulksites_preview => ResourceManager.GetString("bulksites.preview", resourceCulture) ?? string.Empty;
+ public static string bulksites_name => ResourceManager.GetString("bulksites.name", resourceCulture) ?? string.Empty;
+ public static string bulksites_alias => ResourceManager.GetString("bulksites.alias", resourceCulture) ?? string.Empty;
+ public static string bulksites_type => ResourceManager.GetString("bulksites.type", resourceCulture) ?? string.Empty;
+ public static string bulksites_owners => ResourceManager.GetString("bulksites.owners", resourceCulture) ?? string.Empty;
+ public static string bulksites_members => ResourceManager.GetString("bulksites.members", resourceCulture) ?? string.Empty;
+
+ // Phase 4: Folder Structure tab
+ public static string folderstruct_import => ResourceManager.GetString("folderstruct.import", resourceCulture) ?? string.Empty;
+ public static string folderstruct_example => ResourceManager.GetString("folderstruct.example", resourceCulture) ?? string.Empty;
+ public static string folderstruct_execute => ResourceManager.GetString("folderstruct.execute", resourceCulture) ?? string.Empty;
+ public static string folderstruct_preview => ResourceManager.GetString("folderstruct.preview", resourceCulture) ?? string.Empty;
+ public static string folderstruct_library => ResourceManager.GetString("folderstruct.library", resourceCulture) ?? string.Empty;
+ public static string folderstruct_siteurl => ResourceManager.GetString("folderstruct.siteurl", resourceCulture) ?? string.Empty;
+
+ // Phase 4: Templates tab
+ public static string templates_list => ResourceManager.GetString("templates.list", resourceCulture) ?? string.Empty;
+ public static string templates_capture => ResourceManager.GetString("templates.capture", resourceCulture) ?? string.Empty;
+ public static string templates_apply => ResourceManager.GetString("templates.apply", resourceCulture) ?? string.Empty;
+ public static string templates_rename => ResourceManager.GetString("templates.rename", resourceCulture) ?? string.Empty;
+ public static string templates_delete => ResourceManager.GetString("templates.delete", resourceCulture) ?? string.Empty;
+ public static string templates_siteurl => ResourceManager.GetString("templates.siteurl", resourceCulture) ?? string.Empty;
+ public static string templates_name => ResourceManager.GetString("templates.name", resourceCulture) ?? string.Empty;
+ public static string templates_newtitle => ResourceManager.GetString("templates.newtitle", resourceCulture) ?? string.Empty;
+ public static string templates_newalias => ResourceManager.GetString("templates.newalias", resourceCulture) ?? string.Empty;
+ public static string templates_options => ResourceManager.GetString("templates.options", resourceCulture) ?? string.Empty;
+ public static string templates_opt_libraries => ResourceManager.GetString("templates.opt.libraries", resourceCulture) ?? string.Empty;
+ public static string templates_opt_folders => ResourceManager.GetString("templates.opt.folders", resourceCulture) ?? string.Empty;
+ public static string templates_opt_permissions => ResourceManager.GetString("templates.opt.permissions", resourceCulture) ?? string.Empty;
+ public static string templates_opt_logo => ResourceManager.GetString("templates.opt.logo", resourceCulture) ?? string.Empty;
+ public static string templates_opt_settings => ResourceManager.GetString("templates.opt.settings", resourceCulture) ?? string.Empty;
+ public static string templates_empty => ResourceManager.GetString("templates.empty", resourceCulture) ?? string.Empty;
+
+ // Phase 4: Shared bulk operation strings
+ public static string bulk_confirm_title => ResourceManager.GetString("bulk.confirm.title", resourceCulture) ?? string.Empty;
+ public static string bulk_confirm_proceed => ResourceManager.GetString("bulk.confirm.proceed", resourceCulture) ?? string.Empty;
+ public static string bulk_confirm_cancel => ResourceManager.GetString("bulk.confirm.cancel", resourceCulture) ?? string.Empty;
+ public static string bulk_confirm_message => ResourceManager.GetString("bulk.confirm.message", resourceCulture) ?? string.Empty;
+ public static string bulk_result_success => ResourceManager.GetString("bulk.result.success", resourceCulture) ?? string.Empty;
+ public static string bulk_result_allfailed => ResourceManager.GetString("bulk.result.allfailed", resourceCulture) ?? string.Empty;
+ public static string bulk_result_allsuccess => ResourceManager.GetString("bulk.result.allsuccess", resourceCulture) ?? string.Empty;
+ public static string bulk_exportfailed => ResourceManager.GetString("bulk.exportfailed", resourceCulture) ?? string.Empty;
+ public static string bulk_retryfailed => ResourceManager.GetString("bulk.retryfailed", resourceCulture) ?? string.Empty;
+ public static string bulk_validation_invalid => ResourceManager.GetString("bulk.validation.invalid", resourceCulture) ?? string.Empty;
+ public static string bulk_csvimport_title => ResourceManager.GetString("bulk.csvimport.title", resourceCulture) ?? string.Empty;
+ public static string bulk_csvimport_filter => ResourceManager.GetString("bulk.csvimport.filter", resourceCulture) ?? string.Empty;
+
+ // Phase 4: Folder browser dialog
+ public static string folderbrowser_title => ResourceManager.GetString("folderbrowser.title", resourceCulture) ?? string.Empty;
+ public static string folderbrowser_loading => ResourceManager.GetString("folderbrowser.loading", resourceCulture) ?? string.Empty;
+ public static string folderbrowser_select => ResourceManager.GetString("folderbrowser.select", resourceCulture) ?? string.Empty;
+ public static string folderbrowser_cancel => ResourceManager.GetString("folderbrowser.cancel", resourceCulture) ?? string.Empty;
}
}
diff --git a/SharepointToolbox/Localization/Strings.fr.resx b/SharepointToolbox/Localization/Strings.fr.resx
index 8e2d1d5..09b0066 100644
--- a/SharepointToolbox/Localization/Strings.fr.resx
+++ b/SharepointToolbox/Localization/Strings.fr.resx
@@ -221,4 +221,87 @@
Tous (laisser vide)
Lancer l'analyse
Ouvrir les résultats
+
+ Transfert
+ Ajout en masse
+ Sites en masse
+ Structure de dossiers
+
+ Site source
+ Site destination
+ Bibliotheque source
+ Bibliotheque destination
+ Dossier source
+ Dossier destination
+ Mode de transfert
+ Copier
+ Deplacer
+ Politique de conflit
+ Ignorer
+ Ecraser
+ Renommer (ajouter suffixe)
+ Parcourir...
+ Demarrer le transfert
+ Aucun fichier a transferer.
+
+ Importer CSV
+ Charger l'exemple
+ Ajouter les membres
+ Apercu ({0} lignes, {1} valides, {2} invalides)
+ Nom du groupe
+ URL du groupe
+ Courriel
+ Role
+
+ Importer CSV
+ Charger l'exemple
+ Creer les sites
+ Apercu ({0} lignes, {1} valides, {2} invalides)
+ Nom
+ Alias
+ Type
+ Proprietaires
+ Membres
+
+ Importer CSV
+ Charger l'exemple
+ Creer les dossiers
+ Apercu ({0} dossiers a creer)
+ Bibliotheque cible
+ URL du site
+
+ Modeles enregistres
+ Capturer un modele
+ Appliquer le modele
+ Renommer
+ Supprimer
+ URL du site source
+ Nom du modele
+ Titre du nouveau site
+ Alias du nouveau site
+ Options de capture
+ Bibliotheques
+ Dossiers
+ Groupes de permissions
+ Logo du site
+ Parametres du site
+ Aucun modele enregistre.
+
+ Confirmer l'operation
+ Continuer
+ Annuler
+ {0} — Continuer ?
+ Termine : {0} reussis, {1} echoues
+ Les {0} elements ont echoue.
+ Les {0} elements ont ete traites avec succes.
+ Exporter les elements echoues
+ Reessayer les echecs
+ {0} lignes contiennent des erreurs. Corrigez et reimportez.
+ Selectionner un fichier CSV
+ Fichiers CSV (*.csv)|*.csv
+
+ Selectionner un dossier
+ Chargement de l'arborescence...
+ Selectionner
+ Annuler
diff --git a/SharepointToolbox/Localization/Strings.resx b/SharepointToolbox/Localization/Strings.resx
index 74e3864..0390132 100644
--- a/SharepointToolbox/Localization/Strings.resx
+++ b/SharepointToolbox/Localization/Strings.resx
@@ -221,4 +221,87 @@
All (leave empty)
Run Scan
Open Results
+
+ Transfer
+ Bulk Members
+ Bulk Sites
+ Folder Structure
+
+ Source Site
+ Destination Site
+ Source Library
+ Destination Library
+ Source Folder
+ Destination Folder
+ Transfer Mode
+ Copy
+ Move
+ Conflict Policy
+ Skip
+ Overwrite
+ Rename (append suffix)
+ Browse...
+ Start Transfer
+ No files found to transfer.
+
+ Import CSV
+ Load Example
+ Add Members
+ Preview ({0} rows, {1} valid, {2} invalid)
+ Group Name
+ Group URL
+ Email
+ Role
+
+ Import CSV
+ Load Example
+ Create Sites
+ Preview ({0} rows, {1} valid, {2} invalid)
+ Name
+ Alias
+ Type
+ Owners
+ Members
+
+ Import CSV
+ Load Example
+ Create Folders
+ Preview ({0} folders to create)
+ Target Library
+ Site URL
+
+ Saved Templates
+ Capture Template
+ Apply Template
+ Rename
+ Delete
+ Source Site URL
+ Template Name
+ New Site Title
+ New Site Alias
+ Capture Options
+ Libraries
+ Folders
+ Permission Groups
+ Site Logo
+ Site Settings
+ No templates saved yet.
+
+ Confirm Operation
+ Proceed
+ Cancel
+ {0} — Proceed?
+ Completed: {0} succeeded, {1} failed
+ All {0} items failed.
+ All {0} items completed successfully.
+ Export Failed Items
+ Retry Failed
+ {0} rows have validation errors. Fix and re-import.
+ Select CSV File
+ CSV Files (*.csv)|*.csv
+
+ Select Folder
+ Loading folder tree...
+ Select
+ Cancel
diff --git a/SharepointToolbox/Resources/bulk_add_members.csv b/SharepointToolbox/Resources/bulk_add_members.csv
new file mode 100644
index 0000000..6dcca84
--- /dev/null
+++ b/SharepointToolbox/Resources/bulk_add_members.csv
@@ -0,0 +1,8 @@
+GroupName,GroupUrl,Email,Role
+Marketing Team,https://contoso.sharepoint.com/sites/Marketing,user1@contoso.com,Member
+Marketing Team,https://contoso.sharepoint.com/sites/Marketing,manager@contoso.com,Owner
+HR Team,https://contoso.sharepoint.com/sites/HR,hr-admin@contoso.com,Owner
+HR Team,https://contoso.sharepoint.com/sites/HR,recruiter@contoso.com,Member
+HR Team,https://contoso.sharepoint.com/sites/HR,analyst@contoso.com,Member
+IT Support,https://contoso.sharepoint.com/sites/IT,sysadmin@contoso.com,Owner
+IT Support,https://contoso.sharepoint.com/sites/IT,helpdesk@contoso.com,Member
diff --git a/SharepointToolbox/Resources/bulk_create_sites.csv b/SharepointToolbox/Resources/bulk_create_sites.csv
new file mode 100644
index 0000000..c2e9b0a
--- /dev/null
+++ b/SharepointToolbox/Resources/bulk_create_sites.csv
@@ -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;projet-beta;Team;;admin@contoso.com;user3@contoso.com, user4@contoso.com
+Communication RH;comm-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;portail-intranet;Communication;;it-admin@contoso.com;
diff --git a/SharepointToolbox/Resources/folder_structure.csv b/SharepointToolbox/Resources/folder_structure.csv
new file mode 100644
index 0000000..0d09f19
--- /dev/null
+++ b/SharepointToolbox/Resources/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/SharepointToolbox/SharepointToolbox.csproj b/SharepointToolbox/SharepointToolbox.csproj
index c635ea4..2829562 100644
--- a/SharepointToolbox/SharepointToolbox.csproj
+++ b/SharepointToolbox/SharepointToolbox.csproj
@@ -25,6 +25,12 @@
+
+
+
+
+
+
diff --git a/SharepointToolbox/Views/Dialogs/ConfirmBulkOperationDialog.xaml b/SharepointToolbox/Views/Dialogs/ConfirmBulkOperationDialog.xaml
new file mode 100644
index 0000000..dc8180f
--- /dev/null
+++ b/SharepointToolbox/Views/Dialogs/ConfirmBulkOperationDialog.xaml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/SharepointToolbox/Views/Dialogs/ConfirmBulkOperationDialog.xaml.cs b/SharepointToolbox/Views/Dialogs/ConfirmBulkOperationDialog.xaml.cs
new file mode 100644
index 0000000..61635c9
--- /dev/null
+++ b/SharepointToolbox/Views/Dialogs/ConfirmBulkOperationDialog.xaml.cs
@@ -0,0 +1,28 @@
+using System.Windows;
+
+namespace SharepointToolbox.Views.Dialogs;
+
+public partial class ConfirmBulkOperationDialog : Window
+{
+ public bool IsConfirmed { get; private set; }
+
+ public ConfirmBulkOperationDialog(string message)
+ {
+ InitializeComponent();
+ MessageText.Text = message;
+ }
+
+ private void Proceed_Click(object sender, RoutedEventArgs e)
+ {
+ IsConfirmed = true;
+ DialogResult = true;
+ Close();
+ }
+
+ private void Cancel_Click(object sender, RoutedEventArgs e)
+ {
+ IsConfirmed = false;
+ DialogResult = false;
+ Close();
+ }
+}
diff --git a/SharepointToolbox/Views/Dialogs/FolderBrowserDialog.xaml b/SharepointToolbox/Views/Dialogs/FolderBrowserDialog.xaml
new file mode 100644
index 0000000..62c9633
--- /dev/null
+++ b/SharepointToolbox/Views/Dialogs/FolderBrowserDialog.xaml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/SharepointToolbox/Views/Dialogs/FolderBrowserDialog.xaml.cs b/SharepointToolbox/Views/Dialogs/FolderBrowserDialog.xaml.cs
new file mode 100644
index 0000000..df27f21
--- /dev/null
+++ b/SharepointToolbox/Views/Dialogs/FolderBrowserDialog.xaml.cs
@@ -0,0 +1,125 @@
+using System.Windows;
+using System.Windows.Controls;
+using Microsoft.SharePoint.Client;
+using SharepointToolbox.Core.Helpers;
+
+namespace SharepointToolbox.Views.Dialogs;
+
+public partial class FolderBrowserDialog : Window
+{
+ private readonly ClientContext _ctx;
+ public string SelectedLibrary { get; private set; } = string.Empty;
+ public string SelectedFolderPath { get; private set; } = string.Empty;
+
+ public FolderBrowserDialog(ClientContext ctx)
+ {
+ InitializeComponent();
+ _ctx = ctx;
+ Loaded += OnLoaded;
+ }
+
+ private async void OnLoaded(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ // Load libraries
+ var web = _ctx.Web;
+ var lists = _ctx.LoadQuery(web.Lists
+ .Include(l => l.Title, l => l.Hidden, l => l.BaseType, l => l.RootFolder)
+ .Where(l => !l.Hidden && l.BaseType == BaseType.DocumentLibrary));
+ var progress = new Progress();
+ await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(_ctx, progress, CancellationToken.None);
+
+ foreach (var list in lists)
+ {
+ var libNode = new TreeViewItem
+ {
+ Header = list.Title,
+ Tag = new FolderNodeInfo(list.Title, string.Empty),
+ };
+ // Add dummy child for expand arrow
+ libNode.Items.Add(new TreeViewItem { Header = "Loading..." });
+ libNode.Expanded += LibNode_Expanded;
+ FolderTree.Items.Add(libNode);
+ }
+
+ StatusText.Text = $"{FolderTree.Items.Count} libraries loaded.";
+ }
+ catch (Exception ex)
+ {
+ StatusText.Text = $"Error: {ex.Message}";
+ }
+ }
+
+ private async void LibNode_Expanded(object sender, RoutedEventArgs e)
+ {
+ if (sender is not TreeViewItem node || node.Tag is not FolderNodeInfo info)
+ return;
+
+ // Only load children once
+ if (node.Items.Count == 1 && node.Items[0] is TreeViewItem dummy && dummy.Header?.ToString() == "Loading...")
+ {
+ node.Items.Clear();
+ try
+ {
+ var folderUrl = string.IsNullOrEmpty(info.FolderPath)
+ ? GetLibraryRootUrl(info.LibraryTitle)
+ : info.FolderPath;
+
+ var folder = _ctx.Web.GetFolderByServerRelativeUrl(folderUrl);
+ _ctx.Load(folder, f => f.Folders.Include(sf => sf.Name, sf => sf.ServerRelativeUrl));
+ var progress = new Progress();
+ await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(_ctx, progress, CancellationToken.None);
+
+ foreach (var subFolder in folder.Folders)
+ {
+ if (subFolder.Name.StartsWith("_") || subFolder.Name == "Forms")
+ continue;
+
+ var childNode = new TreeViewItem
+ {
+ Header = subFolder.Name,
+ Tag = new FolderNodeInfo(info.LibraryTitle, subFolder.ServerRelativeUrl),
+ };
+ childNode.Items.Add(new TreeViewItem { Header = "Loading..." });
+ childNode.Expanded += LibNode_Expanded;
+ node.Items.Add(childNode);
+ }
+ }
+ catch (Exception ex)
+ {
+ node.Items.Add(new TreeViewItem { Header = $"Error: {ex.Message}" });
+ }
+ }
+ }
+
+ private string GetLibraryRootUrl(string libraryTitle)
+ {
+ var uri = new Uri(_ctx.Url);
+ return $"{uri.AbsolutePath.TrimEnd('/')}/{libraryTitle}";
+ }
+
+ private void FolderTree_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs