diff --git a/background.js b/background.js index b8a73f5..8485fed 100644 --- a/background.js +++ b/background.js @@ -1,12 +1,55 @@ -trektor.runtime.onMessage((msg) => { +trektor.runtime.onMessage.addListener((msg) => { switch (msg.action) { - case 'fetchJSON': - return fetchJSON(...msg.args); + case 'addTask': + return addTask(...msg.args); default: - return Promise.reject('unknown action'); + return Promise.reject(new Error('unknown action')); } }); -function fetchJSON(url, options = {}) { - return fetch(url, options).then((response) => response.json()); +async function addTask(cardId) { + const card = await trektor.trelloGateway.getCard(cardId); + + const taskPrefixes = card.labels + .map((label) => label.name.match(/(?<=#)[a-z0-9]+$/)?.[0]) + .filter((prefix) => prefix !== undefined); + + if (taskPrefixes.length === 0) { + throw new Error('Card has no valid project labels.'); + } + if (taskPrefixes.length > 1) { + throw new Error('Card has multiple project labels.'); + } + const taskPrefix = taskPrefixes[0]; + const taskName = `${taskPrefix}_${card.idShort}`; + const cardTaskName = card.name.match(/(?<=#)[a-z0-9]+_[0-9]+/)?.[0]; + + if (cardTaskName === undefined) { + await trektor.trelloGateway.updateCard(card.id, { + name: `${card.name} #${taskName}`, + }); + } else if (cardTaskName !== taskName) { + throw new Error('Card name includes invalid tracking task.'); + } + const workspaces = await trektor.togglGateway.getWorkspaces(); + + if (workspaces.length === 0) { + throw new Error('Could not find any toggl workspaces.'); + } + if (workspaces.length > 1) { + throw new Error('Found multiple toggl workspaces. Not sure how to deal with that...'); + } + const allProjects = await trektor.togglGateway.getProjects(workspaces[0].id); + const projects = allProjects.filter((project) => project.name.endsWith(`(${taskPrefix})`)); + + if (projects.length === 0) { + throw new Error('Could not find any matching toggl project.'); + } + if (projects.length > 1) { + throw new Error('Found multiple matching toggl projects. Not sure how to deal with that...'); + } + const tasks = await trektor.togglGateway.getTasks(projects[0].id); + if (tasks.some((task) => task.name === taskName)) return; + + await trektor.togglGateway.createTask(projects[0].id, taskName); } diff --git a/content_script.js b/content_script.js index 778fd4c..1b15810 100644 --- a/content_script.js +++ b/content_script.js @@ -1,146 +1,55 @@ -function addButton() { - const sidebar = document.querySelector(".window-sidebar") - const button = document.createElement('span'); - button.classList.add('button-link'); - button.addEventListener('click', onClick); - sidebar.prepend(button); - - const buttonIcon = document.createElement('span'); - buttonIcon.classList.add('icon-sm', 'plugin-icon'); - buttonIcon.innerText = '+'; - button.append(buttonIcon); - - const buttonText = document.createElement('span'); - buttonText.innerText = 'Toggl Task'; - button.append(buttonText); - - sidebar.querySelector(".mod-no-top-margin").classList.remove("mod-no-top-margin") - sidebar.querySelector(".js-sidebar-add-heading").classList.remove("mod-no-top-margin") +async function addButton() { + const sidebar = await awaitSelector(".window-sidebar", 10000); + sidebar.querySelector(".mod-no-top-margin")?.classList?.remove("mod-no-top-margin"); + sidebar.querySelector(".js-sidebar-add-heading")?.classList?.remove("mod-no-top-margin"); + + const button = document.createElement("span"); + button.classList.add("button-link"); + sidebar.prepend(button); + + const buttonIcon = document.createElement("span"); + buttonIcon.classList.add("icon-sm", "plugin-icon"); + buttonIcon.innerText = "+"; + button.append(buttonIcon); + + const buttonText = document.createElement("span"); + buttonText.innerText = "Toggl Task"; + button.append(buttonText); + + button.addEventListener("click", async () => { + const response = await trektor.runtime.sendMessage({ + action: 'addTask', + args: [window.location.pathname.split('/', 3)[2]], + }); + + if (response) window.alert(response); + }); } -const mappings = { - "GG": "gg", - "Audience Builder": "ab", - "Audience Exporter": "ae", - "Cta Calls": "xcta", - "Camper": "camper", - "Praktikum #pr": "pr" -}; - -function onClick() { - let trelloApiKey = "afadffe77f745496f80ebb4bf460c615" - trektor.storage.get(['trello', 'toggl']).then(result => { - const trelloToken = result.trello - const togglToken = result.toggl - - const idLong = window.location.pathname.split("/")[2]; - var url = new URL(`https://api.trello.com/1/cards/${idLong}`) - fetch(url.toString(), { - headers: { - 'Authorization': `OAuth oauth_consumer_key="${trelloApiKey}", oauth_token="${trelloToken}"`, - 'Content-Type': 'application/json' - }, - }).then(response => response.json()).then(response => { - const idShort = response["idShort"] - - var labelShort, labelLong = undefined - for (var i in response["labels"]) { - if (labelShort == undefined) { - labelShort = mappings[response["labels"][i]["name"]] - labelLong = (labelShort != undefined) ? response["labels"][i]["name"] : labelLong - } else if (mappings[response["labels"][i]["name"]] != undefined) { - alert(`Diese Karte hat sowohl "${labelShort}" als auch "${mappings[response["labels"][i]["name"]]}".`) - } - } - - if (labelShort == undefined) { - alert("Diese Karte besitzt kein unterstütztes Projekt Label.") - return false - } - - if (!response["name"].endsWith(`#${labelShort}_${idShort}`)) { - - - url = new URL(`https://api.trello.com/1/cards/${idLong}`) - - var data = { - "name": `${response["name"]} #${labelShort}_${idShort}` - }; - fetch(url.toString(), { - method: 'PUT', - headers: { - 'Authorization': `OAuth oauth_consumer_key="${trelloApiKey}", oauth_token="${trelloToken}"`, - 'Content-Type': 'application/json' - }, - body: JSON.stringify(data) - }).then(response => { - if (!response.ok) { - alert(`Error ${response.status}:\n${response.statusText}`) - return false - } - - }) - - - } - - url = new URL("https://api.track.toggl.com/api/v8/workspaces") - const togglAuth = btoa(`${togglToken}:api_token`) - trektor.fetchJSON(url.toString(), { - headers: { - 'Authorization': `Basic ${togglAuth}`, - 'Content-Type': 'application/json' - } - }).then(response => { - for (var i in response) { - if (response[i]["name"] == "aboutsource") { - var wid = response[i]["id"] - } - } - if (wid == undefined) { - alert("WorkspaceID undefined\nDies bedeutet, du hast entweder keinen Zugriff zum a:s toggl oder es ist kein API-Token angegeben.") - return false - } - - url = new URL(`https://api.track.toggl.com/api/v8/workspaces/${wid}/projects`) - trektor.fetchJSON(url.toString(), { - headers: { - 'Authorization': `Basic ${togglAuth}`, - 'Content-Type': 'application/json' - } - }).then(response => { - for (var i in response) { - if (response[i]["name"].endsWith(`(${labelShort})`)) { - var pid = response[i]["id"] - - url = new URL('https://api.track.toggl.com/api/v8/tasks') - data = { - "task": { - "name": `${labelShort}_${idShort}`, - "pid": pid - } - } - trektor.fetchJSON(url.toString(), { - method: 'POST', - headers: { - 'Authorization': `Basic ${togglAuth}`, - 'Content-Type': 'application/json' - }, - body: JSON.stringify(data) - }) - } - } - }) - }) - - }) - }) - - +function awaitSelector(selector, timeout) { + const element = document.querySelector(".window-sidebar"); + if (element !== null) return Promise.resolve(element); + + return new Promise((resolve, reject) => { + const interval = window.setInterval(() => { + const element = document.querySelector(".window-sidebar"); + + if (element !== null) { + resolve(element); + window.clearInterval(interval); + } else if (timeout < 0) { + reject(new Error('timeout')); + window.clearInterval(interval); + } + timeout -= 100; + }, 100); + }); } -window.addEventListener("pushstate", function () { - if (window.location.pathname.startsWith("/c/")) { - addButton() - } -}) +window.addEventListener("pushstate", () => { + if (window.location.pathname.startsWith("/c/")) addButton(); +}); + +window.addEventListener('load', () => { + if (window.location.pathname.startsWith("/c/")) addButton(); +}); diff --git a/manifest.json b/manifest.json index 939d602..0c60167 100644 --- a/manifest.json +++ b/manifest.json @@ -6,7 +6,6 @@ "icons": { "64": "icons/64.png" }, - "content_scripts": [ { "matches": [ diff --git a/options/index.html b/options/index.html index 743838d..eb3b60b 100644 --- a/options/index.html +++ b/options/index.html @@ -4,15 +4,31 @@ Trektor settings - + - -

Trektor settings

- + - Trello Token generieren
-
Toggl Token (ziemlich weit runter scrollen)
-


+ Trello Token generieren + + + + + + Toggl Token (ziemlich weit runter scrollen) + + + + diff --git a/options/script.js b/options/script.js index 37fc709..37e949f 100644 --- a/options/script.js +++ b/options/script.js @@ -1,14 +1,14 @@ -let trelloTextField = document.querySelector("#trello_token") -let togglTextField = document.querySelector("#toggl_token") +let trelloTextField = document.querySelector("input[name='trello']"); +let togglTextField = document.querySelector("input[name='toggl']"); trelloTextField.addEventListener("input", onChange) togglTextField.addEventListener("input", onChange) function onChange(e) { - trektor.storage.set({[e.target.name]: e.target.value}); + trektor.storage.set({[e.target.name]: e.target.value}); } trektor.storage.get(['trello', 'toggl']).then(result => { - trelloTextField.value = result.trello; - togglTextField.value = result.toggl; + trelloTextField.value = result.trello; + togglTextField.value = result.toggl; }) diff --git a/options/style.css b/options/style.css deleted file mode 100644 index 001d5c5..0000000 --- a/options/style.css +++ /dev/null @@ -1,3 +0,0 @@ -h1 { - font-style: italic; -} diff --git a/trektor.js b/trektor.js index 936e86b..3afca76 100644 --- a/trektor.js +++ b/trektor.js @@ -1,22 +1,22 @@ class ChromeStorage { - constructor() { - this.storage = chrome.storage.local; + constructor(storage) { + this.storage = storage; } get(keys) { - return new Promise((resolve) => { - this.storage.get(keys, resolve); - }) + return new Promise((resolve) => { + this.storage.get(keys, resolve); + }) } set(values) { - return new Promise((resolve) => { - this.storage.set(values, resolve); - }) + return new Promise((resolve) => { + this.storage.set(values, resolve); + }) } } class ChromeRuntime { - constructor() { - this.runtime = chrome.runtime; + constructor(runtime) { + this.runtime = runtime; } sendMessage(msg) { @@ -25,30 +25,106 @@ class ChromeRuntime { }); } - onMessage(callback) { - this.runtime.onMessage.addListener((msg, sender, respond) => { - callback(msg).then((response) => { respond(response) }); - return true; + get onMessage() { + const runtime = this.runtime; + + return { + addListener(callback) { + runtime.onMessage.addListener((msg, sender, respond) => { + callback(msg) + .then((response) => { respond(response) }) + .catch((error) => respond(error.message)); + return true; + }); + }, + } + } +} + +class TrelloGateway { + static ENDPOINT = "https://api.trello.com/1"; + static API_KEY = "afadffe77f745496f80ebb4bf460c615"; + + constructor(storage) { + this.storage = storage; + } + + getCard(id) { + return this.request("get", `/cards/${id}`); + } + + updateCard(id, data) { + return this.request("put", `/cards/${id}`, data); + } + + async request(method, path, data = null) { + const url = this.constructor.ENDPOINT + path; + const { trello: token } = await this.storage.get(["trello"]); + + const response = await fetch(url, { + method, + headers: { + "Authorization": `OAuth oauth_consumer_key="${this.constructor.API_KEY}", oauth_token="${token}"`, + "Content-Type": "application/json; charset=utf-8", + }, + body: (data === null) ? null : JSON.stringify(data), }); + + return response.json(); } } -window.trektor = { - storage: null, - runtime: null, +class TogglGateway { + static ENDPOINT = "https://api.track.toggl.com/api/v8"; + + constructor(storage) { + this.storage = storage; + } + + getWorkspaces() { + return this.request("get", "/workspaces"); + } + + getProjects(workspaceId) { + return this.request("get", `/workspaces/${workspaceId}/projects`); + } + + getTasks(projectId) { + return this.request("get", `/projects/${projectId}/tasks`); + } + + createTask(projectId, name) { + return this.request("post", "/tasks", { task: { name, pid: projectId } }); + } + + async request(method, path, data = null) { + const url = this.constructor.ENDPOINT + path; + const { toggl: token } = await this.storage.get(["toggl"]); - fetchJSON(url, options = {}) { - return this.runtime.sendMessage({ - action: 'fetchJSON', - args: [url, options], + const response = await fetch(url, { + method, + headers: { + "Authorization": `Basic ${btoa(`${token}:api_token`)}`, + "Content-Type": "application/json; charset=utf-8", + }, + body: (data === null) ? null : JSON.stringify(data), }); - }, -}; + + return response.json(); + } +} + +const trektor = {}; if (window.chrome !== undefined) { - window.trektor.storage = new ChromeStorage(); - window.trektor.runtime = new ChromeRuntime(); + trektor.storage = new ChromeStorage(chrome.storage.local); + trektor.runtime = new ChromeRuntime(chrome.runtime); } else { - window.trektor.storage = browser.storage.local; - window.trektor.runtime = browser.runtime; + trektor.storage = browser.storage.local; + trektor.runtime = browser.runtime; } + +trektor.trelloGateway = new TrelloGateway(trektor.storage); +trektor.togglGateway = new TogglGateway(trektor.storage); + +window.trektor = trektor;