Files
Sharepoint-Toolbox/.planning/milestones/v1.0-phases/04-bulk-operations-and-provisioning/04-07-PLAN.md
Dev 655bb79a99
All checks were successful
Release zip package / release (push) Successful in 10s
chore: complete v1.0 milestone
Archive 5 phases (36 plans) to milestones/v1.0-phases/.
Archive roadmap, requirements, and audit to milestones/.
Evolve PROJECT.md with shipped state and validated requirements.
Collapse ROADMAP.md to one-line milestone summary.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 09:15:14 +02:00

21 KiB

phase, plan, title, status, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan title status wave depends_on files_modified autonomous requirements must_haves
04 07 Localization + Shared Dialogs + Example CSV Resources pending 2
04-02
04-03
04-04
04-05
04-06
SharepointToolbox/Localization/Strings.resx
SharepointToolbox/Localization/Strings.fr.resx
SharepointToolbox/Localization/Strings.Designer.cs
SharepointToolbox/Views/Dialogs/ConfirmBulkOperationDialog.xaml
SharepointToolbox/Views/Dialogs/ConfirmBulkOperationDialog.xaml.cs
SharepointToolbox/Views/Dialogs/FolderBrowserDialog.xaml
SharepointToolbox/Views/Dialogs/FolderBrowserDialog.xaml.cs
SharepointToolbox/Resources/bulk_add_members.csv
SharepointToolbox/Resources/bulk_create_sites.csv
SharepointToolbox/Resources/folder_structure.csv
SharepointToolbox/SharepointToolbox.csproj
true
FOLD-02
truths artifacts key_links
All Phase 4 EN/FR localization keys exist in Strings.resx and Strings.fr.resx
Strings.Designer.cs has ResourceManager accessor for new keys
ConfirmBulkOperationDialog shows operation summary and Proceed/Cancel buttons
FolderBrowserDialog shows a TreeView of SharePoint libraries and folders
Example CSV files are embedded resources accessible at runtime
path provides
SharepointToolbox/Views/Dialogs/ConfirmBulkOperationDialog.xaml Pre-write confirmation dialog
path provides
SharepointToolbox/Views/Dialogs/FolderBrowserDialog.xaml Library/folder tree browser for file transfer
path provides
SharepointToolbox/Resources/bulk_add_members.csv Example CSV for bulk member addition
from to via pattern
ConfirmBulkOperationDialog.xaml.cs TranslationSource localized button text and labels TranslationSource.Instance
from to via pattern
Strings.Designer.cs Strings.resx ResourceManager property accessor ResourceManager

Plan 04-07: Localization + Shared Dialogs + Example CSV Resources

Goal

Add all Phase 4 EN/FR localization keys, create the ConfirmBulkOperationDialog and FolderBrowserDialog XAML dialogs, and bundle example CSV files as embedded resources. This plan creates shared infrastructure needed by all 5 tab ViewModels/Views.

Context

Localization follows the established pattern: keys in Strings.resx (EN) and Strings.fr.resx (FR), accessor methods in Strings.Designer.cs (maintained manually per Phase 1 decision). UI strings use TranslationSource.Instance[key] in XAML.

Existing dialogs: ProfileManagementDialog and SitePickerDialog in Views/Dialogs/.

Example CSVs exist in /examples/ directory. Need to copy to Resources/ and mark as EmbeddedResource in .csproj.

Tasks

Task 1: Add all Phase 4 localization keys + Strings.Designer.cs update

Files:

  • SharepointToolbox/Localization/Strings.resx
  • SharepointToolbox/Localization/Strings.fr.resx
  • SharepointToolbox/Localization/Strings.Designer.cs

Action:

Add the following keys to Strings.resx (EN values) and Strings.fr.resx (FR values). Do NOT remove existing keys — append only.

New keys for Strings.resx (EN):

<!-- Phase 4: Tab headers -->
tab.transfer = Transfer
tab.bulkMembers = Bulk Members
tab.bulkSites = Bulk Sites
tab.folderStructure = Folder Structure

