feat(01-06): build WPF shell — MainWindow XAML, ViewModels, LogPanelSink wiring
- Add FeatureTabBase UserControl with ProgressBar/TextBlock/CancelButton strip (Visibility bound to IsRunning, shown only during operations) - Add MainWindowViewModel with TenantProfiles ObservableCollection, ConnectCommand, ClearSessionCommand, ManageProfilesCommand, ProgressUpdatedMessage subscription - Add ProfileManagementViewModel wrapping ProfileService CRUD with input validation - Add SettingsViewModel (extends FeatureViewModelBase) with language/folder settings - Update MainWindow.xaml: DockPanel shell with Toolbar, TabControl (8 tabs), 150px RichTextBox LogPanel, StatusBar (tenant name | ProgressStatus | ProgressPercentage) - MainWindow.xaml.cs: DI constructor, DataContext=viewModel, LoadProfilesAsync on Loaded - App.xaml.cs: register all services, wire LogPanelSink after MainWindow resolved, register DispatcherUnhandledException and UnobservedTaskException global handlers - App.xaml: add BoolToVisibilityConverter resource
This commit is contained in:
@@ -3,6 +3,6 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:local="clr-namespace:SharepointToolbox">
|
xmlns:local="clr-namespace:SharepointToolbox">
|
||||||
<Application.Resources>
|
<Application.Resources>
|
||||||
|
<BooleanToVisibilityConverter x:Key="BoolToVisibilityConverter" />
|
||||||
</Application.Resources>
|
</Application.Resources>
|
||||||
</Application>
|
</Application>
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
|
using System.IO;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
|
using SharepointToolbox.Infrastructure.Auth;
|
||||||
|
using SharepointToolbox.Infrastructure.Logging;
|
||||||
|
using SharepointToolbox.Services;
|
||||||
|
using SharepointToolbox.ViewModels;
|
||||||
|
using SharepointToolbox.ViewModels.Tabs;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
|
|
||||||
namespace SharepointToolbox;
|
namespace SharepointToolbox;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Interaction logic for App.xaml
|
|
||||||
/// </summary>
|
|
||||||
public partial class App : Application
|
public partial class App : Application
|
||||||
{
|
{
|
||||||
[STAThread]
|
[STAThread]
|
||||||
@@ -16,7 +20,7 @@ public partial class App : Application
|
|||||||
using IHost host = Host.CreateDefaultBuilder(args)
|
using IHost host = Host.CreateDefaultBuilder(args)
|
||||||
.UseSerilog((ctx, cfg) => cfg
|
.UseSerilog((ctx, cfg) => cfg
|
||||||
.WriteTo.File(
|
.WriteTo.File(
|
||||||
System.IO.Path.Combine(
|
Path.Combine(
|
||||||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||||
"SharepointToolbox", "logs", "app-.log"),
|
"SharepointToolbox", "logs", "app-.log"),
|
||||||
rollingInterval: RollingInterval.Day,
|
rollingInterval: RollingInterval.Day,
|
||||||
@@ -27,16 +31,49 @@ public partial class App : Application
|
|||||||
host.Start();
|
host.Start();
|
||||||
App app = new();
|
App app = new();
|
||||||
app.InitializeComponent();
|
app.InitializeComponent();
|
||||||
app.MainWindow = host.Services.GetRequiredService<MainWindow>();
|
|
||||||
|
var mainWindow = host.Services.GetRequiredService<MainWindow>();
|
||||||
|
|
||||||
|
// Wire LogPanelSink now that we have the RichTextBox
|
||||||
|
Log.Logger = new LoggerConfiguration()
|
||||||
|
.WriteTo.File(
|
||||||
|
Path.Combine(
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||||
|
"SharepointToolbox", "logs", "app-.log"),
|
||||||
|
rollingInterval: RollingInterval.Day,
|
||||||
|
retainedFileCountLimit: 30)
|
||||||
|
.WriteTo.Sink(new LogPanelSink(mainWindow.GetLogPanel()))
|
||||||
|
.CreateLogger();
|
||||||
|
|
||||||
|
// Global exception handlers
|
||||||
|
app.DispatcherUnhandledException += (s, e) =>
|
||||||
|
{
|
||||||
|
Log.Fatal(e.Exception, "Unhandled UI exception");
|
||||||
|
MessageBox.Show(
|
||||||
|
$"A fatal error occurred:\n{e.Exception.Message}\n\nCheck log for details.",
|
||||||
|
"Fatal Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||||
|
e.Handled = true;
|
||||||
|
};
|
||||||
|
TaskScheduler.UnobservedTaskException += (s, e) =>
|
||||||
|
{
|
||||||
|
Log.Fatal(e.Exception, "Unobserved task exception");
|
||||||
|
e.SetObserved();
|
||||||
|
};
|
||||||
|
|
||||||
|
app.MainWindow = mainWindow;
|
||||||
app.MainWindow.Visibility = Visibility.Visible;
|
app.MainWindow.Visibility = Visibility.Visible;
|
||||||
app.Run();
|
app.Run();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void RegisterServices(HostBuilderContext ctx, IServiceCollection services)
|
private static void RegisterServices(HostBuilderContext ctx, IServiceCollection services)
|
||||||
{
|
{
|
||||||
// Placeholder — services registered in subsequent plans
|
services.AddSingleton<MsalClientFactory>();
|
||||||
|
services.AddSingleton<SessionManager>();
|
||||||
|
services.AddSingleton<ProfileService>();
|
||||||
|
services.AddSingleton<SettingsService>();
|
||||||
|
services.AddSingleton<MainWindowViewModel>();
|
||||||
|
services.AddTransient<ProfileManagementViewModel>();
|
||||||
|
services.AddTransient<SettingsViewModel>();
|
||||||
services.AddSingleton<MainWindow>();
|
services.AddSingleton<MainWindow>();
|
||||||
// LogPanelSink registered in plan 01-06 after MainWindow is created
|
|
||||||
// (requires RichTextBox reference from MainWindow)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,71 @@
|
|||||||
<Window x:Class="SharepointToolbox.MainWindow"
|
<Window x:Class="SharepointToolbox.MainWindow"
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:local="clr-namespace:SharepointToolbox"
|
xmlns:loc="clr-namespace:SharepointToolbox.Localization"
|
||||||
|
xmlns:controls="clr-namespace:SharepointToolbox.Views.Controls"
|
||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
Title="MainWindow" Height="450" Width="800">
|
Title="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[app.title]}"
|
||||||
<Grid>
|
MinWidth="900" MinHeight="600" Height="700" Width="1100">
|
||||||
|
<DockPanel>
|
||||||
|
<!-- Toolbar -->
|
||||||
|
<ToolBar DockPanel.Dock="Top">
|
||||||
|
<ComboBox Width="220" ItemsSource="{Binding TenantProfiles}"
|
||||||
|
SelectedItem="{Binding SelectedProfile}"
|
||||||
|
DisplayMemberPath="Name" />
|
||||||
|
<Button Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[toolbar.connect]}"
|
||||||
|
Command="{Binding ConnectCommand}" />
|
||||||
|
<Button Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[toolbar.manage]}"
|
||||||
|
Command="{Binding ManageProfilesCommand}" />
|
||||||
|
<Separator />
|
||||||
|
<Button Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[toolbar.clear]}"
|
||||||
|
Command="{Binding ClearSessionCommand}" />
|
||||||
|
</ToolBar>
|
||||||
|
|
||||||
</Grid>
|
<!-- StatusBar: tenant name | operation status text | progress % -->
|
||||||
|
<StatusBar DockPanel.Dock="Bottom" Height="24">
|
||||||
|
<StatusBarItem Content="{Binding SelectedProfile.Name}" />
|
||||||
|
<Separator />
|
||||||
|
<StatusBarItem Content="{Binding ProgressStatus}" />
|
||||||
|
<Separator />
|
||||||
|
<StatusBarItem Content="{Binding ProgressPercentage, StringFormat={}{0}%}" />
|
||||||
|
</StatusBar>
|
||||||
|
|
||||||
|
<!-- Log Panel -->
|
||||||
|
<RichTextBox x:Name="LogPanel" DockPanel.Dock="Bottom" Height="150"
|
||||||
|
IsReadOnly="True" VerticalScrollBarVisibility="Auto"
|
||||||
|
Background="Black" Foreground="LimeGreen"
|
||||||
|
FontFamily="Consolas" FontSize="11" />
|
||||||
|
|
||||||
|
<!-- TabControl: 7 stub tabs use FeatureTabBase; Settings tab wired in plan 01-07 -->
|
||||||
|
<TabControl>
|
||||||
|
<TabItem Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[tab.permissions]}">
|
||||||
|
<controls:FeatureTabBase />
|
||||||
|
</TabItem>
|
||||||
|
<TabItem Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[tab.storage]}">
|
||||||
|
<controls:FeatureTabBase />
|
||||||
|
</TabItem>
|
||||||
|
<TabItem Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[tab.search]}">
|
||||||
|
<controls:FeatureTabBase />
|
||||||
|
</TabItem>
|
||||||
|
<TabItem Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[tab.duplicates]}">
|
||||||
|
<controls:FeatureTabBase />
|
||||||
|
</TabItem>
|
||||||
|
<TabItem Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[tab.templates]}">
|
||||||
|
<controls:FeatureTabBase />
|
||||||
|
</TabItem>
|
||||||
|
<TabItem Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[tab.bulk]}">
|
||||||
|
<controls:FeatureTabBase />
|
||||||
|
</TabItem>
|
||||||
|
<TabItem Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[tab.structure]}">
|
||||||
|
<controls:FeatureTabBase />
|
||||||
|
</TabItem>
|
||||||
|
<!-- Settings tab: placeholder TextBlock replaced by SettingsView in plan 01-07 -->
|
||||||
|
<TabItem x:Name="SettingsTabItem"
|
||||||
|
Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[tab.settings]}">
|
||||||
|
<TextBlock Text="Settings (plan 01-07)" HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||||
|
</TabItem>
|
||||||
|
</TabControl>
|
||||||
|
</DockPanel>
|
||||||
</Window>
|
</Window>
|
||||||
|
|||||||
@@ -1,23 +1,26 @@
|
|||||||
using System.Text;
|
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Controls;
|
using System.Windows.Controls;
|
||||||
using System.Windows.Data;
|
using SharepointToolbox.ViewModels;
|
||||||
using System.Windows.Documents;
|
|
||||||
using System.Windows.Input;
|
|
||||||
using System.Windows.Media;
|
|
||||||
using System.Windows.Media.Imaging;
|
|
||||||
using System.Windows.Navigation;
|
|
||||||
using System.Windows.Shapes;
|
|
||||||
|
|
||||||
namespace SharepointToolbox;
|
namespace SharepointToolbox;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Interaction logic for MainWindow.xaml
|
|
||||||
/// </summary>
|
|
||||||
public partial class MainWindow : Window
|
public partial class MainWindow : Window
|
||||||
{
|
{
|
||||||
public MainWindow()
|
private readonly MainWindowViewModel _viewModel;
|
||||||
|
|
||||||
|
public MainWindow(MainWindowViewModel viewModel)
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
_viewModel = viewModel;
|
||||||
|
DataContext = viewModel;
|
||||||
|
Loaded += OnLoaded;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
private async void OnLoaded(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
await _viewModel.LoadProfilesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expose the LogPanel RichTextBox (generated by x:Name="LogPanel") for LogPanelSink wiring
|
||||||
|
public RichTextBox GetLogPanel() => LogPanel;
|
||||||
|
}
|
||||||
|
|||||||
127
SharepointToolbox/ViewModels/MainWindowViewModel.cs
Normal file
127
SharepointToolbox/ViewModels/MainWindowViewModel.cs
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using CommunityToolkit.Mvvm.Messaging;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using SharepointToolbox.Core.Messages;
|
||||||
|
using SharepointToolbox.Core.Models;
|
||||||
|
using SharepointToolbox.Services;
|
||||||
|
|
||||||
|
namespace SharepointToolbox.ViewModels;
|
||||||
|
|
||||||
|
public partial class MainWindowViewModel : ObservableRecipient
|
||||||
|
{
|
||||||
|
private readonly ProfileService _profileService;
|
||||||
|
private readonly SessionManager _sessionManager;
|
||||||
|
private readonly ILogger<MainWindowViewModel> _logger;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private TenantProfile? _selectedProfile;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string _connectionStatus = "Not connected";
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string _progressStatus = string.Empty;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private int _progressPercentage;
|
||||||
|
|
||||||
|
public ObservableCollection<TenantProfile> TenantProfiles { get; } = new();
|
||||||
|
|
||||||
|
public IAsyncRelayCommand ConnectCommand { get; }
|
||||||
|
public IAsyncRelayCommand ClearSessionCommand { get; }
|
||||||
|
public RelayCommand ManageProfilesCommand { get; }
|
||||||
|
|
||||||
|
public MainWindowViewModel(
|
||||||
|
ProfileService profileService,
|
||||||
|
SessionManager sessionManager,
|
||||||
|
ILogger<MainWindowViewModel> logger)
|
||||||
|
{
|
||||||
|
_profileService = profileService;
|
||||||
|
_sessionManager = sessionManager;
|
||||||
|
_logger = logger;
|
||||||
|
|
||||||
|
ConnectCommand = new AsyncRelayCommand(ConnectAsync, () => SelectedProfile != null);
|
||||||
|
ClearSessionCommand = new AsyncRelayCommand(ClearSessionAsync, () => SelectedProfile != null);
|
||||||
|
ManageProfilesCommand = new RelayCommand(OpenProfileManagement);
|
||||||
|
|
||||||
|
IsActive = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnActivated()
|
||||||
|
{
|
||||||
|
base.OnActivated();
|
||||||
|
Messenger.Register<ProgressUpdatedMessage>(this, (r, m) =>
|
||||||
|
{
|
||||||
|
var vm = (MainWindowViewModel)r;
|
||||||
|
vm.ProgressStatus = m.Value.Message;
|
||||||
|
vm.ProgressPercentage = m.Value.Total > 0
|
||||||
|
? (int)(100.0 * m.Value.Current / m.Value.Total)
|
||||||
|
: 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnSelectedProfileChanged(TenantProfile? value)
|
||||||
|
{
|
||||||
|
if (value != null)
|
||||||
|
{
|
||||||
|
WeakReferenceMessenger.Default.Send(new TenantSwitchedMessage(value));
|
||||||
|
}
|
||||||
|
ConnectCommand.NotifyCanExecuteChanged();
|
||||||
|
ClearSessionCommand.NotifyCanExecuteChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task LoadProfilesAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var profiles = await _profileService.GetProfilesAsync();
|
||||||
|
TenantProfiles.Clear();
|
||||||
|
foreach (var profile in profiles)
|
||||||
|
TenantProfiles.Add(profile);
|
||||||
|
|
||||||
|
if (TenantProfiles.Count > 0)
|
||||||
|
SelectedProfile = TenantProfiles[0];
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to load tenant profiles.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ConnectAsync()
|
||||||
|
{
|
||||||
|
if (SelectedProfile == null) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ConnectionStatus = "Connecting...";
|
||||||
|
await _sessionManager.GetOrCreateContextAsync(SelectedProfile, CancellationToken.None);
|
||||||
|
ConnectionStatus = SelectedProfile.Name;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ConnectionStatus = "Connection failed";
|
||||||
|
_logger.LogError(ex, "Failed to connect to tenant {TenantUrl}.", SelectedProfile.TenantUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ClearSessionAsync()
|
||||||
|
{
|
||||||
|
if (SelectedProfile == null) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _sessionManager.ClearSessionAsync(SelectedProfile.TenantUrl);
|
||||||
|
ConnectionStatus = "Not connected";
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to clear session for {TenantUrl}.", SelectedProfile.TenantUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OpenProfileManagement()
|
||||||
|
{
|
||||||
|
// Profile management dialog opened by View layer (plan 01-07)
|
||||||
|
}
|
||||||
|
}
|
||||||
125
SharepointToolbox/ViewModels/ProfileManagementViewModel.cs
Normal file
125
SharepointToolbox/ViewModels/ProfileManagementViewModel.cs
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using SharepointToolbox.Core.Models;
|
||||||
|
using SharepointToolbox.Services;
|
||||||
|
|
||||||
|
namespace SharepointToolbox.ViewModels;
|
||||||
|
|
||||||
|
public partial class ProfileManagementViewModel : ObservableObject
|
||||||
|
{
|
||||||
|
private readonly ProfileService _profileService;
|
||||||
|
private readonly ILogger<ProfileManagementViewModel> _logger;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private TenantProfile? _selectedProfile;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string _newName = string.Empty;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string _newTenantUrl = string.Empty;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string _newClientId = string.Empty;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string _validationMessage = string.Empty;
|
||||||
|
|
||||||
|
public ObservableCollection<TenantProfile> Profiles { get; } = new();
|
||||||
|
|
||||||
|
public IAsyncRelayCommand AddCommand { get; }
|
||||||
|
public IAsyncRelayCommand RenameCommand { get; }
|
||||||
|
public IAsyncRelayCommand DeleteCommand { get; }
|
||||||
|
|
||||||
|
public ProfileManagementViewModel(ProfileService profileService, ILogger<ProfileManagementViewModel> logger)
|
||||||
|
{
|
||||||
|
_profileService = profileService;
|
||||||
|
_logger = logger;
|
||||||
|
|
||||||
|
AddCommand = new AsyncRelayCommand(AddAsync, CanAdd);
|
||||||
|
RenameCommand = new AsyncRelayCommand(RenameAsync, () => SelectedProfile != null && !string.IsNullOrWhiteSpace(NewName));
|
||||||
|
DeleteCommand = new AsyncRelayCommand(DeleteAsync, () => SelectedProfile != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task LoadAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var profiles = await _profileService.GetProfilesAsync();
|
||||||
|
Profiles.Clear();
|
||||||
|
foreach (var p in profiles)
|
||||||
|
Profiles.Add(p);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to load profiles.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool CanAdd()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(NewName)) return false;
|
||||||
|
if (!Uri.TryCreate(NewTenantUrl, UriKind.Absolute, out _)) return false;
|
||||||
|
if (string.IsNullOrWhiteSpace(NewClientId)) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task AddAsync()
|
||||||
|
{
|
||||||
|
if (!CanAdd()) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var profile = new TenantProfile
|
||||||
|
{
|
||||||
|
Name = NewName.Trim(),
|
||||||
|
TenantUrl = NewTenantUrl.Trim(),
|
||||||
|
ClientId = NewClientId.Trim()
|
||||||
|
};
|
||||||
|
await _profileService.AddProfileAsync(profile);
|
||||||
|
Profiles.Add(profile);
|
||||||
|
NewName = string.Empty;
|
||||||
|
NewTenantUrl = string.Empty;
|
||||||
|
NewClientId = string.Empty;
|
||||||
|
ValidationMessage = string.Empty;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ValidationMessage = ex.Message;
|
||||||
|
_logger.LogError(ex, "Failed to add profile.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RenameAsync()
|
||||||
|
{
|
||||||
|
if (SelectedProfile == null || string.IsNullOrWhiteSpace(NewName)) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _profileService.RenameProfileAsync(SelectedProfile.Name, NewName.Trim());
|
||||||
|
SelectedProfile.Name = NewName.Trim();
|
||||||
|
NewName = string.Empty;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ValidationMessage = ex.Message;
|
||||||
|
_logger.LogError(ex, "Failed to rename profile.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task DeleteAsync()
|
||||||
|
{
|
||||||
|
if (SelectedProfile == null) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _profileService.DeleteProfileAsync(SelectedProfile.Name);
|
||||||
|
Profiles.Remove(SelectedProfile);
|
||||||
|
SelectedProfile = null;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ValidationMessage = ex.Message;
|
||||||
|
_logger.LogError(ex, "Failed to delete profile.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
94
SharepointToolbox/ViewModels/Tabs/SettingsViewModel.cs
Normal file
94
SharepointToolbox/ViewModels/Tabs/SettingsViewModel.cs
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using Microsoft.Win32;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using SharepointToolbox.Core.Models;
|
||||||
|
using SharepointToolbox.Localization;
|
||||||
|
using SharepointToolbox.Services;
|
||||||
|
|
||||||
|
namespace SharepointToolbox.ViewModels.Tabs;
|
||||||
|
|
||||||
|
public partial class SettingsViewModel : FeatureViewModelBase
|
||||||
|
{
|
||||||
|
private readonly SettingsService _settingsService;
|
||||||
|
|
||||||
|
private string _selectedLanguage = "en";
|
||||||
|
public string SelectedLanguage
|
||||||
|
{
|
||||||
|
get => _selectedLanguage;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_selectedLanguage == value) return;
|
||||||
|
_selectedLanguage = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
_ = ApplyLanguageAsync(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string _dataFolder = string.Empty;
|
||||||
|
public string DataFolder
|
||||||
|
{
|
||||||
|
get => _dataFolder;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_dataFolder == value) return;
|
||||||
|
_dataFolder = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
_ = _settingsService.SetDataFolderAsync(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public RelayCommand BrowseFolderCommand { get; }
|
||||||
|
|
||||||
|
public SettingsViewModel(SettingsService settingsService, ILogger<FeatureViewModelBase> logger)
|
||||||
|
: base(logger)
|
||||||
|
{
|
||||||
|
_settingsService = settingsService;
|
||||||
|
BrowseFolderCommand = new RelayCommand(BrowseFolder);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task LoadAsync()
|
||||||
|
{
|
||||||
|
var settings = await _settingsService.GetSettingsAsync();
|
||||||
|
_selectedLanguage = settings.Lang;
|
||||||
|
_dataFolder = settings.DataFolder;
|
||||||
|
OnPropertyChanged(nameof(SelectedLanguage));
|
||||||
|
OnPropertyChanged(nameof(DataFolder));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ApplyLanguageAsync(string code)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
TranslationSource.Instance.CurrentCulture = new CultureInfo(code);
|
||||||
|
await _settingsService.SetLanguageAsync(code);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
StatusMessage = ex.Message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BrowseFolder()
|
||||||
|
{
|
||||||
|
// OpenFolderDialog is available in .NET 8+ via Microsoft.Win32
|
||||||
|
var dialog = new OpenFolderDialog
|
||||||
|
{
|
||||||
|
Title = "Select data output folder",
|
||||||
|
InitialDirectory = string.IsNullOrEmpty(_dataFolder)
|
||||||
|
? Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments)
|
||||||
|
: _dataFolder
|
||||||
|
};
|
||||||
|
|
||||||
|
if (dialog.ShowDialog() == true)
|
||||||
|
{
|
||||||
|
DataFolder = dialog.FolderName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Task RunOperationAsync(CancellationToken ct, IProgress<OperationProgress> progress)
|
||||||
|
{
|
||||||
|
// Settings tab has no long-running operation
|
||||||
|
throw new NotSupportedException("Settings tab does not have a run operation.");
|
||||||
|
}
|
||||||
|
}
|
||||||
33
SharepointToolbox/Views/Controls/FeatureTabBase.xaml
Normal file
33
SharepointToolbox/Views/Controls/FeatureTabBase.xaml
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<UserControl x:Class="SharepointToolbox.Views.Controls.FeatureTabBase"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:loc="clr-namespace:SharepointToolbox.Localization">
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- Placeholder content — Phase 2+ replaces Row 0 -->
|
||||||
|
<TextBlock Grid.Row="0"
|
||||||
|
Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[tab.comingsoon]}"
|
||||||
|
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||||
|
|
||||||
|
<!-- Per-tab progress/cancel strip (shown only when IsRunning) -->
|
||||||
|
<Grid Grid.Row="1" Margin="8,4"
|
||||||
|
Visibility="{Binding IsRunning, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<ProgressBar Grid.Column="0" Height="16" Minimum="0" Maximum="100"
|
||||||
|
Value="{Binding ProgressValue}" />
|
||||||
|
<TextBlock Grid.Column="1" Margin="8,0" VerticalAlignment="Center"
|
||||||
|
Text="{Binding StatusMessage}" />
|
||||||
|
<Button Grid.Column="2"
|
||||||
|
Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[btn.cancel]}"
|
||||||
|
Command="{Binding CancelCommand}" Width="70" />
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
||||||
11
SharepointToolbox/Views/Controls/FeatureTabBase.xaml.cs
Normal file
11
SharepointToolbox/Views/Controls/FeatureTabBase.xaml.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using System.Windows.Controls;
|
||||||
|
|
||||||
|
namespace SharepointToolbox.Views.Controls;
|
||||||
|
|
||||||
|
public partial class FeatureTabBase : UserControl
|
||||||
|
{
|
||||||
|
public FeatureTabBase()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user