Compare commits

..

20 commits

Author SHA1 Message Date
David Ingerslev
91aa682979 0.2.1 2025-04-02 09:40:24 +01:00
David Ingerslev
9aade20454 Added note about recurring meetings/appointments 2025-04-02 09:40:09 +01:00
David Ingerslev
16bb41eba6 Added documentation for new helper field 2025-04-02 09:35:19 +01:00
David Ingerslev
69ab0a4b89 0.2.0 2025-04-02 09:26:14 +01:00
David Ingerslev
00735f620e Added helper field for current date/time, helper_currentDT 2025-04-02 09:25:19 +01:00
David Ingerslev
71b4f40d4e 0.1.6 2025-03-30 12:32:11 +01:00
David Ingerslev
5030c9bb16 0.1.5 2025-03-30 12:31:59 +01:00
David Ingerslev
9067271e96 Updating version 2025-03-30 12:31:50 +01:00
David Ingerslev
4e9073e012 Changed import of mustache module to match the import styles of other modules 2025-03-30 12:22:25 +01:00
David Ingerslev
5ac713098c 0.1.4 2025-03-12 06:55:52 +00:00
David Ingerslev
a8b7716030 Updated npm libaries to improve security after Github warning message 2025-03-12 06:54:15 +00:00
David Ingerslev
8ced7ef462 Removed old notice for users that is no longer helpful. 2025-03-12 06:40:18 +00:00
David Ingerslev
e9a0e8d323 0.1.3 2025-03-12 06:38:15 +00:00
David Ingerslev
e88ee9f5aa Improved notices for users 2025-03-12 06:30:22 +00:00
David Ingerslev
e7f3295e60 0.1.2 2025-03-12 06:26:42 +00:00
David Ingerslev
8b0c323c8c Fixed version numbers 2025-03-09 21:48:50 +00:00
David Ingerslev
31af2eed70 Removed the last bit of Sample Plugin code 2025-03-09 21:45:46 +00:00
David Ingerslev
dfc24f575f Add release workflow 2025-03-09 21:33:37 +00:00
David Ingerslev
1dc105b0b2 Ready to release version 0.1.0 2025-03-09 21:24:20 +00:00
David Ingerslev
e24bc362b1 Fixed linting errors and removed funding URL 2025-03-09 21:13:08 +00:00
9 changed files with 1065 additions and 948 deletions

35
.github/workflows/release.yml vendored Normal file
View file

@ -0,0 +1,35 @@
name: Release Obsidian plugin
on:
push:
tags:
- "*"
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v3
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: "18.x"
- name: Build plugin
run: |
npm install
npm run build
- name: Create release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
tag="${GITHUB_REF#refs/tags/}"
gh release create "$tag" \
--title="$tag" \
--draft \
main.js manifest.json styles.css

View file

@ -1,5 +1,5 @@
Copyright (C) 2020-2025 by Dynalist Inc.
Copyright (C) 2025 David Ingerslev
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

View file

