Skip to content

Commit

Permalink
Move over to multiple actions (#1)
Browse files Browse the repository at this point in the history
* First steps to pulling out functionality into seperate files

* Adding eslint and prettier

* Adding Stop Timer Action

* Tidy up

* Updating readme to represent changes

Co-authored-by: Ben Hughes <bhughes.me@gmail.com>
  • Loading branch information
benhughes and Ben Hughes committed Jan 1, 2021
1 parent ac3a8a9 commit f258dd5
Show file tree
Hide file tree
Showing 18 changed files with 2,154 additions and 221 deletions.
15 changes: 15 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module.exports = {
extends: ['airbnb', 'prettier'],
rules: {
'no-param-reassign': 'off',
'no-console': 'off',
},
globals: {
PlugIn: 'readonly',
Version: 'readonly',
Data: 'readonly',
flattenedTags: 'readonly',
Alert: 'readonly',
Tag: 'readonly',
},
};
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules
.vscode
5 changes: 5 additions & 0 deletions .prettierrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// prettier.config.js or .prettierrc.js
module.exports = {
trailingComma: "all",
singleQuote: true,
};
191 changes: 191 additions & 0 deletions OmniToggl.omnifocusjs/Resources/common.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
/* eslint-disable no-bitwise, no-plusplus */

(() => {
// Replace the string below with your API Token found here: https://track.toggl.com/profile
const TOGGL_AUTH_TOKEN = 'REPLACE_ME';
// Name of the tag we use to assign what you're working on
// (this makes it easier to reset the changes made to the name)
const TRACKING_TAG_NAME = 'working-on';
// this is the name prefix so it's easy to identify what you're working on.
// Replace this if you would like something different
const TRACKING_NAME_PREFIX = '🎯';

// the following is a pollyfill for base64 taken from https://github.com/MaxArt2501/base64-js/blob/master/base64.js
function btoa(stringParam) {
const b64 =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
const string = String(stringParam);
let result = '';
const rest = string.length % 3; // To determine the final padding

for (let i = 0; i < string.length; ) {
const a = string.charCodeAt(i++);
const b = string.charCodeAt(i++);
const c = string.charCodeAt(i++);
if (a > 255 || b > 255 || c > 255) {
throw new Error(
"Failed to execute 'btoa' on 'Window': The string to be encoded contains characters outside of the Latin1 range.",
);
}

// eslint-disable-next-line no-bitwise
const bitmap = (a << 16) | (b << 8) | c;
result +=
b64.charAt((bitmap >> 18) & 63) +
b64.charAt((bitmap >> 12) & 63) +
b64.charAt((bitmap >> 6) & 63) +
b64.charAt(bitmap & 63);
}
// If there's need of padding, replace the last 'A's with equal signs
return rest ? result.slice(0, rest - 3) + '==='.substring(rest) : result;
}

const buildErrorObject = (r) => ({
statusCode: r.statusCode,
data: r.bodyString,
});

const AuthHeader = `Basic ${btoa(`${TOGGL_AUTH_TOKEN}:api_token`)}`;

const dependencyLibrary = new PlugIn.Library(new Version('1.0'));

dependencyLibrary.startTogglTimer = async function startTogglTimer(
timeEntry,
) {
const fetchRequest = new URL.FetchRequest();
fetchRequest.bodyData = Data.fromString(
JSON.stringify({
time_entry: timeEntry,
}),
);
fetchRequest.method = 'POST';
fetchRequest.headers = {
Authorization: AuthHeader,
'Content-Type': 'application/json',
};
fetchRequest.url = URL.fromString(
'https://www.toggl.com/api/v8/time_entries/start',
);
const r = await fetchRequest.fetch();

if (r.statusCode !== 200) {
throw buildErrorObject(r);
}

return JSON.parse(r.bodyString).data;
};

dependencyLibrary.getCurrentTogglTimer = async function getCurrentTogglTimer() {
const fetchRequest = new URL.FetchRequest();

fetchRequest.method = 'GET';
fetchRequest.headers = {
Authorization: AuthHeader,
'Content-Type': 'application/json',
};
fetchRequest.url = URL.fromString(
'https://www.toggl.com/api/v8/time_entries/current',
);
const r = await fetchRequest.fetch();

if (r.statusCode !== 200) {
throw buildErrorObject(r);
}

return JSON.parse(r.bodyString).data;
};

dependencyLibrary.stopTogglTimer = async function stopTogglTimer(id) {
const fetchRequest = new URL.FetchRequest();

fetchRequest.method = 'PUT';
fetchRequest.headers = {
Authorization: AuthHeader,
'Content-Type': 'application/json',
};
fetchRequest.url = URL.fromString(
`https://www.toggl.com/api/v8/time_entries/${id}/stop`,
);
const r = await fetchRequest.fetch();

if (r.statusCode !== 200) {
throw buildErrorObject(r);
}

return JSON.parse(r.bodyString).data;
};

dependencyLibrary.createTogglProject = async function createTogglProject(
name,
) {
const fetchRequest = new URL.FetchRequest();
fetchRequest.bodyData = Data.fromString(
JSON.stringify({ project: { name } }),
);
fetchRequest.method = 'POST';
fetchRequest.headers = {
Authorization: AuthHeader,
'Content-Type': 'application/json',
};
fetchRequest.url = URL.fromString(
'https://api.track.toggl.com/api/v8/projects',
);
const r = await fetchRequest.fetch();

if (r.statusCode !== 200) {
throw buildErrorObject(r);
}

return JSON.parse(r.bodyString).data;
};

dependencyLibrary.getTogglProjects = async function getTogglProjects() {
const fetchRequest = new URL.FetchRequest();
fetchRequest.method = 'GET';
fetchRequest.headers = {
Authorization: AuthHeader,
'Content-Type': 'application/json',
};
fetchRequest.url = URL.fromString(
`https://api.track.toggl.com/api/v8/me?with_related_data=true`,
);
const r = await fetchRequest.fetch();

if (r.statusCode !== 200) {
throw buildErrorObject(r);
}

return JSON.parse(r.bodyString).data.projects;
};

dependencyLibrary.log = async function log(message, title = 'Log') {
const a = new Alert(title, message);
a.addOption('OK');
await a.show();
};

const config = {
TOGGL_AUTH_TOKEN,
TRACKING_TAG_NAME,
TRACKING_NAME_PREFIX,
};

dependencyLibrary.resetTasks = () => {
let trackingTag = flattenedTags.find((t) => t.name === TRACKING_TAG_NAME);

if (!trackingTag) {
trackingTag = new Tag(TRACKING_TAG_NAME);
}

trackingTag.tasks.forEach((task) => {
if (task.name.startsWith(TRACKING_NAME_PREFIX)) {
task.name = task.name.replace(TRACKING_NAME_PREFIX, '');
}
task.removeTag(trackingTag);
});
};

dependencyLibrary.config = config;

return dependencyLibrary;
})();
2 changes: 2 additions & 0 deletions OmniToggl.omnifocusjs/Resources/en.lproj/manifest.strings
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
"com.github.benhughes.OmniToggl" = "OmniToggl";
"com.github.benhughes.OmniToggl-dev" = "OmniToggl Dev";
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
"label" = "Start Toggl Timer";
"shortLable" = "Start Timer";
"mediumLable" = "Start Toggl Timer";
"longLabel" = "Start Toggl Timer";
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
"label" = "Stop Toggl Timer";
"shortLable" = "Stop Timer";
"mediumLable" = "Stop Toggl Timer";
"longLabel" = "Stop Toggl Timer";
87 changes: 87 additions & 0 deletions OmniToggl.omnifocusjs/Resources/startTogglTimer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
(() => {
// Main action
const action = new PlugIn.Action(async function startTogglTimerAction(
selection,
) {
const {
config: { TRACKING_TAG_NAME, TRACKING_NAME_PREFIX },
startTogglTimer,
createTogglProject,
getTogglProjects,
resetTasks,
log,
} = this.common;

const trackingTag = flattenedTags.find((t) => t.name === TRACKING_TAG_NAME);

try {
resetTasks();

let projects = [];

try {
projects = await getTogglProjects();
} catch (e) {
await log(
'An error occurred getting projects',
'See console for more info',
);
console.log(e);
}

const task = selection.tasks[0];
const projectName = task.containingProject && task.containingProject.name;

const toggleProject = projects.find(
(p) => p.name.trim().toLowerCase() === projectName.trim().toLowerCase(),
);

const taskName = task.name;
let pid;
if (!projectName) {
pid = null;
} else if (!toggleProject) {
console.log(`project not found creating new ${projectName} project`);
try {
const r = await createTogglProject(projectName);
console.log(`project created id: ${r.id}`);
pid = r.id;
} catch (e) {
console.log(`Error creating project ${projectName}`);
console.log(JSON.stringify(e, null, 2));
}
} else {
pid = toggleProject.id;
}
console.log('pid is: ', pid);

const taskTags = task.tags.map((t) => t.name);

try {
const r = await startTogglTimer({
description: taskName,
created_with: 'omnifocus',
tags: taskTags,
pid,
});
task.name = TRACKING_NAME_PREFIX + task.name;
task.addTag(trackingTag);
console.log('Timer started successfully', JSON.stringify(r));
} catch (e) {
await log('An error occurred', 'See console for more info');
console.log(JSON.stringify(e, null, 2));
}
} catch (e) {
await log('An error occurred', 'See console for more info');
console.log(e);
console.log(JSON.stringify(e, null, 2));
}
});

action.validate = function startTogglTimerValidate(selection) {
// selection options: tasks, projects, folders, tags
return selection.tasks.length === 1;
};

return action;
})();
29 changes: 29 additions & 0 deletions OmniToggl.omnifocusjs/Resources/stopTogglTimer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
(() => {
// Main action
const action = new PlugIn.Action(async function stopTogglTimerAction() {
const {
getCurrentTogglTimer,
stopTogglTimer,
resetTasks,
log,
} = this.common;

try {
const currentTimer = await getCurrentTogglTimer();
if (currentTimer) {
await stopTogglTimer(currentTimer.id);
}
resetTasks();
} catch (e) {
log('Please try again later', 'Error stopping current task');
console.log(JSON.stringify(e, null, 2));
}
});

action.validate = function startTogglTimerValidate() {
// selection options: tasks, projects, folders, tags
return true;
};

return action;
})();
20 changes: 20 additions & 0 deletions OmniToggl.omnifocusjs/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"defaultLocale": "en",
"author": "Ben Hughes",
"identifier": "com.github.benhughes.OmniToggl",
"version": "1.1",
"description": "A collection of actions for starting Toggl from OmniFocus. See: https://github.com/benhughes/OmniToggl",
"actions": [
{
"identifier": "startTogglTimer"
},
{
"identifier": "stopTogglTimer"
}
],
"libraries": [
{
"identifier": "common"
}
]
}
Loading

0 comments on commit f258dd5

Please sign in to comment.