<!-- Phase 4: Transfer tab -->
transfer.sourcesite = Source Site
transfer.destsite = Destination Site
transfer.sourcelibrary = Source Library
transfer.destlibrary = Destination Library
transfer.sourcefolder = Source Folder
transfer.destfolder = Destination Folder
transfer.mode = Transfer Mode
transfer.mode.copy = Copy
transfer.mode.move = Move
transfer.conflict = Conflict Policy
transfer.conflict.skip = Skip
transfer.conflict.overwrite = Overwrite
transfer.conflict.rename = Rename (append suffix)
transfer.browse = Browse...
transfer.start = Start Transfer
transfer.nofiles = No files found to transfer.

<!-- Phase 4: Bulk Members tab -->
bulkmembers.import = Import CSV
bulkmembers.example = Load Example
bulkmembers.execute = Add Members
bulkmembers.preview = Preview ({0} rows, {1} valid, {2} invalid)
bulkmembers.groupname = Group Name
bulkmembers.groupurl = Group URL
bulkmembers.email = Email
bulkmembers.role = Role

<!-- Phase 4: Bulk Sites tab -->
bulksites.import = Import CSV
bulksites.example = Load Example
bulksites.execute = Create Sites
bulksites.preview = Preview ({0} rows, {1} valid, {2} invalid)
bulksites.name = Name
bulksites.alias = Alias
bulksites.type = Type
bulksites.owners = Owners
bulksites.members = Members

<!-- Phase 4: Folder Structure tab -->
folderstruct.import = Import CSV
folderstruct.example = Load Example
folderstruct.execute = Create Folders
folderstruct.preview = Preview ({0} folders to create)
folderstruct.library = Target Library
folderstruct.siteurl = Site URL

<!-- Phase 4: Templates tab -->
templates.list = Saved Templates
templates.capture = Capture Template
templates.apply = Apply Template
templates.rename = Rename
templates.delete = Delete
templates.siteurl = Source Site URL
templates.name = Template Name
templates.newtitle = New Site Title
templates.newalias = New Site Alias
templates.options = Capture Options
templates.opt.libraries = Libraries
templates.opt.folders = Folders
templates.opt.permissions = Permission Groups
templates.opt.logo = Site Logo
templates.opt.settings = Site Settings
templates.empty = No templates saved yet.

<!-- Phase 4: Shared bulk operation strings -->
bulk.confirm.title = Confirm Operation
bulk.confirm.proceed = Proceed
bulk.confirm.cancel = Cancel
bulk.confirm.message = {0} — Proceed?
bulk.result.success = Completed: {0} succeeded, {1} failed
bulk.result.allfailed = All {0} items failed.
bulk.result.allsuccess = All {0} items completed successfully.
bulk.exportfailed = Export Failed Items
bulk.retryfailed = Retry Failed
bulk.validation.invalid = {0} rows have validation errors. Fix and re-import.
bulk.csvimport.title = Select CSV File
bulk.csvimport.filter = CSV Files (*.csv)|*.csv

<!-- Phase 4: Folder browser dialog -->
folderbrowser.title = Select Folder
folderbrowser.loading = Loading folder tree...
folderbrowser.select = Select
folderbrowser.cancel = Cancel

New keys for Strings.fr.resx (FR):

tab.transfer = Transfert
tab.bulkMembers = Ajout en masse
tab.bulkSites = Sites en masse
tab.folderStructure = Structure de dossiers

transfer.sourcesite = Site source
transfer.destsite = Site destination
transfer.sourcelibrary = Bibliotheque source
transfer.destlibrary = Bibliotheque destination
transfer.sourcefolder = Dossier source
transfer.destfolder = Dossier destination
transfer.mode = Mode de transfert
transfer.mode.copy = Copier
transfer.mode.move = Deplacer
transfer.conflict = Politique de conflit
transfer.conflict.skip = Ignorer
transfer.conflict.overwrite = Ecraser
transfer.conflict.rename = Renommer (ajouter suffixe)
transfer.browse = Parcourir...
transfer.start = Demarrer le transfert
transfer.nofiles = Aucun fichier a transferer.

