Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6a51a58224 | |||
| 9b451c9cbe |
@@ -3,8 +3,8 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:conv="clr-namespace:MarcusLaw.OutlookAddin.UI.Converters"
|
xmlns:conv="clr-namespace:MarcusLaw.OutlookAddin.UI.Converters"
|
||||||
Title="שמור קבצים מצורפים ל-Klear"
|
Title="שמור קבצים מצורפים ל-Klear"
|
||||||
Height="560" Width="620"
|
Height="640" Width="620"
|
||||||
MinHeight="420" MinWidth="500"
|
MinHeight="480" MinWidth="500"
|
||||||
FlowDirection="RightToLeft"
|
FlowDirection="RightToLeft"
|
||||||
WindowStartupLocation="CenterOwner"
|
WindowStartupLocation="CenterOwner"
|
||||||
ShowInTaskbar="False"
|
ShowInTaskbar="False"
|
||||||
@@ -144,24 +144,38 @@
|
|||||||
|
|
||||||
<!-- Folder -->
|
<!-- Folder -->
|
||||||
<DockPanel Grid.Row="2" Margin="0,10,0,0" LastChildFill="True">
|
<DockPanel Grid.Row="2" Margin="0,10,0,0" LastChildFill="True">
|
||||||
<TextBlock DockPanel.Dock="Left" Text="תיקייה:" VerticalAlignment="Center"
|
<DockPanel DockPanel.Dock="Top" LastChildFill="True" Margin="0,0,0,4">
|
||||||
Margin="0,0,8,0" Foreground="{StaticResource Muted}" FontSize="12" />
|
<TextBlock DockPanel.Dock="Left" Text="תיקיית יעד:" VerticalAlignment="Center"
|
||||||
<TextBlock DockPanel.Dock="Right" Text="טוען…" VerticalAlignment="Center"
|
Margin="0,0,8,0" Foreground="{StaticResource Muted}" FontSize="12" />
|
||||||
Margin="8,0,0,0" Foreground="{StaticResource Muted}" FontSize="12"
|
<TextBlock DockPanel.Dock="Right" Text="טוען…" VerticalAlignment="Center"
|
||||||
Visibility="{Binding IsLoadingSubfolders, Converter={StaticResource BoolToVis}}" />
|
Margin="8,0,0,0" Foreground="{StaticResource Muted}" FontSize="12"
|
||||||
<ComboBox ItemsSource="{Binding Subfolders}"
|
Visibility="{Binding IsLoadingSubfolders, Converter={StaticResource BoolToVis}}" />
|
||||||
SelectedItem="{Binding SelectedSubfolder}"
|
<TextBlock Text="בחר/י תיקייה או תת-תיקייה לשמירה (ניתן להרחיב רמות)"
|
||||||
Padding="6,4">
|
VerticalAlignment="Center" Foreground="{StaticResource Muted}" FontSize="11"
|
||||||
<ComboBox.Style>
|
TextTrimming="CharacterEllipsis" />
|
||||||
<Style TargetType="ComboBox">
|
</DockPanel>
|
||||||
<Style.Triggers>
|
<Border Style="{StaticResource Card}" MaxHeight="150">
|
||||||
<DataTrigger Binding="{Binding IsLoadingSubfolders}" Value="True">
|
<TreeView x:Name="FolderTreeView"
|
||||||
<Setter Property="IsEnabled" Value="False" />
|
ItemsSource="{Binding FolderTree}"
|
||||||
</DataTrigger>
|
BorderThickness="0"
|
||||||
</Style.Triggers>
|
Background="Transparent"
|
||||||
</Style>
|
SelectedItemChanged="FolderTreeView_SelectedItemChanged">
|
||||||
</ComboBox.Style>
|
<TreeView.ItemContainerStyle>
|
||||||
</ComboBox>
|
<Style TargetType="TreeViewItem">
|
||||||
|
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
|
||||||
|
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
|
||||||
|
</Style>
|
||||||
|
</TreeView.ItemContainerStyle>
|
||||||
|
<TreeView.ItemTemplate>
|
||||||
|
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,1">
|
||||||
|
<TextBlock Text="📁" Margin="0,0,4,0" FontSize="13" VerticalAlignment="Center" />
|
||||||
|
<TextBlock Text="{Binding Name}" VerticalAlignment="Center" FontSize="13" />
|
||||||
|
</StackPanel>
|
||||||
|
</HierarchicalDataTemplate>
|
||||||
|
</TreeView.ItemTemplate>
|
||||||
|
</TreeView>
|
||||||
|
</Border>
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
|
|
||||||
<!-- Attachments header -->
|
<!-- Attachments header -->
|
||||||
|
|||||||
@@ -32,5 +32,15 @@ namespace MarcusLaw.OutlookAddin.UI.Dialogs
|
|||||||
DialogResult = _viewModel?.DialogResult;
|
DialogResult = _viewModel?.DialogResult;
|
||||||
Close();
|
Close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WPF's TreeView.SelectedItem is read-only, so we mirror the selection
|
||||||
|
// into the view-model here (the chosen node drives the save target).
|
||||||
|
private void FolderTreeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
|
||||||
|
{
|
||||||
|
if (_viewModel != null)
|
||||||
|
{
|
||||||
|
_viewModel.SelectedFolder = e.NewValue as FolderNodeViewModel;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,168 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows.Threading;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using MarcusLaw.OutlookAddin.Core.Services;
|
||||||
|
using Serilog;
|
||||||
|
|
||||||
|
namespace MarcusLaw.OutlookAddin.UI.ViewModels
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// One node in the network-storage folder tree shown by the
|
||||||
|
/// save-attachments dialog. Children are listed lazily the first time the
|
||||||
|
/// node is expanded, so the user can drill into nested subfolders to any
|
||||||
|
/// depth without us listing the whole tree up front.
|
||||||
|
/// </summary>
|
||||||
|
public sealed partial class FolderNodeViewModel : ObservableObject
|
||||||
|
{
|
||||||
|
private readonly IEspoCrmClient? _client;
|
||||||
|
private readonly ILogger? _logger;
|
||||||
|
private readonly Dispatcher? _dispatcher;
|
||||||
|
private bool _childrenLoaded;
|
||||||
|
private bool _loadingStarted;
|
||||||
|
|
||||||
|
/// <summary>Label shown in the tree.</summary>
|
||||||
|
public string Name { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Path relative to the share root (e.g. "56-ליאור זייני/מסמכים").
|
||||||
|
/// Used to list this folder's children. Empty for placeholders.
|
||||||
|
/// </summary>
|
||||||
|
public string FullPath { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Path relative to the case-root folder (e.g. "מסמכים/רפואי"), or
|
||||||
|
/// null for the case-root node itself. This is what the save target
|
||||||
|
/// appends to the resolved case folder.
|
||||||
|
/// </summary>
|
||||||
|
public string? RelativePath { get; }
|
||||||
|
|
||||||
|
/// <summary>True for the transient "loading…" child placeholder.</summary>
|
||||||
|
public bool IsPlaceholder { get; }
|
||||||
|
|
||||||
|
public bool IsRoot => !IsPlaceholder && RelativePath == null;
|
||||||
|
|
||||||
|
public ObservableCollection<FolderNodeViewModel> Children { get; } =
|
||||||
|
new ObservableCollection<FolderNodeViewModel>();
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool isExpanded;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool isSelected;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool isLoading;
|
||||||
|
|
||||||
|
/// <summary>Real folder node.</summary>
|
||||||
|
public FolderNodeViewModel(
|
||||||
|
IEspoCrmClient client,
|
||||||
|
ILogger logger,
|
||||||
|
Dispatcher dispatcher,
|
||||||
|
string name,
|
||||||
|
string fullPath,
|
||||||
|
string? relativePath)
|
||||||
|
{
|
||||||
|
_client = client ?? throw new ArgumentNullException(nameof(client));
|
||||||
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
|
_dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
|
||||||
|
Name = name;
|
||||||
|
FullPath = fullPath;
|
||||||
|
RelativePath = relativePath;
|
||||||
|
// A placeholder child makes the expand chevron appear before we've
|
||||||
|
// listed; expanding swaps it for the real children (or, if there
|
||||||
|
// are none, the node becomes a leaf).
|
||||||
|
Children.Add(new FolderNodeViewModel());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Placeholder ("loading…") node constructor.</summary>
|
||||||
|
private FolderNodeViewModel()
|
||||||
|
{
|
||||||
|
Name = "טוען…";
|
||||||
|
FullPath = string.Empty;
|
||||||
|
RelativePath = string.Empty;
|
||||||
|
IsPlaceholder = true;
|
||||||
|
_childrenLoaded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnIsExpandedChanged(bool value)
|
||||||
|
{
|
||||||
|
if (value && !_childrenLoaded && !_loadingStarted)
|
||||||
|
{
|
||||||
|
_loadingStarted = true;
|
||||||
|
_ = LoadChildrenAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lists this folder's subfolders and replaces the placeholder with
|
||||||
|
/// real child nodes. <paramref name="seedNames"/> (used for the root)
|
||||||
|
/// guarantees the default case subfolders show even if the share
|
||||||
|
/// listing lags or omits a not-yet-created folder.
|
||||||
|
/// </summary>
|
||||||
|
public async Task LoadChildrenAsync(IEnumerable<string>? seedNames = null)
|
||||||
|
{
|
||||||
|
if (_client == null) return;
|
||||||
|
IsLoading = true;
|
||||||
|
|
||||||
|
// Build into a local list and swap once at the end. The seeds are
|
||||||
|
// added first so that even if the share listing fails the default
|
||||||
|
// subfolders still show; on failure for a non-seeded node the list
|
||||||
|
// is empty and the node reads as a leaf rather than "loading…".
|
||||||
|
var seen = new HashSet<string>(StringComparer.Ordinal);
|
||||||
|
var loaded = new List<FolderNodeViewModel>();
|
||||||
|
if (seedNames != null)
|
||||||
|
{
|
||||||
|
foreach (var n in seedNames)
|
||||||
|
{
|
||||||
|
if (seen.Add(n)) loaded.Add(MakeChild(n));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var items = await _client.ListNetworkStorageFolderAsync(FullPath).ConfigureAwait(true);
|
||||||
|
foreach (var item in items)
|
||||||
|
{
|
||||||
|
if (!item.IsFolder) continue;
|
||||||
|
if (!seen.Add(item.Name)) continue;
|
||||||
|
loaded.Add(MakeChild(item.Name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger?.Warning(ex, "Listing subfolders under '{Path}' failed", FullPath);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
// Marshal onto the Dispatcher: once this node's TreeViewItem
|
||||||
|
// exists, Children is bound and must only be mutated on the UI
|
||||||
|
// thread (the network await above can resume on a thread-pool
|
||||||
|
// thread in this VSTO host).
|
||||||
|
OnUi(() =>
|
||||||
|
{
|
||||||
|
Children.Clear();
|
||||||
|
foreach (var child in loaded) Children.Add(child);
|
||||||
|
});
|
||||||
|
_childrenLoaded = true;
|
||||||
|
IsLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private FolderNodeViewModel MakeChild(string name)
|
||||||
|
{
|
||||||
|
var fullPath = string.IsNullOrEmpty(FullPath) ? name : FullPath + "/" + name;
|
||||||
|
var relativePath = IsRoot ? name : RelativePath + "/" + name;
|
||||||
|
return new FolderNodeViewModel(_client!, _logger!, _dispatcher!, name, fullPath, relativePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnUi(Action action)
|
||||||
|
{
|
||||||
|
var d = _dispatcher;
|
||||||
|
if (d == null || d.CheckAccess()) action();
|
||||||
|
else d.Invoke(action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows.Threading;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
using MarcusLaw.OutlookAddin.Core.Models;
|
using MarcusLaw.OutlookAddin.Core.Models;
|
||||||
@@ -31,6 +32,7 @@ namespace MarcusLaw.OutlookAddin.UI.ViewModels
|
|||||||
|
|
||||||
private readonly IEspoCrmClient _client;
|
private readonly IEspoCrmClient _client;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
|
private readonly Dispatcher _dispatcher;
|
||||||
private CancellationTokenSource? _searchCts;
|
private CancellationTokenSource? _searchCts;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
@@ -40,7 +42,7 @@ namespace MarcusLaw.OutlookAddin.UI.ViewModels
|
|||||||
private CaseEntity? selectedCase;
|
private CaseEntity? selectedCase;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string? selectedSubfolder;
|
private FolderNodeViewModel? selectedFolder;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string? statusMessage;
|
private string? statusMessage;
|
||||||
@@ -58,7 +60,12 @@ namespace MarcusLaw.OutlookAddin.UI.ViewModels
|
|||||||
|
|
||||||
public ObservableCollection<CaseEntity> Cases { get; } = new ObservableCollection<CaseEntity>();
|
public ObservableCollection<CaseEntity> Cases { get; } = new ObservableCollection<CaseEntity>();
|
||||||
|
|
||||||
public ObservableCollection<string> Subfolders { get; } = new ObservableCollection<string>();
|
/// <summary>
|
||||||
|
/// Folder tree for the selected case. Holds a single root node (the
|
||||||
|
/// case folder); its children are the subfolders, loaded lazily so the
|
||||||
|
/// user can drill into any nesting depth.
|
||||||
|
/// </summary>
|
||||||
|
public ObservableCollection<FolderNodeViewModel> FolderTree { get; } = new ObservableCollection<FolderNodeViewModel>();
|
||||||
|
|
||||||
public ObservableCollection<AttachmentRowViewModel> Attachments { get; } = new ObservableCollection<AttachmentRowViewModel>();
|
public ObservableCollection<AttachmentRowViewModel> Attachments { get; } = new ObservableCollection<AttachmentRowViewModel>();
|
||||||
|
|
||||||
@@ -78,14 +85,20 @@ namespace MarcusLaw.OutlookAddin.UI.ViewModels
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Subfolder string the user picked, or null to save to the case
|
/// Folder the user picked, as a path relative to the case folder
|
||||||
/// folder root.
|
/// (e.g. "מסמכים/רפואי"), or null to save to the case folder root.
|
||||||
|
/// Multi-segment paths are honoured by the save target, which appends
|
||||||
|
/// this to <see cref="ResolvedCaseFolderPath"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? ChosenSubfolderName =>
|
public string? ChosenSubfolderName
|
||||||
string.IsNullOrEmpty(SelectedSubfolder) ||
|
{
|
||||||
string.Equals(SelectedSubfolder, RootSentinel, StringComparison.Ordinal)
|
get
|
||||||
? null
|
{
|
||||||
: SelectedSubfolder;
|
var f = SelectedFolder;
|
||||||
|
if (f == null || f.IsPlaceholder || f.IsRoot) return null;
|
||||||
|
return string.IsNullOrEmpty(f.RelativePath) ? null : f.RelativePath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public CaseEntity? PickedCase => SelectedCase;
|
public CaseEntity? PickedCase => SelectedCase;
|
||||||
|
|
||||||
@@ -98,8 +111,15 @@ namespace MarcusLaw.OutlookAddin.UI.ViewModels
|
|||||||
{
|
{
|
||||||
_client = client ?? throw new ArgumentNullException(nameof(client));
|
_client = client ?? throw new ArgumentNullException(nameof(client));
|
||||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
|
// Captured on the UI thread (the VM is constructed on the thread
|
||||||
|
// that shows the dialog). Used to marshal ObservableCollection
|
||||||
|
// mutations back onto the Dispatcher — in this VSTO host the WPF
|
||||||
|
// SynchronizationContext is not installed, so ConfigureAwait(true)
|
||||||
|
// resumes on a thread-pool thread and a bound-collection Add throws
|
||||||
|
// "changes ... from a thread different from the Dispatcher thread".
|
||||||
|
_dispatcher = Dispatcher.CurrentDispatcher;
|
||||||
|
|
||||||
ResetSubfolders();
|
ResetFolderTree();
|
||||||
|
|
||||||
foreach (var att in attachments)
|
foreach (var att in attachments)
|
||||||
{
|
{
|
||||||
@@ -139,12 +159,20 @@ namespace MarcusLaw.OutlookAddin.UI.ViewModels
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ResetSubfolders()
|
/// <summary>Runs <paramref name="action"/> on the UI/Dispatcher thread.</summary>
|
||||||
|
private void OnUi(Action action)
|
||||||
{
|
{
|
||||||
Subfolders.Clear();
|
if (_dispatcher.CheckAccess()) action();
|
||||||
Subfolders.Add(RootSentinel);
|
else _dispatcher.Invoke(action);
|
||||||
foreach (var f in DefaultCaseSubfolders) Subfolders.Add(f);
|
}
|
||||||
SelectedSubfolder = Subfolders[0];
|
|
||||||
|
private void ResetFolderTree()
|
||||||
|
{
|
||||||
|
OnUi(() =>
|
||||||
|
{
|
||||||
|
FolderTree.Clear();
|
||||||
|
SelectedFolder = null;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
partial void OnSearchTextChanged(string value)
|
partial void OnSearchTextChanged(string value)
|
||||||
@@ -169,7 +197,7 @@ namespace MarcusLaw.OutlookAddin.UI.ViewModels
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task RefreshSubfoldersForSelectedCaseAsync()
|
public async Task RefreshSubfoldersForSelectedCaseAsync()
|
||||||
{
|
{
|
||||||
ResetSubfolders();
|
ResetFolderTree();
|
||||||
ResolvedCaseFolderPath = null;
|
ResolvedCaseFolderPath = null;
|
||||||
|
|
||||||
var c = SelectedCase;
|
var c = SelectedCase;
|
||||||
@@ -220,21 +248,28 @@ namespace MarcusLaw.OutlookAddin.UI.ViewModels
|
|||||||
c.Id, c.Number ?? "(none)", primaryContactName ?? "(none)", c.NetworkStorageFolderPath ?? "(none)", folderPath);
|
c.Id, c.Number ?? "(none)", primaryContactName ?? "(none)", c.NetworkStorageFolderPath ?? "(none)", folderPath);
|
||||||
if (string.IsNullOrEmpty(folderPath)) return;
|
if (string.IsNullOrEmpty(folderPath)) return;
|
||||||
|
|
||||||
|
// Build the tree rooted at the case folder. The root node is
|
||||||
|
// selected by default (→ save to case root, matching the previous
|
||||||
|
// default), expanded, and seeded with the default subfolders so
|
||||||
|
// they appear even before the share listing returns. Deeper levels
|
||||||
|
// load lazily as the user expands them.
|
||||||
|
var root = new FolderNodeViewModel(_client, _logger, _dispatcher, RootSentinel, folderPath, relativePath: null);
|
||||||
|
OnUi(() =>
|
||||||
|
{
|
||||||
|
FolderTree.Add(root);
|
||||||
|
SelectedFolder = root;
|
||||||
|
root.IsSelected = true;
|
||||||
|
});
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var items = await _client.ListNetworkStorageFolderAsync(folderPath).ConfigureAwait(true);
|
await root.LoadChildrenAsync(DefaultCaseSubfolders).ConfigureAwait(true);
|
||||||
foreach (var item in items)
|
|
||||||
{
|
|
||||||
if (!item.IsFolder) continue;
|
|
||||||
if (Subfolders.Contains(item.Name)) continue;
|
|
||||||
if (string.Equals(item.Name, RootSentinel, StringComparison.Ordinal)) continue;
|
|
||||||
Subfolders.Add(item.Name);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.Warning(ex, "Listing subfolders for case {CaseId} failed", c.Id);
|
_logger.Warning(ex, "Listing subfolders for case {CaseId} failed", c.Id);
|
||||||
}
|
}
|
||||||
|
OnUi(() => root.IsExpanded = true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -33,6 +33,6 @@ using System.Security;
|
|||||||
// You can specify all the values or you can default the Build and Revision Numbers
|
// You can specify all the values or you can default the Build and Revision Numbers
|
||||||
// by using the '*' as shown below:
|
// by using the '*' as shown below:
|
||||||
// [assembly: AssemblyVersion("1.0.*")]
|
// [assembly: AssemblyVersion("1.0.*")]
|
||||||
[assembly: AssemblyVersion("1.2.14.0")]
|
[assembly: AssemblyVersion("1.2.16.0")]
|
||||||
[assembly: AssemblyFileVersion("1.2.14.0")]
|
[assembly: AssemblyFileVersion("1.2.16.0")]
|
||||||
|
|
||||||
|
|||||||
@@ -127,6 +127,62 @@ namespace OutlookAddin.Services
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Shows a message box that is guaranteed to render above Outlook and
|
||||||
|
/// any other window. A bare <see cref="MessageBox.Show(string)"/> has
|
||||||
|
/// no owner — once our WPF dialog closes there is no active window on
|
||||||
|
/// the add-in thread, so the box sinks behind Outlook and the user
|
||||||
|
/// never sees the success/error result. We anchor it to a transient,
|
||||||
|
/// invisible top-most owner window parented to the foreground Outlook
|
||||||
|
/// window so it always surfaces on top. Caption is always "Klear".
|
||||||
|
/// </summary>
|
||||||
|
private MessageBoxResult ShowMessage(
|
||||||
|
string text,
|
||||||
|
MessageBoxButton button = MessageBoxButton.OK,
|
||||||
|
MessageBoxImage icon = MessageBoxImage.None)
|
||||||
|
{
|
||||||
|
Window? owner = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// A 1×1 borderless window at screen centre: imperceptible, but
|
||||||
|
// gives the MessageBox a top-most owner to z-order above. The
|
||||||
|
// box centres on this point → effectively centred on screen.
|
||||||
|
owner = new Window
|
||||||
|
{
|
||||||
|
Width = 1,
|
||||||
|
Height = 1,
|
||||||
|
WindowStyle = WindowStyle.None,
|
||||||
|
ResizeMode = ResizeMode.NoResize,
|
||||||
|
ShowInTaskbar = false,
|
||||||
|
Topmost = true,
|
||||||
|
WindowStartupLocation = WindowStartupLocation.CenterScreen,
|
||||||
|
};
|
||||||
|
|
||||||
|
var hwnd = GetForegroundWindow();
|
||||||
|
if (hwnd == IntPtr.Zero)
|
||||||
|
{
|
||||||
|
hwnd = System.Diagnostics.Process.GetCurrentProcess().MainWindowHandle;
|
||||||
|
}
|
||||||
|
if (hwnd != IntPtr.Zero)
|
||||||
|
{
|
||||||
|
new WindowInteropHelper(owner).Owner = hwnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
owner.Show();
|
||||||
|
owner.Activate();
|
||||||
|
return MessageBox.Show(owner, text, "Klear", button, icon);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.Warning(ex, "ShowMessage top-most owner failed; falling back to ownerless box");
|
||||||
|
return MessageBox.Show(text, "Klear", button, icon);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
try { owner?.Close(); } catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static void EnableModernTls()
|
private static void EnableModernTls()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -402,9 +458,8 @@ namespace OutlookAddin.Services
|
|||||||
if (mail == null) return;
|
if (mail == null) return;
|
||||||
if (!IsConfigured)
|
if (!IsConfigured)
|
||||||
{
|
{
|
||||||
MessageBox.Show(
|
ShowMessage(
|
||||||
"התוסף עדיין לא מוגדר. פתח את ההגדרות תחילה.",
|
"התוסף עדיין לא מוגדר. פתח את ההגדרות תחילה.",
|
||||||
"Klear",
|
|
||||||
MessageBoxButton.OK,
|
MessageBoxButton.OK,
|
||||||
MessageBoxImage.Information);
|
MessageBoxImage.Information);
|
||||||
return;
|
return;
|
||||||
@@ -423,9 +478,8 @@ namespace OutlookAddin.Services
|
|||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger.Error(ex, "LaunchComposeFromCaseAsync failed");
|
Logger.Error(ex, "LaunchComposeFromCaseAsync failed");
|
||||||
MessageBox.Show(
|
ShowMessage(
|
||||||
"טעינת התיק נכשלה: " + ex.Message,
|
"טעינת התיק נכשלה: " + ex.Message,
|
||||||
"Klear",
|
|
||||||
MessageBoxButton.OK,
|
MessageBoxButton.OK,
|
||||||
MessageBoxImage.Error);
|
MessageBoxImage.Error);
|
||||||
}
|
}
|
||||||
@@ -444,9 +498,8 @@ namespace OutlookAddin.Services
|
|||||||
if (mail == null) return;
|
if (mail == null) return;
|
||||||
if (!IsConfigured)
|
if (!IsConfigured)
|
||||||
{
|
{
|
||||||
var result = MessageBox.Show(
|
var result = ShowMessage(
|
||||||
"התוסף עדיין לא מוגדר. לפתוח את חלון ההגדרות עכשיו?",
|
"התוסף עדיין לא מוגדר. לפתוח את חלון ההגדרות עכשיו?",
|
||||||
"Klear",
|
|
||||||
MessageBoxButton.YesNo,
|
MessageBoxButton.YesNo,
|
||||||
MessageBoxImage.Information);
|
MessageBoxImage.Information);
|
||||||
if (result == MessageBoxResult.Yes) LaunchSettings();
|
if (result == MessageBoxResult.Yes) LaunchSettings();
|
||||||
@@ -463,9 +516,8 @@ namespace OutlookAddin.Services
|
|||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger.Error(ex, "LaunchSaveAttachmentsAsync: extract failed");
|
Logger.Error(ex, "LaunchSaveAttachmentsAsync: extract failed");
|
||||||
MessageBox.Show(
|
ShowMessage(
|
||||||
"קריאת הקבצים המצורפים נכשלה: " + ex.Message,
|
"קריאת הקבצים המצורפים נכשלה: " + ex.Message,
|
||||||
"Klear",
|
|
||||||
MessageBoxButton.OK,
|
MessageBoxButton.OK,
|
||||||
MessageBoxImage.Error);
|
MessageBoxImage.Error);
|
||||||
return;
|
return;
|
||||||
@@ -473,9 +525,8 @@ namespace OutlookAddin.Services
|
|||||||
|
|
||||||
if (envelope.Attachments.Count == 0)
|
if (envelope.Attachments.Count == 0)
|
||||||
{
|
{
|
||||||
MessageBox.Show(
|
ShowMessage(
|
||||||
"אין במייל הזה קבצים מצורפים לשמירה.",
|
"אין במייל הזה קבצים מצורפים לשמירה.",
|
||||||
"Klear",
|
|
||||||
MessageBoxButton.OK,
|
MessageBoxButton.OK,
|
||||||
MessageBoxImage.Information);
|
MessageBoxImage.Information);
|
||||||
return;
|
return;
|
||||||
@@ -510,9 +561,8 @@ namespace OutlookAddin.Services
|
|||||||
: SaveAttachmentsViewModel.ResolveCaseFolderPath(picked);
|
: SaveAttachmentsViewModel.ResolveCaseFolderPath(picked);
|
||||||
if (string.IsNullOrEmpty(caseFolder))
|
if (string.IsNullOrEmpty(caseFolder))
|
||||||
{
|
{
|
||||||
MessageBox.Show(
|
ShowMessage(
|
||||||
"לא ניתן לחשב את נתיב התיקייה של התיק. ייתכן שהתיק עוד לא הוגדר ב-Klear.",
|
"לא ניתן לחשב את נתיב התיקייה של התיק. ייתכן שהתיק עוד לא הוגדר ב-Klear.",
|
||||||
"Klear",
|
|
||||||
MessageBoxButton.OK,
|
MessageBoxButton.OK,
|
||||||
MessageBoxImage.Warning);
|
MessageBoxImage.Warning);
|
||||||
return;
|
return;
|
||||||
@@ -540,9 +590,8 @@ namespace OutlookAddin.Services
|
|||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger.Error(ex, "LaunchSaveAttachmentsAsync failed");
|
Logger.Error(ex, "LaunchSaveAttachmentsAsync failed");
|
||||||
MessageBox.Show(
|
ShowMessage(
|
||||||
"שמירת הקבצים נכשלה: " + ex.Message,
|
"שמירת הקבצים נכשלה: " + ex.Message,
|
||||||
"Klear",
|
|
||||||
MessageBoxButton.OK,
|
MessageBoxButton.OK,
|
||||||
MessageBoxImage.Error);
|
MessageBoxImage.Error);
|
||||||
}
|
}
|
||||||
@@ -572,7 +621,7 @@ namespace OutlookAddin.Services
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ShowAttachmentSummary(AttachmentSaveSummary summary, string caseLabel, string targetPath)
|
private void ShowAttachmentSummary(AttachmentSaveSummary summary, string caseLabel, string targetPath)
|
||||||
{
|
{
|
||||||
string message;
|
string message;
|
||||||
MessageBoxImage icon;
|
MessageBoxImage icon;
|
||||||
@@ -589,7 +638,7 @@ namespace OutlookAddin.Services
|
|||||||
message = parts.Count == 0 ? "לא נשמרו פריטים." : string.Join(" · ", parts);
|
message = parts.Count == 0 ? "לא נשמרו פריטים." : string.Join(" · ", parts);
|
||||||
icon = summary.FailedCount > 0 ? MessageBoxImage.Warning : MessageBoxImage.Information;
|
icon = summary.FailedCount > 0 ? MessageBoxImage.Warning : MessageBoxImage.Information;
|
||||||
}
|
}
|
||||||
MessageBox.Show(message, "Klear", MessageBoxButton.OK, icon);
|
ShowMessage(message, MessageBoxButton.OK, icon);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -601,9 +650,8 @@ namespace OutlookAddin.Services
|
|||||||
{
|
{
|
||||||
if (!IsConfigured)
|
if (!IsConfigured)
|
||||||
{
|
{
|
||||||
MessageBox.Show(
|
ShowMessage(
|
||||||
"התוסף עדיין לא מוגדר. פתח את ההגדרות תחילה.",
|
"התוסף עדיין לא מוגדר. פתח את ההגדרות תחילה.",
|
||||||
"Klear",
|
|
||||||
MessageBoxButton.OK,
|
MessageBoxButton.OK,
|
||||||
MessageBoxImage.Information);
|
MessageBoxImage.Information);
|
||||||
return;
|
return;
|
||||||
@@ -620,9 +668,8 @@ namespace OutlookAddin.Services
|
|||||||
{
|
{
|
||||||
Logger.Error(ex, "LaunchComposeFromCaseByIdAsync failed for caseId={CaseId}", caseId);
|
Logger.Error(ex, "LaunchComposeFromCaseByIdAsync failed for caseId={CaseId}", caseId);
|
||||||
if (mail != null) try { Marshal.ReleaseComObject(mail); } catch { }
|
if (mail != null) try { Marshal.ReleaseComObject(mail); } catch { }
|
||||||
MessageBox.Show(
|
ShowMessage(
|
||||||
"פתיחת המייל מתיק נכשלה: " + ex.Message,
|
"פתיחת המייל מתיק נכשלה: " + ex.Message,
|
||||||
"Klear",
|
|
||||||
MessageBoxButton.OK,
|
MessageBoxButton.OK,
|
||||||
MessageBoxImage.Error);
|
MessageBoxImage.Error);
|
||||||
}
|
}
|
||||||
@@ -685,9 +732,8 @@ namespace OutlookAddin.Services
|
|||||||
{
|
{
|
||||||
if (!IsConfigured)
|
if (!IsConfigured)
|
||||||
{
|
{
|
||||||
var result = MessageBox.Show(
|
var result = ShowMessage(
|
||||||
"התוסף עדיין לא מוגדר. לפתוח את חלון ההגדרות עכשיו?",
|
"התוסף עדיין לא מוגדר. לפתוח את חלון ההגדרות עכשיו?",
|
||||||
"Klear",
|
|
||||||
MessageBoxButton.YesNo,
|
MessageBoxButton.YesNo,
|
||||||
MessageBoxImage.Information);
|
MessageBoxImage.Information);
|
||||||
if (result == MessageBoxResult.Yes)
|
if (result == MessageBoxResult.Yes)
|
||||||
@@ -700,9 +746,8 @@ namespace OutlookAddin.Services
|
|||||||
var mailItems = ReadCurrentSelection();
|
var mailItems = ReadCurrentSelection();
|
||||||
if (mailItems.Count == 0)
|
if (mailItems.Count == 0)
|
||||||
{
|
{
|
||||||
MessageBox.Show(
|
ShowMessage(
|
||||||
"לא נבחר מייל לתיוק. סמן מייל אחד או יותר ונסה שוב.",
|
"לא נבחר מייל לתיוק. סמן מייל אחד או יותר ונסה שוב.",
|
||||||
"Klear",
|
|
||||||
MessageBoxButton.OK,
|
MessageBoxButton.OK,
|
||||||
MessageBoxImage.Information);
|
MessageBoxImage.Information);
|
||||||
return;
|
return;
|
||||||
@@ -743,9 +788,8 @@ namespace OutlookAddin.Services
|
|||||||
Logger.Warning(
|
Logger.Warning(
|
||||||
"Filing skipped — selected EspoEntityRef has no usable Id/EntityType (id={Id}, name={Name}, type={Type})",
|
"Filing skipped — selected EspoEntityRef has no usable Id/EntityType (id={Id}, name={Name}, type={Type})",
|
||||||
vm.SelectedTarget?.Id, vm.SelectedTarget?.Name, vm.SelectedTarget?.EntityType);
|
vm.SelectedTarget?.Id, vm.SelectedTarget?.Name, vm.SelectedTarget?.EntityType);
|
||||||
MessageBox.Show(
|
ShowMessage(
|
||||||
"התיק שנבחר לא הוחזר עם סוג ישות (entityType) תקין. ייתכן ש-Klear שלך מחזיר שדה אחר; הלוג מכיל את הפרטים. שלח לאדמין.",
|
"התיק שנבחר לא הוחזר עם סוג ישות (entityType) תקין. ייתכן ש-Klear שלך מחזיר שדה אחר; הלוג מכיל את הפרטים. שלח לאדמין.",
|
||||||
"Klear",
|
|
||||||
MessageBoxButton.OK,
|
MessageBoxButton.OK,
|
||||||
MessageBoxImage.Warning);
|
MessageBoxImage.Warning);
|
||||||
return;
|
return;
|
||||||
@@ -917,7 +961,7 @@ namespace OutlookAddin.Services
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ShowSummary(FilingBatchSummary summary, FilingTarget target)
|
private void ShowSummary(FilingBatchSummary summary, FilingTarget target)
|
||||||
{
|
{
|
||||||
string message;
|
string message;
|
||||||
MessageBoxImage icon;
|
MessageBoxImage icon;
|
||||||
@@ -938,7 +982,7 @@ namespace OutlookAddin.Services
|
|||||||
icon = summary.FailedCount > 0 ? MessageBoxImage.Warning : MessageBoxImage.Information;
|
icon = summary.FailedCount > 0 ? MessageBoxImage.Warning : MessageBoxImage.Information;
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageBox.Show(message, "Klear", MessageBoxButton.OK, icon);
|
ShowMessage(message, MessageBoxButton.OK, icon);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string? TryGetSenderName(Outlook.MailItem item)
|
private static string? TryGetSenderName(Outlook.MailItem item)
|
||||||
|
|||||||
Reference in New Issue
Block a user