Skip to content
Snippets Groups Projects
Commit 52d7c06c authored by Reinhold Kainhofer's avatar Reinhold Kainhofer
Browse files

Implement inserting placeholders, add alert box

parent 530a9be8
Branches
No related tags found
No related merge requests found
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<OfficeApp xmlns="http://schemas.microsoft.com/office/appforoffice/1.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bt="http://schemas.microsoft.com/office/officeappbasictypes/1.0" xmlns:mailappor="http://schemas.microsoft.com/office/mailappversionoverrides/1.0" xsi:type="MailApp"> <OfficeApp xmlns="http://schemas.microsoft.com/office/appforoffice/1.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bt="http://schemas.microsoft.com/office/officeappbasictypes/1.0" xmlns:mailappor="http://schemas.microsoft.com/office/mailappversionoverrides/1.0" xsi:type="MailApp">
<Id>8df42c88-61ce-4c8a-aa94-a46d56cbf46c</Id> <Id>8df42c88-61ce-4c8a-aa94-a46d56cbf46c</Id>
<Version>0.1.0.0</Version> <Version>1.0.0.0</Version>
<ProviderName>Open Tools</ProviderName> <ProviderName>Open Tools</ProviderName>
<DefaultLocale>en-US</DefaultLocale> <DefaultLocale>en-US</DefaultLocale>
<DisplayName DefaultValue="Outlook MailMerge"/> <DisplayName DefaultValue="Outlook MailMerge"/>
......
...@@ -28,4 +28,42 @@ body { ...@@ -28,4 +28,42 @@ body {
padding: 8px; padding: 8px;
text-align: left; text-align: left;
} }
\ No newline at end of file
/* Container for the message */
.message-container {
display: none; /* Hidden by default */
position: fixed;
top: 20px;
right: 20px;
padding: 15px;
background-color: #f44336; /* Red background for error messages */
color: white;
border-radius: 5px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
z-index: 1000;
max-width: 300px;
font-family: Arial, sans-serif;
transition: opacity 0.3s ease;
}
/* Close button styles */
.close-button {
position: absolute;
top: 5px;
right: 5px;
font-size: 20px;
cursor: pointer;
color: white;
}
/* Add animation for showing and hiding */
.message-container.show {
display: block;
opacity: 1;
}
.message-container.hide {
opacity: 0;
display: none;
}
\ No newline at end of file
...@@ -10,7 +10,10 @@ ...@@ -10,7 +10,10 @@
<div name="background"> <div name="background">
<p>This add-on provides serial mail with individual attachments. Data is provided by an Excel table, Individual fields in the text can be inserted as {%fieldname%}.</p> <p>This add-on provides serial mail with individual attachments. Data is provided by an Excel table, Individual fields in the text can be inserted as {%fieldname%}.</p>
<p class="label"></p> <div id="messageDiv" class="message-container">
<span id="closeMessageButton" class="close-button">&times;</span>
<p id="messageText"></p>
</div>
<fieldset> <fieldset>
<legend>1. Select data source (Excel file):</legend> <legend>1. Select data source (Excel file):</legend>
...@@ -21,7 +24,10 @@ ...@@ -21,7 +24,10 @@
<fieldset> <fieldset>
<legend>2. E-Mail - Placeholder fields:</legend> <legend>2. E-Mail - Placeholder fields:</legend>
<div class="placeholder_select">Placeholder: <select id="placeholderSelect" disabled></select><input id="insertPlaceholder" /></div> <label for="placeholderSelect">Placeholder:</label>
<select id="placeholderSelect" disabled></select>
<button id="placeholderInsertButton">Insert</button>
<div id="placeholderCopyLabel">Copy</div>
</fieldset> </fieldset>
<fieldset> <fieldset>
...@@ -47,6 +53,7 @@ ...@@ -47,6 +53,7 @@
<div id="dataPreview"></div> <div id="dataPreview"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
<script src="https://appsforoffice.microsoft.com/lib/1/hosted/office.js"></script>
<script src="taskpane.js"></script> <script src="taskpane.js"></script>
</div> </div>
</body> </body>
......
/* /*
* Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. * Copyright (c) 2025 Reinhold Kainhofer, Open Tools, office@open-tools.net
* See LICENSE in the project root for license information.
*/ */
/* global document, Office */ // Ensure the Office.js library is loaded the office context is fully initialized!
Office.onReady(function (info) {
// Office is ready
/*
Office.onReady(() => {
// Office Add-in is ready
console.log("Add-in is ready.");
});
Office.onReady((info) => {
if (info.host === Office.HostType.Outlook) { if (info.host === Office.HostType.Outlook) {
document.getElementById("sideload-msg").style.display = "none"; // Now you can safely use Office.context.mailbox
document.getElementById("app-body").style.display = "flex"; Office.context.mailbox.item.body.getTypeAsync((result) => {
document.getElementById("run").onclick = run; if (result.status === Office.AsyncResultStatus.Succeeded) {
console.log(result.value); // the body type (e.g., Html or Text)
} else {
console.error("Failed to get body type:", result.error);
}
});
} }
}); });
export async function run() {
// Insert your Outlook code here
const item = Office.context.mailbox.item;
let insertAt = document.getElementById("item-subject");
let label = document.createElement("b").appendChild(document.createTextNode("Subject: "));
insertAt.appendChild(label);
insertAt.appendChild(document.createElement("br"));
insertAt.appendChild(document.createTextNode(item.subject));
insertAt.appendChild(document.createElement("br"));
};
*/
const fileInput = document.getElementById("excelFileInput");
const worksheetSelect = document.getElementById("worksheetSelect");
const placeholderSelect = document.getElementById("placeholderSelect");
const attachmentColumnSelect = document.getElementById("attachmentColumnSelect");
const appendExtensionCheckbox = document.getElementById("appendExtensionCheckbox");
const fileExtensionInput = document.getElementById("fileExtensionInput");
const attachmentFolderButton = document.getElementById('selectAttachmentFolderButton');
const attachmentFolderSpan = document.getElementById('selectedAttachmentFolder');
let workbook = null; let workbook = null;
// Create a global EventTarget instance
const eventBus = new EventTarget();
function displayMessage(message) {
const messageDiv = document.getElementById("messageDiv");
const messageText = document.getElementById("messageText");
const closeButton = document.getElementById("closeMessageButton");
// Set the message text
messageText.textContent = message;
// Show the message
messageDiv.classList.add("show");
messageDiv.classList.remove("hide");
setTimeout(() => {
messageDiv.classList.add("hide");
setTimeout(() => {
messageDiv.classList.remove("show", "hide");
}, 300);
}, 5000); // Hide the message after 5 seconds
// Close button functionality
closeButton.addEventListener("click", function() {
messageDiv.classList.add("hide");
setTimeout(() => {
messageDiv.classList.remove("show", "hide");
}, 300); // Remove the show and hide classes after the fade-out animation
});
fileInput.addEventListener("change", handleFile); }
/***********************************************************
* 1. Select data source (Excel worksheet, CSV file etc.)
*
* After selecting the file, potentially multiple sheets / tables are available for selection
*
***********************************************************/
const worksheetSelect = document.getElementById("worksheetSelect");
document.getElementById("excelFileInput").addEventListener("change", handleFile);
worksheetSelect.addEventListener("change", handleWorksheetSelection); worksheetSelect.addEventListener("change", handleWorksheetSelection);
attachmentColumnSelect.addEventListener("change", handleAttachmentColumnSelection);
appendExtensionCheckbox.addEventListener("change", () => {
fileExtensionInput.disabled = !appendExtensionCheckbox.checked;
});
attachmentFolderButton.addEventListener("click", async () => {
try {
const directoryHandle = await window.showDirectoryPicker();
attachmentFolderSpan.textContent = `Selected folder: ${directoryHandle.name}`;
console.log("Selected folder:", directoryHandle);
} catch (err) {
console.error("Folder selection cancelled or failed", err);
}
});
/*
* Read the selected file (Excel, csv) into memory
*/
function handleFile(event) { function handleFile(event) {
const file = event.target.files[0]; const file = event.target.files[0];
if (!file) return; if (!file) return;
...@@ -66,26 +77,25 @@ function handleFile(event) { ...@@ -66,26 +77,25 @@ function handleFile(event) {
const reader = new FileReader(); const reader = new FileReader();
reader.onload = function (e) { reader.onload = function (e) {
const data = new Uint8Array(e.target.result); const data = new Uint8Array(e.target.result);
const binary = Array.from(data).map(byte => String.fromCharCode(byte)).join('');
const fileType = file.name.split(".").pop().toLowerCase(); const fileType = file.name.split(".").pop().toLowerCase();
// Determine the file type and parse accordingly // Determine the file type and parse accordingly
const options = { type: "array" };
if (fileType === "xlsb") { if (fileType === "xlsb") {
options.type = "binary"; workbook = XLSX.read(binary, { type: "binary" });
} else {
workbook = XLSX.read(data, { type: "array" });
} }
workbook = XLSX.read(data, options);
populateWorksheetDropdown(); populateWorksheetDropdown();
}; };
const fileType = file.name.split(".").pop().toLowerCase(); reader.readAsArrayBuffer(file);
if (fileType === "xlsb") {
reader.readAsBinaryString(file);
} else {
reader.readAsArrayBuffer(file);
}
} }
/*
* Make the list of worksheets selectable in a combo box and auto-select the first one
*/
function populateWorksheetDropdown() { function populateWorksheetDropdown() {
worksheetSelect.innerHTML = ""; worksheetSelect.innerHTML = "";
worksheetSelect.disabled = false; worksheetSelect.disabled = false;
...@@ -112,28 +122,190 @@ function handleWorksheetSelection(event) { ...@@ -112,28 +122,190 @@ function handleWorksheetSelection(event) {
// Populate the placeholders combobox and the attachments column combo // Populate the placeholders combobox and the attachments column combo
const worksheet = workbook.Sheets[sheetName]; const worksheet = workbook.Sheets[sheetName];
const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 }); const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
const columns = jsonData[0] || [];
populateColumnSelect(jsonData[0] || []); const dataReadyEvent = new CustomEvent("dataReady", {
displayData(jsonData); // TODO: Where/how shall we display the table data? detail: {
data: jsonData, // Pass the loaded data as part of the event
columns: columns, // Pass the availabe column names
}
});
eventBus.dispatchEvent(dataReadyEvent);
} }
function populateColumnSelect(columns) {
/***********************************************************
* 2. E-Mail Contents - Allow inserting placeholders from the available column
*
* After selecting the file and sheet, a combobox with all available placeholders is provided
* Next to is is a "Insert" button, which inserts the placeholder of the form {%fieldname%}
* into the mail at the current position. For Subject, To/CC/BCC we cannot automatically insert,
* but at least provide a copy button to copy the placeholder into the clipboard (TODO!)
*
***********************************************************/
const placeholderSelect = document.getElementById("placeholderSelect");
const placeholderInsertButton = document.getElementById("placeholderInsertButton");
const placeholderCopyLabel = document.getElementById("placeholderCopyLabel");
// Attach event listener to the previous controls / eventbus
placeholderSelect.addEventListener("change", enablePlaceholderInsert);
placeholderInsertButton.addEventListener("click", insertPlaceholder);
placeholderCopyLabel.addEventListener("click", copyPlaceholderToClipboard)
eventBus.addEventListener("dataReady", populatePlaceholderSelect);
function populatePlaceholderSelect(event) {
const columns = event.detail.columns;
placeholderSelect.innerHTML = ""; placeholderSelect.innerHTML = "";
columns.forEach((column, index) => {
const option = document.createElement("option");
option.value = index;
option.textContent = column || `Column ${index + 1}`;
placeholderSelect.appendChild(option);
});
placeholderSelect.disabled = false;
placeholderSelect.dispatchEvent(new Event("change"));
}
function enablePlaceholderInsert(event) {
const placeholder = event.target.value;
placeholderInsertButton.disabled = !placeholder;
}
// Function to insert placeholder
async function insertPlaceholder() {
try {
// Get the selected field from the combo box
// const selectedField = placeholderSelect.value;
const selectedText = placeholderSelect.options[placeholderSelect.selectedIndex].text;
if (!selectedText) {
displayMessage("Please select a field before inserting.");
return;
}
const placeholder = `{%${selectedText}%}`;
// Use the Office.js API to get the current item
await Office.context.mailbox.item.body.getTypeAsync(async (result) => {
if (result.status === Office.AsyncResultStatus.Succeeded) {
const bodyType = result.value;
if (bodyType === Office.MailboxEnums.BodyType.Html) {
// Insert HTML placeholder into the body
await Office.context.mailbox.item.body.setSelectedDataAsync(placeholder, { coercionType: Office.CoercionType.Html } );
} else if (bodyType === Office.MailboxEnums.BodyType.Text) {
// Insert plain text placeholder into the body
await Office.context.mailbox.item.body.setSelectedDataAsync(placeholder, { coercionType: Office.CoercionType.Text } );
}
}
});
} catch (error) {
console.error("Error inserting placeholder:", error);
displayMessage("There was an error inserting the placeholder. Please try again.");
}
}
/*
* Function to insert the placeholder into other fields
* -> TODO: Add a dropdown to the "Insert" button to append to Subject/To/CC/BCC alternatively!
*/
async function insertIntoField(field, placeholder) {
try {
const currentValue = await Office.context.mailbox.item[field].getAsync();
if (currentValue.status === Office.AsyncResultStatus.Succeeded) {
const updatedValue = currentValue.value + placeholder;
await Office.context.mailbox.item[field].setAsync(updatedValue);
}
} catch (error) {
console.error(`Error inserting into ${field}:`, error);
}
}
// Example usage:
// insertIntoField('subject', '{%fieldname%}');
// insertIntoField('to', '{%fieldname%}');
function copyPlaceholderToClipboard() {
//const selectedField = placeholderSelect.value;
const selectedText = placeholderSelect.options[placeholderSelect.selectedIndex].text;
if (!selectedText) {
displayMessage("Please select a field before copying.");
return;
}
const placeholder = `{%${selectedText}%}`;
navigator.clipboard.writeText(placeholder).then(() => {
displayMessage("Copied to clipboard!");
})
.catch((err) => {
console.error("Error copying to clipboard:", err);
});
}
/***********************************************************
* 3. Attachments handling
*
* Provide a combobox to select the column for individual attachments,
* optionally appends a file extension (e.g. "pdf") and select
* a folder where the attachments are stored.
*
***********************************************************/
const attachmentColumnSelect = document.getElementById("attachmentColumnSelect");
const appendExtensionCheckbox = document.getElementById("appendExtensionCheckbox");
const fileExtensionInput = document.getElementById("fileExtensionInput");
const attachmentFolderButton = document.getElementById('selectAttachmentFolderButton');
const attachmentFolderSpan = document.getElementById('selectedAttachmentFolder');
eventBus.addEventListener("dataReady", populateAttachmentColumnSelect);
attachmentColumnSelect.addEventListener("change", handleAttachmentColumnSelection);
function populateAttachmentColumnSelect(event) {
const columns = event.detail.columns;
attachmentColumnSelect.innerHTML = '<option value="">Select Column</option>'; attachmentColumnSelect.innerHTML = '<option value="">Select Column</option>';
columns.forEach((column, index) => { columns.forEach((column, index) => {
const option = document.createElement("option"); const option = document.createElement("option");
option.value = index; option.value = index;
option.textContent = column || `Spalte ${index + 1}`; option.textContent = column || `Column ${index + 1}`;
attachmentColumnSelect.appendChild(option); attachmentColumnSelect.appendChild(option);
// Insert the same option into the placeholder combobox:
const option2 = option.cloneNode(true);
placeholderSelect.appendChild(option2);
}); });
attachmentColumnSelect.disabled = false; attachmentColumnSelect.disabled = false;
placeholderSelect.disabled = false;
} }
appendExtensionCheckbox.addEventListener("change", () => {
fileExtensionInput.disabled = !appendExtensionCheckbox.checked;
});
attachmentFolderButton.addEventListener("click", async () => {
try {
// TODO: This does not seem to work with the web-application of Outlook!
const directoryHandle = await window.showDirectoryPicker();
attachmentFolderSpan.textContent = `Selected folder: ${directoryHandle.name}`;
console.log("Selected folder:", directoryHandle);
} catch (err) {
console.error("Folder selection cancelled or failed", err);
}
});
function handleAttachmentColumnSelection() { function handleAttachmentColumnSelection() {
if (attachmentColumnSelect.value !== "") { if (attachmentColumnSelect.value !== "") {
appendExtensionCheckbox.disabled = false; appendExtensionCheckbox.disabled = false;
...@@ -145,7 +317,22 @@ function handleAttachmentColumnSelection() { ...@@ -145,7 +317,22 @@ function handleAttachmentColumnSelection() {
} }
} }
function displayData(data) {
/***********************************************************
* 4. Sanity Checks, general data handling and review
*
*
*
***********************************************************/
eventBus.addEventListener("dataReady", displayData);
function displayData(event) {
const data = event.detail.data;
const previewDiv = document.getElementById("dataPreview"); const previewDiv = document.getElementById("dataPreview");
previewDiv.innerHTML = ""; previewDiv.innerHTML = "";
...@@ -168,6 +355,17 @@ function displayData(data) { ...@@ -168,6 +355,17 @@ function displayData(data) {
previewDiv.appendChild(table); previewDiv.appendChild(table);
} }
/***********************************************************
* 5. Mail sending
*
*
*
***********************************************************/
function replacePlaceholders(template, row) { function replacePlaceholders(template, row) {
return template.replace(/{%([^%]+)%}/g, (match, columnName) => { return template.replace(/{%([^%]+)%}/g, (match, columnName) => {
return row[columnName] || ""; // Replace with column value or empty string return row[columnName] || ""; // Replace with column value or empty string
...@@ -183,7 +381,7 @@ async function addAttachment(mailItem, filePath, fileName) { ...@@ -183,7 +381,7 @@ async function addAttachment(mailItem, filePath, fileName) {
} }
} }
async function sendSerialMails(data, filePath) { async function sendSerialMails(data, filePath, sendOrSave) {
const item = Office.context.mailbox.item; const item = Office.context.mailbox.item;
for (const row of data) { for (const row of data) {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment