From 84f58e7cdbd018426a825888bbf5b322c73dfea7 Mon Sep 17 00:00:00 2001 From: Reinhold Kainhofer <office@open-tools.net> Date: Mon, 6 Jan 2025 11:03:01 +0100 Subject: [PATCH] Some more word on the add-in. I'm still struggling with duplicate event handlers being called, functions exiting early because of this. For now, it appears that the workbook is not loaded because of this, so most things are broken for now. --- .eslintrc.json | 22 +- .prettierrc | 8 + package-lock.json | 2 +- package.json | 4 +- src/taskpane/taskpane.html | 16 +- src/taskpane/taskpane.js | 651 ++++++++++++++++++++++++++++--------- 6 files changed, 548 insertions(+), 155 deletions(-) create mode 100644 .prettierrc diff --git a/.eslintrc.json b/.eslintrc.json index e406c09..01faded 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,8 +1,24 @@ { "plugins": [ - "office-addins" + "office-addins", + "prettier" ], "extends": [ - "plugin:office-addins/recommended" - ] + "plugin:office-addins/recommended", + "plugin:prettier/recommended", + "esling:recommended" + ], + "env": { + "browser": true, + "node": true, + "es2021": true + }, + "globals": { + "Office": "readonly", // Declare Office as a global variable + "XLSX": "readonly" + }, + "rules": { + "no-multiple-empty-lines": "off", // Example: turn off the multiple empty lines rule + "prettier/prettier": "error" // Enforce Prettier formatting as ESLint errors + } } diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..7839c43 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,8 @@ +{ + "printWidth": 80, + "tabWidth": 2, + "semi": true, + "singleQuote": true, + "trailingComma": "all", + "endOfLine": "auto" +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index f250de9..730fcf8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "acorn": "^8.11.3", "babel-loader": "^9.1.3", "copy-webpack-plugin": "^12.0.2", - "eslint-plugin-office-addins": "^3.0.2", + "eslint-plugin-office-addins": "^3.0.3", "file-loader": "^6.2.0", "html-loader": "^5.0.0", "html-webpack-plugin": "^5.6.0", diff --git a/package.json b/package.json index eba23cc..79cb072 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "acorn": "^8.11.3", "babel-loader": "^9.1.3", "copy-webpack-plugin": "^12.0.2", - "eslint-plugin-office-addins": "^3.0.2", + "eslint-plugin-office-addins": "^3.0.3", "file-loader": "^6.2.0", "html-loader": "^5.0.0", "html-webpack-plugin": "^5.6.0", @@ -59,4 +59,4 @@ "last 2 versions", "ie 11" ] -} \ No newline at end of file +} diff --git a/src/taskpane/taskpane.html b/src/taskpane/taskpane.html index e2f2d4f..58e8afe 100644 --- a/src/taskpane/taskpane.html +++ b/src/taskpane/taskpane.html @@ -15,12 +15,19 @@ <p id="messageText"></p> </div> - <fieldset> + <!--fieldset> <legend>1. Select data source (Excel file):</legend> <input type="file" id="excelFileInput" accept=".xlsx, .xlsm, .xlsb, .xlts, .xltm, .xls" /> <div class="worksheet_select">Worksheet: <select id="worksheetSelect" disabled></select></div> + </fieldset--> + <fieldset> + <legend>1. Select data source (Excel file):</legend> + <button id="selectFileButton">Select File</button> + <span id="selectedFileName" style="margin-left: 10px; font-slant: italic;">No file selected</span> + <div class="worksheet_select"> + Worksheet: <select id="worksheetSelect" disabled></select> + </div> </fieldset> - <fieldset> <legend>2. E-Mail - Placeholder fields:</legend> @@ -50,9 +57,12 @@ </div> </fieldset> + <button id="sendNowButton" disabled>Send now</button> + <button id="sendLaterButton" disabled>Send later</button> + <div id="dataPreview"></div> - <script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script> + <script src="https://cdn.sheetjs.com/xlsx-latest/package/dist/xlsx.full.min.js"></script> <script src="https://appsforoffice.microsoft.com/lib/1/hosted/office.js"></script> <script src="taskpane.js"></script> </div> diff --git a/src/taskpane/taskpane.js b/src/taskpane/taskpane.js index 4603f55..4ecab95 100644 --- a/src/taskpane/taskpane.js +++ b/src/taskpane/taskpane.js @@ -2,10 +2,11 @@ * Copyright (c) 2025 Reinhold Kainhofer, Open Tools, office@open-tools.net */ + // Ensure the Office.js library is loaded the office context is fully initialized! Office.onReady(function (info) { // Office is ready - if (info.host === Office.HostType.Outlook) { +/* if (info.host === Office.HostType.Outlook) { // Now you can safely use Office.context.mailbox Office.context.mailbox.item.body.getTypeAsync((result) => { if (result.status === Office.AsyncResultStatus.Succeeded) { @@ -14,18 +15,30 @@ Office.onReady(function (info) { console.error("Failed to get body type:", result.error); } }); - } + }*/ }); + let workbook = null; +let worksheetData = []; // Holds the JSON data from the selected worksheet + +let excelFileHandle = null; +let attachmentsFolderHandle = null; + + + + // Create a global EventTarget instance const eventBus = new EventTarget(); + +/*** + * displayMessage: Warning/Errors in a temporary, non-blocking popup div + */ 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; @@ -40,67 +53,123 @@ function displayMessage(message) { 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 - }); +// Close button functionality +document.getElementById("closeMessageButton").addEventListener("click", function () { + const messageDiv = document.getElementById("messageDiv"); + messageDiv.classList.add("hide"); + setTimeout(() => { + messageDiv.classList.remove("show", "hide"); + }, 300); // Remove the show and hide classes after the fade-out animation +}); -} /*********************************************************** * 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); +document.getElementById("selectFileButton").addEventListener("click", async () => { + try { + // Open the file picker and allow selecting a single file +/* if (!('showOpenFilePicker' in window)) { + displayMessage("File picker API is not supported in this browser."); + return; + }*/ + const [handle] = await window.showOpenFilePicker({ + types: [ + { + description: "Spreadsheet Files", + accept: { + "application/vnd.ms-excel": [".xlsx", ".xlsm", ".xlsb", ".xls"], + "application/vnd.oasis.opendocument.spreadsheet": [".ods"], + "text/csv": [".csv"], + "text/plain": [".txt"], + }, + }, + ], + multiple: false, + }); + + const setSpreadsheetEvent = new CustomEvent("setSpreadsheetFile", { + detail: { + handle: handle, // Pass the file handle + }, + }); + eventBus.dispatchEvent(setSpreadsheetEvent); + } catch (err) { + console.error("File selection canceled or failed", err); + } +}); -/* + + + +/* * Read the selected file (Excel, csv) into memory */ -function handleFile(event) { - const file = event.target.files[0]; - if (!file) return; - - const reader = new FileReader(); - reader.onload = function (e) { - 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(); - - // Determine the file type and parse accordingly - if (fileType === "xlsb") { - workbook = XLSX.read(binary, { type: "binary" }); - } else { - workbook = XLSX.read(data, { type: "array" }); - } +eventBus.addEventListener("setSpreadsheetFile", async (event) => { + // Save the file handle for later access + const handle = event.detail.handle; + if (!handle) return; + + const permissionStatus = await handle.queryPermission(); + if (permissionStatus !== 'granted') { + // Prompt user to grant permission again + await handle.requestPermission(); + console.log('Permission is required for this file!'); + // TODO: exit if permission was not granted! + } + + excelFileHandle = handle; + saveHandles(); + + // Display the file name + document.getElementById("selectedFileName").textContent = handle.name; + + const file = await handle.getFile(); + const fileType = file.name.split('.').pop().toLowerCase(); + const arrayBuffer = await file.arrayBuffer(); + + if (fileType === 'csv' || fileType === 'txt') { + // Convert text content to a SheetJS workbook + const text = await file.text(); + workbook = XLSX.read(text, { type: 'string' }); + } else { + // For binary files (.xlsx, .ods) + workbook = XLSX.read(arrayBuffer, { type: 'array' }); + } + const sheetNames = workbook.SheetNames; + + // Populate the worksheet dropdown + populateWorksheetDropdown(sheetNames); +}); - populateWorksheetDropdown(); - }; - reader.readAsArrayBuffer(file); -} -/* +/* * Make the list of worksheets selectable in a combo box and auto-select the first one */ -function populateWorksheetDropdown() { +function populateWorksheetDropdown(sheetNames) { + if (!sheetNames || sheetNames.length === 0) { + worksheetSelect.disabled = true; + displayMessage("No worksheets found in the workbook.") + console.error("No worksheets found in the workbook."); + return; + } worksheetSelect.innerHTML = ""; worksheetSelect.disabled = false; - workbook.SheetNames.forEach((sheetName) => { + sheetNames.forEach((sheetName) => { const option = document.createElement("option"); option.value = sheetName; option.textContent = sheetName; @@ -108,27 +177,27 @@ function populateWorksheetDropdown() { }); // Automatically select the first worksheet and trigger the "change" event - if (worksheetSelect.options.length >= 1) { + if (worksheetSelect.options.length > 0) { worksheetSelect.selectedIndex = 0; - worksheetSelect.dispatchEvent(new Event("change")); } + worksheetSelect.dispatchEvent(new Event("change")); } function handleWorksheetSelection(event) { const sheetName = event.target.value; - if (!sheetName) return; - if (!workbook) return; // no file loaded yet! + if (!workbook || !sheetName) return;// no file loaded yet or no workseet selected! // Populate the placeholders combobox and the attachments column combo const worksheet = workbook.Sheets[sheetName]; const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 }); - const columns = jsonData[0] || []; + worksheetData = jsonData; + const columns = jsonData[0] || []; const dataReadyEvent = new CustomEvent("dataReady", { detail: { - data: jsonData, // Pass the loaded data as part of the event - columns: columns, // Pass the availabe column names - } + data: jsonData, // Pass the loaded data + columns: columns, // Pass the availabe column names + }, }); eventBus.dispatchEvent(dataReadyEvent); } @@ -138,12 +207,12 @@ function handleWorksheetSelection(event) { /*********************************************************** * 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"); @@ -153,18 +222,33 @@ 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) +placeholderCopyLabel.addEventListener("click", copyPlaceholderToClipboard); eventBus.addEventListener("dataReady", populatePlaceholderSelect); function populatePlaceholderSelect(event) { const columns = event.detail.columns; + const currentSelectedValue = placeholderSelect.value; placeholderSelect.innerHTML = ""; + columns.forEach((column, index) => { const option = document.createElement("option"); - option.value = index; + option.value = column; option.textContent = column || `Column ${index + 1}`; placeholderSelect.appendChild(option); }); + + // Check if the previously selected value exists in the new list + const matchingOption = Array.from(attachmentColumnSelect.options).find(option => + option.value === currentSelectedValue || option.textContent === currentSelectedValue + ); + + // If the selected value is found, restore it + if (matchingOption) { + attachmentColumnSelect.value = currentSelectedValue; + } else if (columns.length > 0) { + // If no match is found, default to the first option + attachmentColumnSelect.value = columns[0]; + } placeholderSelect.disabled = false; placeholderSelect.dispatchEvent(new Event("change")); } @@ -175,12 +259,10 @@ function enablePlaceholderInsert(event) { } - // 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) { @@ -191,16 +273,17 @@ async function insertPlaceholder() { const placeholder = `{%${selectedText}%}`; // Use the Office.js API to get the current item - await Office.context.mailbox.item.body.getTypeAsync(async (result) => { + Office.context.mailbox.item.body.getTypeAsync((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 } ); + return Office.context.mailbox.item.body.setSelectedDataAsync(placeholder, { + coercionType: Office.CoercionType.Html, + }); + } else { + return Office.context.mailbox.item.body.setSelectedDataAsync(placeholder, { + coercionType: Office.CoercionType.Text, + }); } } }); @@ -211,12 +294,12 @@ async function insertPlaceholder() { } -/* +/* * 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) { +/*async function insertIntoField(field, placeholder) { try { const currentValue = await Office.context.mailbox.item[field].getAsync(); if (currentValue.status === Office.AsyncResultStatus.Succeeded) { @@ -228,10 +311,10 @@ async function insertIntoField(field, placeholder) { } } - // Example usage: // insertIntoField('subject', '{%fieldname%}'); // insertIntoField('to', '{%fieldname%}'); +*/ @@ -245,12 +328,14 @@ function copyPlaceholderToClipboard() { } const placeholder = `{%${selectedText}%}`; - navigator.clipboard.writeText(placeholder).then(() => { - displayMessage("Copied to clipboard!"); + navigator.clipboard + .writeText(placeholder) + .then(() => { + displayMessage("Copied to clipboard!"); }) .catch((err) => { console.error("Error copying to clipboard:", err); - }); + }); } @@ -258,46 +343,73 @@ function copyPlaceholderToClipboard() { /*********************************************************** * 3. Attachments handling - * + * * Provide a combobox to select the column for individual attachments, - * optionally appends a file extension (e.g. "pdf") and select + * 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'); - +const attachmentFolderButton = document.getElementById("selectAttachmentFolderButton"); +const attachmentFolderSpan = document.getElementById("selectedAttachmentFolder"); eventBus.addEventListener("dataReady", populateAttachmentColumnSelect); attachmentColumnSelect.addEventListener("change", handleAttachmentColumnSelection); - function populateAttachmentColumnSelect(event) { + // (Re-)Populate the column selection combobox. -> Store the current selection and reinstate it after re-filling the list! const columns = event.detail.columns; - attachmentColumnSelect.innerHTML = '<option value="">Select Column</option>'; + // Store the currently selected value + const currentSelectedValue = attachmentColumnSelect.value; + + attachmentColumnSelect.innerHTML = '<option value="">No attachment (select column if needed)</option>'; columns.forEach((column, index) => { const option = document.createElement("option"); - option.value = index; + option.value = column; option.textContent = column || `Column ${index + 1}`; attachmentColumnSelect.appendChild(option); }); + + // Check if the previously selected value exists in the new list + const matchingOption = Array.from(attachmentColumnSelect.options).find(option => + option.value === currentSelectedValue || option.textContent === currentSelectedValue + ); + + // If the selected value is found, restore it + if (matchingOption) { + attachmentColumnSelect.value = currentSelectedValue; + } else if (columns.length > 0) { + // If no match is found, default to the first option + attachmentColumnSelect.value = columns[0]; + } attachmentColumnSelect.disabled = false; + attachmentColumnSelect.dispatchEvent(new Event("change")); } - 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}`; + // If the directory Picker is not supported, show a warning until we find a solution... TODO + if (typeof window.showDirectoryPicker === "undefined") { + console.warn("Directory picker API is not supported in this browser."); + displayMessage("Directory picker API is not supported in this browser."); + return; + } + + const directoryHandle = await window.showDirectoryPicker(); + const setAttachmentFolderEvent = new CustomEvent("setAttachmentsFolder", { + detail: { + handle: directoryHandle, // Pass the file handle + }, + }); + eventBus.dispatchEvent(setAttachmentFolderEvent); + console.log("Selected folder:", directoryHandle); } catch (err) { console.error("Folder selection cancelled or failed", err); @@ -305,6 +417,33 @@ attachmentFolderButton.addEventListener("click", async () => { }); +/* +* Set the selected folder handle as attachments folder: +* - store in global variable for mail sending +* - display name in label +* - store in persistent storage to survive reloads +*/ +eventBus.addEventListener("setAttachmentsFolder", async (event) => { + // Save the file handle for later access + const handle = event.detail.handle; + if (!handle) return; + + const permissionStatus = await handle.queryPermission(); + if (permissionStatus !== 'granted') { + // Prompt user to grant permission again + await handle.requestPermission(); + console.log('Permission is required for this file!'); + // TODO: exit if permission was not granted! + } + + attachmentsFolderHandle = handle; // Update the global variable + saveHandles(); + attachmentFolderSpan.textContent = `Selected folder: ${handle.name}`; +}); + + + + function handleAttachmentColumnSelection() { if (attachmentColumnSelect.value !== "") { @@ -323,9 +462,9 @@ function handleAttachmentColumnSelection() { /*********************************************************** * 4. Sanity Checks, general data handling and review - * - * - * + * + * + * ***********************************************************/ eventBus.addEventListener("dataReady", displayData); @@ -360,11 +499,45 @@ function displayData(event) { /*********************************************************** * 5. Mail sending - * - * - * + * + * + * ***********************************************************/ +const sendNowButton = document.getElementById("sendNowButton"); +const sendLaterButton = document.getElementById("sendLaterButton"); + + +attachmentColumnSelect.addEventListener("change", handleAttachmentColumnSelection); +eventBus.addEventListener("dataReady", async () => { + sendNowButton.disabled = false; + sendLaterButton.disabled = false; +}); + + + +sendNowButton.addEventListener("click", async () => { + try { + sendSerialMails(/*sendNow=*/ true); + } catch (err) { + console.error("Error sending mails: ", err); + } +}); + +sendLaterButton.addEventListener("click", async () => { + try { + sendSerialMails(/*sendNow=*/ false); + } catch (err) { + console.error("Error sending mails: ", err); + } +}); + + + + + + + function replacePlaceholders(template, row) { return template.replace(/{%([^%]+)%}/g, (match, columnName) => { @@ -372,82 +545,268 @@ function replacePlaceholders(template, row) { }); } -async function addAttachment(mailItem, filePath, fileName) { - const attachmentUrl = `${filePath}/${fileName}`; +async function addAttachment(mailItem, folderHandle, fileName) { try { - await mailItem.addFileAttachmentAsync(attachmentUrl, fileName); + // Step 1: Get the file handle from the directory and file name + const fileHandle = await folderHandle.getFileHandle(fileName); + + // Step 2: Request permission if necessary (query for permission) + const permissionStatus = await fileHandle.queryPermission(); + if (permissionStatus !== 'granted') { + // Request permission from the user if needed + await fileHandle.requestPermission(); + } + + // Step 3: Get the file data (Blob) from the file handle + const file = await fileHandle.getFile(); + const fileBlob = file; // You can use file as a Blob object + + // Step 4: Attach the file to the email + mailItem.attachments.addFileAttachment(fileBlob, file.name) + .then(() => { + console.log(`File "${file.name}" successfully attached to the email.`); + }) + .catch((error) => { + console.error('Error attaching file:', error); + }); } catch (error) { - console.error("Error adding attachment:", error); + console.error('Error accessing or attaching the file:', error); } } -async function sendSerialMails(data, filePath, sendOrSave) { + +async function sendSerialMails(sendNow = false) { + if (!worksheetData || worksheetData.length === 0) { + displayMessage("No data available to send emails. Please select a worksheet."); + return; + } + const item = Office.context.mailbox.item; + const attachmentColumn = attachmentColumnSelect.options[attachmentColumnSelect.selectedIndex].text; + const hasAttachmentsConfigured = attachmentColumn && attachmentsFolderHandle; + + let fileExtension = appendExtensionCheckbox.checked ? fileExtensionInput.value : ""; + if (fileExtension && !fileExtension.startsWith(".")) { + fileExtension = `.${fileExtension}`; + } + + for (const row of worksheetData) { + try { + // Function to get the recipient list as a string (if available) + async function getRecipientAsync(recipientField) { + return new Promise((resolve, reject) => { + recipientField.getAsync((result) => { + if (result.status === Office.AsyncResultStatus.Succeeded) { + // Extract the email addresses (or return empty string if none) + const emails = result.value.map((recipient) => recipient.emailAddress).join(", "); + resolve(emails); // Join multiple recipients with a comma + } else { + reject("Error retrieving recipient"); + } + }); + }); + } + + + // Fetch recipients asynchronously and replace placeholders + const to = await getRecipientAsync(item.to); + const cc = await getRecipientAsync(item.cc); + const bcc = await getRecipientAsync(item.bcc); + const subject = replacePlaceholders(String(item.subject), row); + + + const bodyResult = await item.body.getAsync(Office.CoercionType.Html); + const body = replacePlaceholders(bodyResult.value || "", row); + + // Create a copy of the email + const mailItem = await Office.context.mailbox.item.displayReplyAllFormAsync(); + + // Replace fields + const new_to = replacePlaceholders(to, row); + const new_cc = replacePlaceholders(cc, row); + const new_bcc = replacePlaceholders(bcc, row); + const new_subject = replacePlaceholders(item.subject, row); + if (to != new_to) + mailItem.to = new_to; + if (cc != new_cc) + mailItem.cc = new_cc; + if (bcc != new_bcc) + mailItem.bcc = new_bcc; + if (subject != new_subject) + mailItem.subject = new_subject; + mailItem.body.setAsync(body, { coercionType: Office.CoercionType.Html }); + + // Handle attachment only if configuration and data are available + if (hasAttachmentsConfigured && attachmentColumn) { + const attachmentFileName = row[attachmentColumn]; + if (attachmentFileName) { + let formattedFileName = attachmentFileName.trim(); + + // Ensure the file name has the correct extension + if (fileExtension && !formattedFileName.endsWith(fileExtension)) { + formattedFileName += fileExtension; + } + + try { + await addAttachment(mailItem, attachmentsFolderHandle, formattedFileName); + } catch (error) { + console.warn(`Skipping attachment for row: ${JSON.stringify(row)}, reason: ${error.message}`); + } + } + } + + // Save the mail as a draft or send immediately + if (sendNow) { + await mailItem.sendAsync(); + console.log("Mails sent immediately."); + } else { + await mailItem.saveAsync(); + console.log("Mails saved as draft."); + } - for (const row of data) { - const to = row["To"]; - const cc = row["CC"]; - const bcc = row["BCC"]; - const subject = replacePlaceholders(item.subject, row); - const body = replacePlaceholders(item.body.getAsync(Office.CoercionType.Html).value, row); - const attachmentFile = row["AttachmentFileName"]; - - // Create a copy of the email - const mailItem = await Office.context.mailbox.item.displayReplyAllFormAsync(); - - // Replace fields - mailItem.to = to; - mailItem.cc = cc; - mailItem.bcc = bcc; - mailItem.subject = subject; - mailItem.body.setAsync(body, { coercionType: Office.CoercionType.Html }); - - // Add attachment - if (attachmentFile) { - await addAttachment(mailItem, filePath, attachmentFile); + } catch (error) { + console.error("Error processing row:", JSON.stringify(row), "Error:", error); } + } + displayMessage("Mails sent!"); +} + - // Send the mail - await mailItem.sendAsync(); + + + +/*********************************************************** + * 6. Persisting form settings across sessions: Use Office.js Storage + * + * + * + ***********************************************************/ + +worksheetSelect.addEventListener("change", saveFormDataToRoaming); +placeholderSelect.addEventListener("change", saveFormDataToRoaming); +attachmentColumnSelect.addEventListener("change", saveFormDataToRoaming); +appendExtensionCheckbox.addEventListener("change", saveFormDataToRoaming); +fileExtensionInput.addEventListener("input", saveFormDataToRoaming); +attachmentFolderSpan.addEventListener("click", saveFormDataToRoaming); // TODO: This should be called AFTER the folder picker was used! + +function saveFormDataToRoaming() { + const formData = { + worksheetName: worksheetSelect.value, + placeholder: placeholderSelect.value, + attachmentColumn: attachmentColumnSelect.value, + appendExtension: appendExtensionCheckbox.checked, + fileExtension: fileExtensionInput.value, + }; + Office.context.roamingSettings.set('mailMergeFormData', formData); + Office.context.roamingSettings.saveAsync(); +} + +function loadFormDataFromRoaming() { + const formData = Office.context.roamingSettings.get('mailMergeFormData'); + if (formData) { + worksheetSelect.value = formData.worksheetName || ''; + placeholderSelect.value = formData.placeholder || ''; + attachmentColumnSelect.value = formData.attachmentColumn || ''; + appendExtensionCheckbox.checked = formData.appendExtension || false; + fileExtensionInput.value = formData.fileExtension || ''; + + handleAttachmentColumnSelection(); } } -async function saveSerialMailsAsDrafts(data, filePath) { - const item = Office.context.mailbox.item; - for (const row of data) { - const to = row["To"]; - const cc = row["CC"]; - const bcc = row["BCC"]; - const subject = replacePlaceholders(item.subject, row); - const body = replacePlaceholders(item.body.getAsync(Office.CoercionType.Html).value, row); - const attachmentFile = row["AttachmentFileName"]; - - // Create a new draft mail - const mailItem = await Office.context.mailbox.item.displayReplyAllFormAsync(); - - // Replace fields - mailItem.to = to; - mailItem.cc = cc; - mailItem.bcc = bcc; - mailItem.subject = subject; - mailItem.body.setAsync(body, { coercionType: Office.CoercionType.Html }); - - // Add attachment - if (attachmentFile) { - try { - await addAttachment(mailItem, filePath, attachmentFile); - } catch (error) { - console.warn(`Attachment not found for row: ${JSON.stringify(row)}`); +// Function to open or create the IndexedDB database +async function openIndexedDB() { + return new Promise((resolve, reject) => { + const request = indexedDB.open('fileHandlesDB', 1); // Open or create the database + + request.onupgradeneeded = (event) => { + const db = event.target.result; + // Create the object store if it doesn't exist + if (!db.objectStoreNames.contains('fileHandles')) { + db.createObjectStore('fileHandles'); } + }; + + request.onsuccess = (event) => { + resolve(event.target.result); // Return the database instance + }; + + request.onerror = (event) => { + reject('Error opening IndexedDB:', event.target.error); + }; + }); +} + + +// Save file/directory handles persistently +async function saveHandles() { + // Storing/loading handles to/from Office Storage API does not work: +/* const settings = { + excelFileHandle: excelFileHandle ? await excelFileHandle.queryPermission() : null, + attachmentsFolderHandle: attachmentsFolderHandle ? await attachmentsFolderHandle.queryPermission() : null, + }; + Office.context.roamingSettings.set("fileHandles", settings); + Office.context.roamingSettings.saveAsync(); + */ + + // Instead, try storing it to indexDB instead: + const db = await openIndexedDB(); + if (db) { + const transaction = db.transaction(['fileHandles'], 'readwrite'); + const store = transaction.objectStore('fileHandles'); + + const data = { + excelFileHandle: excelFileHandle, + attachmentsFolderHandle: attachmentsFolderHandle, + }; + store.put(data, 'fileHandles'); // Storing the metadata + await transaction.complete; + } +} + +// Load file/directory handles persistently +async function loadHandles() { +// const settings = Office.context.roamingSettings.get("fileHandles"); // Storing/loading handles to/from Office Storage API does not work! + const settings = await loadFromIndexedDB('fileHandles'); // Instead, try loading them from indexDB instead: + + if (settings) { + if (settings.excelFileHandle) { + const setSpreadsheetEvent = new CustomEvent("setSpreadsheetFile", { + detail: { handle: settings.excelFileHandle, }, + }); + eventBus.dispatchEvent(setSpreadsheetEvent); } - // Save the mail as a draft or send immediately - try { - await mailItem.saveAsync(); -// await mailItem.sendAsync(); - console.log("Mail saved as draft."); - } catch (error) { - console.error("Error saving draft:", error); + if (settings.attachmentsFolderHandle) { + const setAttachmentsFolderEvent = new CustomEvent("setAttachmentsFolder", { + detail: { handle: settings.attachmentsFolderHandle, }, + }); + eventBus.dispatchEvent(setAttachmentsFolderEvent); } } } + + +// Example pseudo-function for IndexedDB +async function loadFromIndexedDB(storeName) { + const db = await openIndexedDB(); + const transaction = db.transaction(storeName, 'readonly'); + const store = transaction.objectStore(storeName); + const request = store.get('fileHandles'); + const result = await request.onsuccess; + return result.target.result; +} + +Office.onReady(function (info) { + if (info.host === Office.HostType.Outlook) { + /*loadHandles().then(() => { + if (excelFileHandle) { + console.log("Excel file handle loaded successfully"); + } + if (attachmentsFolderHandle) { + console.log("Attachment folder handle loaded successfully"); + } + });*/ +// loadFormDataFromRoaming(); + } +}); -- GitLab