bulkmembers.import = Importer CSV
bulkmembers.example = Charger l'exemple
bulkmembers.execute = Ajouter les membres
bulkmembers.preview = Apercu ({0} lignes, {1} valides, {2} invalides)
bulkmembers.groupname = Nom du groupe
bulkmembers.groupurl = URL du groupe
bulkmembers.email = Courriel
bulkmembers.role = Role

bulksites.import = Importer CSV
bulksites.example = Charger l'exemple
bulksites.execute = Creer les sites
bulksites.preview = Apercu ({0} lignes, {1} valides, {2} invalides)
bulksites.name = Nom
bulksites.alias = Alias
bulksites.type = Type
bulksites.owners = Proprietaires
bulksites.members = Membres

folderstruct.import = Importer CSV
folderstruct.example = Charger l'exemple
folderstruct.execute = Creer les dossiers
folderstruct.preview = Apercu ({0} dossiers a creer)
folderstruct.library = Bibliotheque cible
folderstruct.siteurl = URL du site

templates.list = Modeles enregistres
templates.capture = Capturer un modele
templates.apply = Appliquer le modele
templates.rename = Renommer
templates.delete = Supprimer
templates.siteurl = URL du site source
templates.name = Nom du modele
templates.newtitle = Titre du nouveau site
templates.newalias = Alias du nouveau site
templates.options = Options de capture
templates.opt.libraries = Bibliotheques
templates.opt.folders = Dossiers
templates.opt.permissions = Groupes de permissions
templates.opt.logo = Logo du site
templates.opt.settings = Parametres du site
templates.empty = Aucun modele enregistre.

bulk.confirm.title = Confirmer l'operation
bulk.confirm.proceed = Continuer
bulk.confirm.cancel = Annuler
bulk.confirm.message = {0} — Continuer ?
bulk.result.success = Termine : {0} reussis, {1} echoues
bulk.result.allfailed = Les {0} elements ont echoue.
bulk.result.allsuccess = Les {0} elements ont ete traites avec succes.
bulk.exportfailed = Exporter les elements echoues
bulk.retryfailed = Reessayer les echecs
bulk.validation.invalid = {0} lignes contiennent des erreurs. Corrigez et reimportez.
bulk.csvimport.title = Selectionner un fichier CSV
bulk.csvimport.filter = Fichiers CSV (*.csv)|*.csv

folderbrowser.title = Selectionner un dossier
folderbrowser.loading = Chargement de l'arborescence...
folderbrowser.select = Selectionner
folderbrowser.cancel = Annuler

Update Strings.Designer.cs — add ResourceManager property accessors for all new keys. Follow the exact pattern of existing entries (static property with ResourceManager.GetString). Since there are many keys, the executor should add all keys programmatically following the existing pattern in the file.

Verify:

dotnet build SharepointToolbox/SharepointToolbox.csproj --no-restore -q

Done: All localization keys compile. EN and FR values present.

Task 2: Create shared dialogs + bundle example CSVs

Files:

  • SharepointToolbox/Views/Dialogs/ConfirmBulkOperationDialog.xaml
  • SharepointToolbox/Views/Dialogs/ConfirmBulkOperationDialog.xaml.cs
  • SharepointToolbox/Views/Dialogs/FolderBrowserDialog.xaml
  • SharepointToolbox/Views/Dialogs/FolderBrowserDialog.xaml.cs
  • SharepointToolbox/Resources/bulk_add_members.csv
  • SharepointToolbox/Resources/bulk_create_sites.csv
  • SharepointToolbox/Resources/folder_structure.csv
  • SharepointToolbox/SharepointToolbox.csproj

Action:

  1. Create ConfirmBulkOperationDialog.xaml:
<Window x:Class="SharepointToolbox.Views.Dialogs.ConfirmBulkOperationDialog"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:loc="clr-namespace:SharepointToolbox.Localization"
        Title="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[bulk.confirm.title]}"
        Width="450" Height="220" WindowStartupLocation="CenterOwner"
        ResizeMode="NoResize">
    <Grid Margin="20">
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>

        <TextBlock x:Name="MessageText" Grid.Row="0"
                   TextWrapping="Wrap" FontSize="14"
                   VerticalAlignment="Center" />

        <StackPanel Grid.Row="1" Orientation="Horizontal"
                    HorizontalAlignment="Right" Margin="0,20,0,0">
            <Button Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[bulk.confirm.cancel]}"
                    Width="100" Margin="0,0,10,0" IsCancel="True"
                    Click="Cancel_Click" />
            <Button x:Name="ProceedButton"
                    Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[bulk.confirm.proceed]}"
                    Width="100" IsDefault="True"
                    Click="Proceed_Click" />
        </StackPanel>
    </Grid>
</Window>
  1. Create ConfirmBulkOperationDialog.xaml.cs:
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();
    }
}
  1. Create FolderBrowserDialog.xaml:
<Window x:Class="SharepointToolbox.Views.Dialogs.FolderBrowserDialog"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:loc="clr-namespace:SharepointToolbox.Localization"
        Title="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[folderbrowser.title]}"
        Width="400" Height="500" WindowStartupLocation="CenterOwner"
        ResizeMode="CanResizeWithGrip">
    <DockPanel Margin="10">
        <!-- Status -->
        <TextBlock x:Name="StatusText" DockPanel.Dock="Top" Margin="0,0,0,10"
                   Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[folderbrowser.loading]}" />

        <!-- Buttons -->
        <StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal"
                    HorizontalAlignment="Right" Margin="0,10,0,0">
            <Button Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[folderbrowser.cancel]}"
                    Width="80" Margin="0,0,10,0" IsCancel="True"
                    Click="Cancel_Click" />
            <Button x:Name="SelectButton"
                    Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[folderbrowser.select]}"
                    Width="80" IsDefault="True" IsEnabled="False"
                    Click="Select_Click" />
        </StackPanel>

        <!-- Tree -->
        <TreeView x:Name="FolderTree" SelectedItemChanged="FolderTree_SelectedItemChanged" />
    </DockPanel>
</Window>
  1. Create FolderBrowserDialog.xaml.cs:
using System.Windows;
using System.Windows.Controls;
using Microsoft.SharePoint.Client;
using SharepointToolbox.Infrastructure.Auth;

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<Core.Models.OperationProgress>();
            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<Core.Models.OperationProgress>();
                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<object> e)
    {
        if (e.NewValue is TreeViewItem node && node.Tag is FolderNodeInfo info)
        {
            SelectedLibrary = info.LibraryTitle;
            SelectedFolderPath = info.FolderPath;
            SelectButton.IsEnabled = true;
        }
    }

    private void Select_Click(object sender, RoutedEventArgs e)
    {
        DialogResult = true;
        Close();
    }

    private void Cancel_Click(object sender, RoutedEventArgs e)
    {
        DialogResult = false;
        Close();
    }

    private record FolderNodeInfo(string LibraryTitle, string FolderPath);
}
  1. Bundle example CSVs as embedded resources. Create SharepointToolbox/Resources/ directory and copy the example CSVs there with extended schemas.

Create Resources/bulk_add_members.csv:

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

Create Resources/bulk_create_sites.csv (keep semicolon delimiter matching existing example):

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;

Create Resources/folder_structure.csv (copy from existing example):

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;
  1. Add EmbeddedResource entries to SharepointToolbox.csproj:
<ItemGroup>
  <EmbeddedResource Include="Resources\bulk_add_members.csv" />
  <EmbeddedResource Include="Resources\bulk_create_sites.csv" />
  <EmbeddedResource Include="Resources\folder_structure.csv" />
</ItemGroup>

Verify:

dotnet build SharepointToolbox/SharepointToolbox.csproj --no-restore -q

Done: All localization keys added (EN + FR). ConfirmBulkOperationDialog and FolderBrowserDialog compile. Example CSVs bundled as embedded resources. All new XAML dialogs compile.

Commit: feat(04-07): add Phase 4 localization, shared dialogs, and example CSV resources