mirror of
https://github.com/davidingerslev/outlook-meeting-notes.git
synced 2026-07-22 12:20:25 +00:00
Compare commits
20 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91aa682979 | ||
|
|
9aade20454 | ||
|
|
16bb41eba6 | ||
|
|
69ab0a4b89 | ||
|
|
00735f620e | ||
|
|
71b4f40d4e | ||
|
|
5030c9bb16 | ||
|
|
9067271e96 | ||
|
|
4e9073e012 | ||
|
|
5ac713098c | ||
|
|
a8b7716030 | ||
|
|
8ced7ef462 | ||
|
|
e9a0e8d323 | ||
|
|
e88ee9f5aa | ||
|
|
e7f3295e60 | ||
|
|
8b0c323c8c | ||
|
|
31af2eed70 | ||
|
|
dfc24f575f | ||
|
|
1dc105b0b2 | ||
|
|
e24bc362b1 |
9 changed files with 1065 additions and 948 deletions
35
.github/workflows/release.yml
vendored
Normal file
35
.github/workflows/release.yml
vendored
Normal 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
|
||||
4
LICENSE
4
LICENSE
|
|
@ -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.
|
||||
|
|
|
|||
26
README.md
26
README.md
|
|
@ -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
137
main.ts
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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
1765
package-lock.json
generated
File diff suppressed because it is too large
Load diff
22
package.json
22
package.json
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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": [
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
Loading…
Reference in a new issue