Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c685f4f91d | |||
| 5eae3da213 |
@@ -258,15 +258,21 @@ jobs:
|
||||
& msbuild @msbuildArgs
|
||||
if ($LASTEXITCODE -ne 0) { throw "msbuild /t:Publish failed with $LASTEXITCODE" }
|
||||
|
||||
# Sanity check: did the VSTO publish target produce a .vsto file?
|
||||
$vsto = Get-ChildItem -Recurse -Filter '*.vsto' -Path publish | Select-Object -First 1
|
||||
if (-not $vsto) { throw "msbuild /t:Publish completed but no .vsto file found under publish/" }
|
||||
# Sanity check + rename to canonical OutlookAddin.vsto so the URL
|
||||
# matches PROVIDER_URL (hardcoded to .../OutlookAddin.vsto). Restrict
|
||||
# to publish/ root -- there's a second .vsto inside Application Files
|
||||
# that must keep its versioned name. Verify the renamed file exists
|
||||
# before continuing; v1.2.11 shipped broken because a silent failure
|
||||
# here left no OutlookAddin.vsto in the tar and 404'd the CDN.
|
||||
$vsto = Get-ChildItem -File -Filter '*.vsto' -Path publish | Select-Object -First 1
|
||||
if (-not $vsto) { throw "msbuild /t:Publish completed but no .vsto file found at publish/ root" }
|
||||
Write-Host " produced $($vsto.FullName)"
|
||||
|
||||
# Rename to canonical OutlookAddin.vsto so the ClickOnce manifest
|
||||
# URL matches PROVIDER_URL (which is hardcoded to .../OutlookAddin.vsto).
|
||||
if ($vsto.Name -ne 'OutlookAddin.vsto') {
|
||||
Rename-Item -LiteralPath $vsto.FullName -NewName 'OutlookAddin.vsto'
|
||||
Rename-Item -LiteralPath $vsto.FullName -NewName 'OutlookAddin.vsto' -ErrorAction Stop
|
||||
Write-Host " renamed -> OutlookAddin.vsto"
|
||||
}
|
||||
if (-not (Test-Path 'publish/OutlookAddin.vsto')) {
|
||||
throw "post-rename: publish/OutlookAddin.vsto does not exist"
|
||||
}
|
||||
|
||||
Write-Host "=== STEP F: stage protocol handler ==="
|
||||
|
||||
@@ -213,13 +213,22 @@ namespace MarcusLaw.OutlookAddin.Core.Services
|
||||
// is sent in the "file" field as a data-URI. The previous extra
|
||||
// "contents" alias was an undocumented legacy that strict tenants
|
||||
// reject with 400.
|
||||
//
|
||||
// We deliberately DO NOT bind the upload to Document.file
|
||||
// (relatedType=Document / field=file). EspoCRM only enforces the
|
||||
// per-field "allowed file types" list when a field is supplied, and
|
||||
// Marcus-Law's Document.file field is configured to accept document
|
||||
// types only (PDF/Word) — images (JPEG/PNG) were rejected with
|
||||
// 403 "Not allowed file type". This flow never creates a Document;
|
||||
// the attachment is a standalone blob we immediately push to the
|
||||
// network share, so an unbound role=Attachment upload is the
|
||||
// correct shape and lets every file type through. (Server-side fix:
|
||||
// widen Document.file's accepted types in the Entity Manager.)
|
||||
var payload = new Dictionary<string, object?>
|
||||
{
|
||||
["name"] = fileName,
|
||||
["type"] = mime,
|
||||
["role"] = "Attachment",
|
||||
["relatedType"] = "Document",
|
||||
["field"] = "file",
|
||||
["file"] = dataUri
|
||||
};
|
||||
|
||||
|
||||
@@ -107,6 +107,38 @@ namespace MarcusLaw.OutlookAddin.UI.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Seeds the case list with the cases already resolved for the mail's
|
||||
/// sender (from the sidebar match) so the user doesn't have to search
|
||||
/// for a client we've already identified. When exactly one case is
|
||||
/// known we auto-select it (which kicks the subfolder/folder-path
|
||||
/// resolve); with several we just show them and let the user pick.
|
||||
/// The user can still type in the search box to override — that
|
||||
/// repopulates <see cref="Cases"/> from a fresh server search.
|
||||
/// </summary>
|
||||
public void PreselectCases(IReadOnlyList<CaseEntity> cases)
|
||||
{
|
||||
if (cases == null || cases.Count == 0) return;
|
||||
|
||||
Cases.Clear();
|
||||
foreach (var c in cases)
|
||||
{
|
||||
if (c == null || string.IsNullOrEmpty(c.Id)) continue;
|
||||
Cases.Add(c);
|
||||
}
|
||||
if (Cases.Count == 0) return;
|
||||
|
||||
if (Cases.Count == 1)
|
||||
{
|
||||
SelectedCase = Cases[0];
|
||||
StatusMessage = "התיק זוהה אוטומטית מהשולח";
|
||||
}
|
||||
else
|
||||
{
|
||||
StatusMessage = $"{Cases.Count} תיקים מקושרים לשולח — בחר/י תיק";
|
||||
}
|
||||
}
|
||||
|
||||
private void ResetSubfolders()
|
||||
{
|
||||
Subfolders.Clear();
|
||||
|
||||
@@ -257,6 +257,9 @@
|
||||
<EmbeddedResource Include="Ribbon\InspectorRibbon.xml">
|
||||
<LogicalName>OutlookAddin.Ribbon.InspectorRibbon.xml</LogicalName>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="Ribbon\ReadMailRibbon.xml">
|
||||
<LogicalName>OutlookAddin.Ribbon.ReadMailRibbon.xml</LogicalName>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="Images\klear-file.png">
|
||||
<LogicalName>OutlookAddin.Images.klear-file.png</LogicalName>
|
||||
</EmbeddedResource>
|
||||
|
||||
@@ -33,6 +33,6 @@ using System.Security;
|
||||
// You can specify all the values or you can default the Build and Revision Numbers
|
||||
// by using the '*' as shown below:
|
||||
// [assembly: AssemblyVersion("1.0.*")]
|
||||
[assembly: AssemblyVersion("1.2.11.0")]
|
||||
[assembly: AssemblyFileVersion("1.2.11.0")]
|
||||
[assembly: AssemblyVersion("1.2.13.0")]
|
||||
[assembly: AssemblyFileVersion("1.2.13.0")]
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ namespace OutlookAddin.Ribbon
|
||||
{
|
||||
private const string ExplorerResource = "OutlookAddin.Ribbon.ExplorerRibbon.xml";
|
||||
private const string InspectorResource = "OutlookAddin.Ribbon.InspectorRibbon.xml";
|
||||
private const string ReadMailResource = "OutlookAddin.Ribbon.ReadMailRibbon.xml";
|
||||
|
||||
// Every button whose `getEnabled` depends on the current Explorer
|
||||
// selection must be re-evaluated on every SelectionChange — Office
|
||||
@@ -40,6 +41,8 @@ namespace OutlookAddin.Ribbon
|
||||
return LoadResource(ExplorerResource);
|
||||
case "Microsoft.Outlook.Mail.Compose":
|
||||
return LoadResource(InspectorResource);
|
||||
case "Microsoft.Outlook.Mail.Read":
|
||||
return LoadResource(ReadMailResource);
|
||||
default:
|
||||
return string.Empty;
|
||||
}
|
||||
@@ -77,6 +80,51 @@ namespace OutlookAddin.Ribbon
|
||||
_inspectorRibbon = ribbon;
|
||||
}
|
||||
|
||||
// The read-mail inspector ribbon loads fresh for each opened message,
|
||||
// so its getEnabled callback re-runs per window — no cached IRibbonUI
|
||||
// or invalidation is needed here.
|
||||
public void OnReadMailRibbonLoad(IRibbonUI ribbon)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enabled when the message open in this read inspector has at least
|
||||
/// one attachment. Mirrors <see cref="OnGetSaveAttachmentsEnabled"/>
|
||||
/// but reads the item from the Inspector context rather than the
|
||||
/// Explorer selection.
|
||||
/// </summary>
|
||||
public bool OnGetSaveAttachmentsInspectorEnabled(IRibbonControl control)
|
||||
{
|
||||
try
|
||||
{
|
||||
var inspector = control?.Context as Outlook.Inspector;
|
||||
if (inspector?.CurrentItem is not Outlook.MailItem mail) return false;
|
||||
var attachments = mail.Attachments;
|
||||
if (attachments == null) return false;
|
||||
return attachments.Count > 0;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void OnSaveAttachmentsFromInspector(IRibbonControl control)
|
||||
{
|
||||
try
|
||||
{
|
||||
var inspector = control?.Context as Outlook.Inspector;
|
||||
if (inspector?.CurrentItem is Outlook.MailItem mail)
|
||||
{
|
||||
_ = Globals.ThisAddIn.AddInHost.LaunchSaveAttachmentsAsync(mail);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Globals.ThisAddIn.AddInHost.Logger.Warning(ex, "OnSaveAttachmentsFromInspector failed");
|
||||
}
|
||||
}
|
||||
|
||||
public bool OnGetFileToEspoCrmEnabled(IRibbonControl control)
|
||||
{
|
||||
try
|
||||
@@ -206,6 +254,7 @@ namespace OutlookAddin.Ribbon
|
||||
{
|
||||
case "FileToEspoCrm": fileName = "klear-file.png"; break;
|
||||
case "SaveAttachments": fileName = "klear-attach.png"; break;
|
||||
case "SaveAttachmentsRead": fileName = "klear-attach.png"; break;
|
||||
case "ShowSidebar": fileName = "klear-sidebar.png"; break;
|
||||
case "OpenEspoCrmSettings": fileName = "klear-settings.png"; break;
|
||||
case "ComposeFromCase": fileName = "klear-compose.png"; break;
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<customUI xmlns="http://schemas.microsoft.com/office/2009/07/customui" onLoad="OnReadMailRibbonLoad">
|
||||
<ribbon>
|
||||
<tabs>
|
||||
<tab idMso="TabReadMessage">
|
||||
<group id="MarcusLawReadGroup" label="Klear">
|
||||
<button id="SaveAttachmentsRead"
|
||||
label="שמור קבצים"
|
||||
screentip="שמור את הקבצים המצורפים בלבד"
|
||||
supertip="מעלה את הקבצים המצורפים של המייל הזה ל-Klear כ-Documents מקושרים לתיק שתבחר, בלי לתייק את המייל עצמו."
|
||||
size="large"
|
||||
getImage="OnGetImage"
|
||||
onAction="OnSaveAttachmentsFromInspector"
|
||||
getEnabled="OnGetSaveAttachmentsInspectorEnabled" />
|
||||
</group>
|
||||
</tab>
|
||||
</tabs>
|
||||
</ribbon>
|
||||
</customUI>
|
||||
@@ -485,6 +485,13 @@ namespace OutlookAddin.Services
|
||||
// Subfolders are populated lazily once the user picks a case;
|
||||
// the default static set is already pre-loaded.
|
||||
|
||||
// If the sender already maps to a known client, seed the case
|
||||
// list so the user sees the identified case pre-selected instead
|
||||
// of having to search. Uses the same matching lookup the sidebar
|
||||
// does (cached for 10 min), so this is normally instant. Any
|
||||
// failure here is non-fatal — the dialog still opens empty.
|
||||
await TryPreselectSenderCaseAsync(vm, envelope.From).ConfigureAwait(true);
|
||||
|
||||
var dialog = new SaveAttachmentsDialog(vm);
|
||||
AttachOwnerToOutlook(dialog);
|
||||
var ok = dialog.ShowDialog();
|
||||
@@ -541,6 +548,30 @@ namespace OutlookAddin.Services
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up the mail sender against the CRM (reusing the sidebar's
|
||||
/// cached match) and, when it resolves to one or more linked cases,
|
||||
/// seeds the save-attachments dialog so the already-identified client
|
||||
/// case shows up pre-selected. Best-effort: swallows any error.
|
||||
/// </summary>
|
||||
private async Task TryPreselectSenderCaseAsync(SaveAttachmentsViewModel vm, string? senderEmail)
|
||||
{
|
||||
if (MatchingService == null || string.IsNullOrWhiteSpace(senderEmail)) return;
|
||||
try
|
||||
{
|
||||
var match = await MatchingService.LookupAsync(senderEmail!).ConfigureAwait(true);
|
||||
// Ambiguous (same email on several contacts) → don't guess; let
|
||||
// the user search. A clean match with linked cases → seed them.
|
||||
if (match == null || match.IsAmbiguous) return;
|
||||
if (match.RecentCases == null || match.RecentCases.Count == 0) return;
|
||||
vm.PreselectCases(match.RecentCases);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Warning(ex, "Sender-case preselect for save-attachments failed (non-fatal)");
|
||||
}
|
||||
}
|
||||
|
||||
private static void ShowAttachmentSummary(AttachmentSaveSummary summary, string caseLabel, string targetPath)
|
||||
{
|
||||
string message;
|
||||
|
||||
Reference in New Issue
Block a user