@ -12,7 +12,14 @@ By processing .msg files, the plugin does not depend on having to run code withi
Outlook or on Microsoft 365 administrators authorising an app to connect to the
Microsoft Graph API.
It relies on the wonderful [msgreader](https://github.com/HiraokaHyperTools/msgreader),
Note about recurring meetings or appointments: Microsoft Outlook does not include
any fields that indicate which of the recurring appointments has been dragged-and-dropped,
so it is not possible to differentiate between them. This means that the start date/time
of the first appointment in the recurring series will be used by default. You may wish
to change the filename template to include the current date/time instead (or as well)
using the helper field [helper_currentDT](#helper_currentDT).
The plugin relies on the wonderful [msgreader](https://github.com/HiraokaHyperTools/msgreader),
[mustache.js](https://github.com/janl/mustache.js), and
[mustache-validator](https://github.com/eliasm307/mustache-validator) libraries.
@ -69,8 +76,8 @@ Note - the invalid characters are: `/` `*` `"` `\` `<` `>` `:` `|` `?`
The default template can be customised, or you can write a new
template using mustache syntax (see [the manual](https://mustache.github.io/mustache.5.html)).
All .msg [fields](https://hiraokahypertools.github.io/msgreader/typedoc/interfaces/MsgReader.FieldsData.html)
can be used in a template, and there are also some additional helper functions you can
use to format fields.
can be used in a template, and there are also some additional helper fields and
functions you can use to format fields.
### Default template
The default template for notes is:
@ -89,6 +96,19 @@ meeting-invite: {{body}}
---
```
### Helper fields for templates
There is currently only one helper field available:
#### helper_currentDT
The date and time at which the meeting was dragged-and-dropped onto the icon,
in ISO format (like *2025-04-02T09:31:12+01:00*).
You will probably want to use the [helper_dateFormat](#helper_dateFormat) function
to format it:
```
{{#helper_dateFormat}}{{helper_currentDT}}|YYYY-MM-DD HH.mm.ss{{/helper_dateFormat}}
```
### Helper functions for templates
To use a helper function in a template, include a section for the function, e.g.:
```

137
main.ts
View file

@ -1,7 +1,8 @@
import { App, displayTooltip, Editor, MarkdownView, Modal, Notice, Plugin, PluginSettingTab, Setting, moment } from 'obsidian';
import { App, displayTooltip, Editor, MarkdownView, Notice, Plugin, PluginSettingTab, Setting, TooltipPlacement, EventRef } from 'obsidian';
import MsgReader from '@kenjiuno/msgreader';
import proxyData from 'mustache-validator';
const Mustache = require('mustache');
import Mustache from 'mustache';
import moment from 'moment';
const OutlookMeetingNotesDefaultFilenamePattern =
'{{#helper_dateFormat}}{{apptStartWhole}}|YYYY-MM-DD HH.mm{{/helper_dateFormat}} {{subject}}';
@ -39,24 +40,27 @@ export default class OutlookMeetingNotes extends Plugin {
//TODO: Add functionality to export meeting notes nicely
async createMeetingNote(msgPromise: Promise<MsgReader>) {
const msg = await msgPromise;
async createMeetingNote(msg: MsgReader) {
try {
const { vault } = this.app;
// Get the file data from MsgReader
const fileData = msg.getFileData();
const origFileData = msg.getFileData();
// Check if we got a suitable meeting
if (fileData.dataType != 'msg') {
if (origFileData.dataType != 'msg') {
throw new TypeError('Outlook Meeting Notes cannot process the file. '
+ 'MsgReader did not parse the file as valid msg format.');
} else if (fileData.messageClass != 'IPM.Appointment') {
} else if (origFileData.messageClass != 'IPM.Appointment') {
throw new TypeError('Outlook Meeting Notes cannot process the file. '
+ 'It is a valid msg file but not an appointment or meeting.');
}
this.addHelperFunctions(fileData);
this.addHelperFunctions(origFileData);
// Add helper field for the current date and time
let fileData = origFileData as any;
fileData.helper_currentDT = moment().format();
let folderPath = this.settings.notesFolder;
if (folderPath == '') { folderPath = '/'; }
@ -76,7 +80,7 @@ export default class OutlookMeetingNotes extends Plugin {
let meetingNoteFile = vault.getFileByPath(filePath);
if (meetingNoteFile) {
// File already exists
// TODO: send a message to the user
new Notice(meetingNoteFile.basename + ' already exists: opening it');
}
else {
if (vault.getFolderByPath(newFolderPath) == null) {
@ -86,15 +90,16 @@ export default class OutlookMeetingNotes extends Plugin {
this.settings.notesTemplate,
fileData);
meetingNoteFile = await vault.create(filePath, mustacheOutput);
new Notice('New file: ' + meetingNoteFile.basename);
new Notice('New file created: ' + meetingNoteFile.basename);
}
const openInNewTab = false;
this.app.workspace.getLeaf(openInNewTab).openFile(meetingNoteFile);
// @ts-ignore: Property 'internalPlugins' does not exist on type 'App'.
const fe = this.app.internalPlugins.getEnabledPluginById("file-explorer");
if (fe) { fe.revealInFolder(meetingNoteFile); }
} catch (ee: unknown) {
// TODO: Handle errors reasonably -- differently between msg missing elements and errors creating file
if (ee instanceof Error) { new Notice(ee.name + ':\n' + ee.message); }
if (ee instanceof Error) { new Notice('Error (' + ee.name + '):\n' + ee.message); }
throw ee;
}
}
@ -113,7 +118,6 @@ export default class OutlookMeetingNotes extends Plugin {
else {
// One file was dropped, hand it over to createMeetingNote
const droppedFile = droppedFiles[0];
new Notice('Dropped: ' + droppedFile.name);
const fr = new FileReader();
fr.onload = () => {
@ -134,6 +138,8 @@ export default class OutlookMeetingNotes extends Plugin {
}
}
private ribbonIconEl: HTMLElement;
async onload() {
await this.loadSettings();
@ -141,48 +147,47 @@ export default class OutlookMeetingNotes extends Plugin {
// This creates an icon in the left ribbon.
// Create an icon that does nothing when clicked, as the effect is from
const ribbonIconEl = this.addRibbonIcon('calendar-clock', tooltipMessage, () => { });
this.ribbonIconEl = this.addRibbonIcon('calendar-clock', tooltipMessage, () => { });
// These respond to something being dragged over the ribbon icon and dropped onto it
this.registerEvent(ribbonIconEl.on('dragenter', 'div', () => {
// Style for icon-dragover is defined in the CSS file.
ribbonIconEl.toggleClass('is-being-dragged-over', true);
this.ribbonIconEl.addEventListener('dragenter', () => {
// Style for is-being-dragged-over is defined in the CSS file.
this.ribbonIconEl.toggleClass('is-being-dragged-over', true);
// Display a tooltip.
const ttPosition = ribbonIconEl.getAttribute('data-tooltip-position');
const ttDelay = ribbonIconEl.getAttribute('data-tooltip-delay');
const ttPosition = this.ribbonIconEl.getAttribute('data-tooltip-position') as TooltipPlacement;
const ttDelay = this.ribbonIconEl.getAttribute('data-tooltip-delay');
if (ttPosition != null && ttDelay != null) {
displayTooltip(ribbonIconEl, tooltipMessage, { placement: ttPosition, delay: Number(ttDelay) });
displayTooltip(this.ribbonIconEl, tooltipMessage, { placement: ttPosition, delay: Number(ttDelay) });
} else {
displayTooltip(ribbonIconEl, tooltipMessage);
displayTooltip(this.ribbonIconEl, tooltipMessage);
}
}));
this.registerEvent(ribbonIconEl.on('dragleave', 'div', () => {
ribbonIconEl.toggleClass('is-being-dragged-over', false);
});
this.ribbonIconEl.addEventListener('dragleave', () => {
this.ribbonIconEl.toggleClass('is-being-dragged-over', false);
// Remove the tooltip when the user leaves the ribbon icon while still dragging.
const tooltip = document.getElementsByClassName('tooltip')[0]
if (tooltip) { tooltip.remove(); }
}));
this.registerEvent(ribbonIconEl.on('dragover', 'div', (dragevt) => {
});
this.ribbonIconEl.addEventListener('dragover', (dragevt) => {
// User is dragging something over the icon
// Prevent the default behaviour of not allowing the drop event
dragevt.preventDefault();
// The dropEffect changes the cursor. Options are 'none', 'move', 'copy', and 'link'.
if (dragevt.dataTransfer != null) { dragevt.dataTransfer.dropEffect = 'copy'; }
}));
this.registerEvent(ribbonIconEl.on('drop', 'div', (dropevt) => {
});
this.ribbonIconEl.addEventListener('drop', (dropevt) => {
// User has dropped something on the icon
// If the user drops something, dragleave doesn't get called.
ribbonIconEl.toggleClass('is-being-dragged-over', false);
this.ribbonIconEl.toggleClass('is-being-dragged-over', false);
// Prevent the default behaviour because we're handling it.
dropevt.preventDefault();
// Call the custom function defined above.
this.handleDropEvent(dropevt);
}));
ribbonIconEl.addClass('outlook-meeting-notes-icon');
});
this.ribbonIconEl.addClass('outlook-meeting-notes-icon');
// This adds a status bar item to the bottom of the app. Does not work on mobile apps.
// const statusBarItemEl = this.addStatusBarItem();
@ -190,58 +195,12 @@ export default class OutlookMeetingNotes extends Plugin {
//TODO: Add commands to create meeting notes.
// This adds a simple command that can be triggered anywhere
this.addCommand({
id: 'open-sample-modal-simple',
name: 'Open sample modal (simple)',
callback: () => {
new OutlookMeetingNotesModal(this.app).open();
}
});
// This adds an editor command that can perform some operation on the current editor instance
this.addCommand({
id: 'sample-editor-command',
name: 'OutlookMeetingNotes editor command',
editorCallback: (editor: Editor, view: MarkdownView) => {
console.log(editor.getSelection());
editor.replaceSelection('OutlookMeetingNotes Editor Command');
}
});
// This adds a complex command that can check whether the current state of the app allows execution of the command
this.addCommand({
id: 'open-sample-modal-complex',
name: 'Open sample modal (complex)',
checkCallback: (checking: boolean) => {
// Conditions to check
const markdownView = this.app.workspace.getActiveViewOfType(MarkdownView);
if (markdownView) {
// If checking is true, we're simply 'checking' if the command can be run.
// If checking is false, then we want to actually perform the operation.
if (!checking) {
new OutlookMeetingNotesModal(this.app).open();
}
// This command will only show up in Command Palette when the check function returns true
return true;
}
}
});
// This adds a settings tab so the user can configure various aspects of the plugin
this.addSettingTab(new OutlookMeetingNotesSettingTab(this.app, this));
// If the plugin hooks up any global DOM events (on parts of the app that doesn't belong to this plugin)
// Using this function will automatically remove the event listener when this plugin is disabled.
// this.registerDomEvent(document, 'click', (evt: MouseEvent) => {
// console.log('click', evt);
// });
// When registering intervals, this function will automatically clear the interval when the plugin is disabled.
//this.registerInterval(window.setInterval(() => console.log('setInterval'), 5 * 60 * 1000));
}
onunload() {
//Don't need to remove event listeners because the icon will be gone.
}
async loadSettings() {
@ -266,8 +225,8 @@ export default class OutlookMeetingNotes extends Plugin {
}
}
};
for (let func in helperFunctions) {
let func: 'firstWord' | 'dateFormat';
for (func in helperFunctions) {
hash['helper_' + func] = helperFunctions[func];
}
// Add helper functions to all objects in arrays so that the helper
@ -276,7 +235,7 @@ export default class OutlookMeetingNotes extends Plugin {
if (hash[property] instanceof Array) {
for (let subproperty in hash[property]) {
if (hash[property][subproperty] instanceof Object) {
for (let func in helperFunctions) {
for (func in helperFunctions) {
hash[property][subproperty]['helper_' + func] = helperFunctions[func];
}
}
@ -345,22 +304,6 @@ export default class OutlookMeetingNotes extends Plugin {
}
class OutlookMeetingNotesModal extends Modal {
constructor(app: App) {
super(app);
}
onOpen() {
const { contentEl } = this;
contentEl.setText('Woah!');
}
onClose() {
const { contentEl } = this;
contentEl.empty();
}
}
class OutlookMeetingNotesSettingTab extends PluginSettingTab {
plugin: OutlookMeetingNotes;

View file

@ -1,11 +1,10 @@
{
"id": "outlook-meeting-notes",
"name": "Outlook Meeting Notes",
"version": "0.0.2",
"minAppVersion": "0.15.0",
"version": "0.2.1",
"minAppVersion": "1.8.9",
"description": "Creates meeting notes for Outlook appointments and meetings.",
"author": "David Ingerslev",
"authorUrl": "https://github.com/davidingerslev",
"fundingUrl": "https://github.com/davidingerslev",
"isDesktopOnly": false
}
}

1765
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
{
"name": "obsidian-sample-plugin",
"version": "1.0.0",
"description": "This is a sample plugin for Obsidian (https://obsidian.md)",
"name": "outlook-meeting-notes",
"version": "0.2.1",
"description": "Creates meeting notes for Outlook appointments and meetings.",
"main": "main.js",
"scripts": {
"dev": "node esbuild.config.mjs",
@ -12,14 +12,16 @@
"author": "",
"license": "MIT",
"devDependencies": {
"@types/node": "^16.11.6",
"@typescript-eslint/eslint-plugin": "5.29.0",
"@typescript-eslint/parser": "5.29.0",
"builtin-modules": "3.3.0",
"esbuild": "0.17.3",
"@types/moment": "^2.11.29",
"@types/mustache": "^4.2.5",
"@types/node": "^22.13.10",
"@typescript-eslint/eslint-plugin": "8.26.1",
"@typescript-eslint/parser": "8.26.1",
"builtin-modules": "5.0.0",
"esbuild": "0.25.1",
"obsidian": "latest",
"tslib": "2.4.0",
"typescript": "4.7.4"
"tslib": "2.8.1",
"typescript": "5.8.2"
},
"dependencies": {
"@kenjiuno/msgreader": "^1.22.0",

View file

@ -4,10 +4,11 @@
"inlineSourceMap": true,
"inlineSources": true,
"module": "ESNext",
"target": "ES6",
"target": "es2021",
"allowJs": true,
"noImplicitAny": true,
"moduleResolution": "node",
"esModuleInterop": true,
"importHelpers": true,
"isolatedModules": true,
"strictNullChecks": true,
@ -15,7 +16,8 @@
"DOM",
"ES5",
"ES6",
"ES7"
"ES7",
"es2021"
]
},
"include": [

View file

@ -1,3 +1,10 @@
{
"1.0.0": "0.15.0"
}
"0.1.1": "0.15.0",
"0.1.2": "0.15.0",
"0.1.3": "0.15.0",
"0.1.4": "1.8.9",
"0.1.5": "1.8.9",
"0.1.6": "1.8.9",
"0.2.0": "1.8.9",
"0.2.1": "1.8.9"
}