-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Harvest Chrome Extension - V1 #1
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
# Harvest Extension | ||
|
||
The Harvest Extension is a Chrome extension that allows Crowdlinker team to start the Harvest App timer directly from the Shortcut Board and Google Calendar to track time more efficiently. | ||
|
||
## Installation | ||
|
||
1. Clone the repository: `git clone https://github.com/CrowdLinker/Harvest-Chrome-Extension.git` | ||
2. Open Google Chrome and go to `chrome://extensions` | ||
3. Enable Developer mode by toggling the switch in the top right corner | ||
4. Click on "Load unpacked" and select the cloned repository folder. | ||
|
||
## Usage | ||
Follow the below Notion link to get the detailed information on how to use the extension. | ||
|
||
[Harvest Extension Usage](https://www.notion.so/crowdlinker/Project-Polaris-Time-Tracking-Assistant-593ebc0ae0554358bf3ba438430e9559#b3a38f59c1424d9a95826a8f0498320d) | ||
|
||
## Contributing | ||
|
||
Contributions are welcome! If you have any suggestions, bug reports, or feature request please open an issue or submit a pull request. | ||
|
||
## Contact | ||
|
||
For any inquiries, please contact the Crowdlinker team at [[email protected]](mailto:[email protected]). |
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @mayank2424 This file is following multiple indentation formatting. Could you please set it to 2 spaces and re-indent/prettify this file. I would suggest adding a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @prateekkathal I'll address this change. Thank you! |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
chrome.runtime.onInstalled.addListener(async () => { | ||
try { | ||
const tab = await getCurrentTab(); | ||
await executeScript(tab); | ||
} catch (err) { | ||
console.error(`Failed to fetch script: ${err}`); | ||
} | ||
}); | ||
|
||
async function executeScript(tab) { | ||
try { | ||
// Avoid running in chrome:// pages | ||
if(tab.url.startsWith('chrome://')) { | ||
return; | ||
} | ||
|
||
chrome.scripting.executeScript({ | ||
target: { tabId: tab.id }, | ||
files: ['harvest-script.js'], | ||
}) | ||
.then(() => { | ||
console.log('Script injected successfully.'); | ||
}) | ||
.catch(err => { | ||
console.error(`Failed to inject script: ${err}`); | ||
}); | ||
} catch (err) { | ||
console.error(`Failed to execute script: ${err}`); | ||
} | ||
} | ||
|
||
|
||
async function getCurrentTab() { | ||
let queryOptions = { active: true, lastFocusedWindow: true }; | ||
// `tab` will either be a `tabs.Tab` instance or `undefined`. | ||
let [tab] = await chrome.tabs.query(queryOptions); | ||
return tab; | ||
} | ||
|
||
|
||
chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => { | ||
if (changeInfo.status === 'complete' && (tab.url.startsWith('https://app.shortcut.com/') || tab.url.startsWith('https://calendar.google.com/'))) { | ||
await executeScript(tab); | ||
} | ||
}); | ||
|
||
|
||
// TODO: Not used | ||
async function checkIfHarvestTimerIsRunning() { | ||
try { | ||
const apiResponse = await fetch("https://api.harvestapp.com/v2/platform/running_time_entry", { | ||
method: "GET", | ||
// credentials: 'include' | ||
}); | ||
|
||
const response = await apiResponse.json(); | ||
|
||
console.log({ response }); | ||
} catch(error) { | ||
console.error("Failed to fetch running time entry.", error); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,288 @@ | ||
function detectStoryAndAppendTimerElement() { | ||
const shortcutStoryModalContainerId = "story-dialog-parent"; | ||
|
||
const storyModal = document.getElementById(shortcutStoryModalContainerId); | ||
|
||
if(!storyModal) { | ||
console.log("Story modal not found"); | ||
return; | ||
} | ||
|
||
const harvestTimerID = "polaris-harvest-timer"; | ||
|
||
if(storyModal) { | ||
// chrome.runtime.sendMessage({ action: 'checkIfHarvestTimerIsRunning' }, function(response) { | ||
// }); | ||
|
||
const { | ||
storyId, | ||
storyName, | ||
storyPermaLink | ||
} = getShortcutStoryDetails(); | ||
|
||
const elementToAppendId = document.getElementById('cid-breadcrumbs-story-dialog'); | ||
const startTimerButton = document.getElementById(harvestTimerID); | ||
|
||
if(elementToAppendId) { | ||
|
||
console.log("Element found to append timer."); | ||
if(!storyId || !storyName || !storyPermaLink) { | ||
console.log("Story details not found."); | ||
return; | ||
} | ||
|
||
createStartTimerButton("shortcut", elementToAppendId, harvestTimerID, storyId, storyName, storyPermaLink); | ||
|
||
window._harvestPlatformConfig = { | ||
"applicationName": "Crowdlinker", | ||
"permalink": window.location.href, | ||
"skipStyling": true // Override the default styling | ||
}; | ||
|
||
// Use to dispatch event after loading buttons | ||
const event = new CustomEvent("harvest-event:timers:add", { | ||
detail: { element: document.querySelector(".harvest-timer") } | ||
}); | ||
document.querySelector("#harvest-messaging").dispatchEvent(event); | ||
} | ||
} | ||
} | ||
|
||
function detectGoogleCalendarEvent() { | ||
const googleCalendarEventContainer = document.querySelector('[data-open-edit-note]'); | ||
const eventHeaderElement = document.querySelector('.wv9rPe'); | ||
|
||
if(!googleCalendarEventContainer || !eventHeaderElement) { | ||
return; | ||
} | ||
|
||
const harvestTimerID = "polaris-harvest-timer"; | ||
|
||
if(!document.getElementById(harvestTimerID)) { | ||
const eventName = document.querySelector('[data-open-edit-note] [data-text]').textContent; | ||
const eventId = googleCalendarEventContainer.getAttribute('data-eventid'); | ||
|
||
createStartTimerButton("google-calendar", eventHeaderElement, harvestTimerID, eventId, eventName); | ||
|
||
window._harvestPlatformConfig = { | ||
"applicationName": "Crowdlinker", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @mayank2424 @ankesh7 Can we move these to a common Same for any RGB/Hex colors that you feel will be nice to haves for the community. |
||
"permalink": window.location.href, | ||
"skipStyling": true // Override the default styling | ||
}; | ||
|
||
// Use to dispatch event after loading buttons | ||
const event = new CustomEvent("harvest-event:timers:add", { | ||
detail: { element: document.querySelector(".harvest-timer") } | ||
}); | ||
document.querySelector("#harvest-messaging").dispatchEvent(event); | ||
} | ||
|
||
} | ||
|
||
console.log("Content script loaded."); | ||
|
||
window.addEventListener("load", () => { | ||
console.log("Window loaded.", { window }); | ||
|
||
// TODO: Not working as expected, need to fix this | ||
// function observeDOM() { | ||
// const observer = new MutationObserver(() => { | ||
// console.log("DOM mutation detected."); | ||
// detectStoryAndAppendTimerElement(); | ||
// }); | ||
|
||
// observer.observe(document.body, { childList: true, subtree: true }); | ||
// } | ||
setInterval(() => { | ||
if(window.location.href.includes("https://app.shortcut.com/")) { | ||
chrome.storage.sync.get('shortcutCheckBox') | ||
.then((res) => { | ||
if(res?.shortcutCheckBox) { | ||
detectStoryAndAppendTimerElement(); | ||
} | ||
else if(!window.localStorage.getItem("harvest_timer_started")) { | ||
// delete timer element | ||
const timerElement = document.getElementById("polaris-harvest-timer"); | ||
if(timerElement) { | ||
timerElement.remove(); | ||
} | ||
} | ||
}) | ||
} | ||
else if( window.location.href.includes("https://calendar.google.com/")) { | ||
chrome.storage.sync.get('googleCalendarCheckBox') | ||
.then((res) => { | ||
if(res?.googleCalendarCheckBox) { | ||
detectGoogleCalendarEvent(); | ||
} | ||
else if(!window.localStorage.getItem("google_calendar_timer_started")) { | ||
// delete timer element | ||
const timerElement = document.getElementById("polaris-harvest-timer"); | ||
if(timerElement) { | ||
timerElement.remove(); | ||
} | ||
} | ||
}) | ||
|
||
} | ||
}, 500); | ||
}); | ||
|
||
|
||
function getShortcutStoryDetails() { | ||
const storyDialogElement = document.querySelector(".story-dialog"); | ||
|
||
if(!storyDialogElement) { | ||
return null; | ||
} | ||
|
||
const storyId = storyDialogElement?.getAttribute("data-id"); | ||
const storyName = storyDialogElement?.querySelector(".story-name")?.textContent; | ||
|
||
const storyAttributesElement = storyDialogElement?.querySelector(".story-attributes"); | ||
const storyPermaLink = storyAttributesElement?.querySelector('.attribute > span+button')?.baseURI; | ||
|
||
return { | ||
storyId, | ||
storyName, | ||
storyPermaLink | ||
}; | ||
|
||
} | ||
|
||
// Using Standard JavaScript: | ||
document.body.addEventListener("harvest-event:ready", function (event) { | ||
console.log("Harvest Buttons are ready."); | ||
console.log({ event }); | ||
}); | ||
|
||
|
||
// Listen for messages from the injected script | ||
window.onmessage = (event) => { | ||
if (event.origin !== "https://platform.harvestapp.com") { | ||
return; | ||
} | ||
|
||
console.log(`Received message`); | ||
console.log(event.data); | ||
const { type, value } = event.data; | ||
|
||
if(type === "timer:started") { | ||
console.log("Timer started."); | ||
const buttonElement = document.getElementById("start_time_btn"); | ||
|
||
if(buttonElement) { | ||
buttonElement.innerHTML = "Stop Timer"; | ||
buttonElement.style.backgroundColor = "rgb(189 54 54)"; | ||
|
||
if(value?.external_reference?.permalink?.includes("https://app.shortcut.com/")) { | ||
window.localStorage.setItem("harvest_timer_started", JSON.stringify(value)); | ||
} | ||
else if(value?.external_reference?.permalink?.includes("https://calendar.google.com")){ | ||
window.localStorage.setItem("google_calendar_timer_started", JSON.stringify(value)); | ||
} | ||
} | ||
} | ||
|
||
if(type === "timer:stopped") { | ||
console.log("Timer stopped."); | ||
const buttonElement = document.getElementById("start_time_btn"); | ||
|
||
if(buttonElement) { | ||
buttonElement.innerHTML = "Start Timer"; | ||
buttonElement.style.backgroundColor = "#188433"; | ||
|
||
if(value?.external_reference?.permalink?.includes("https://app.shortcut.com/")) { | ||
window.localStorage.removeItem("harvest_timer_started"); | ||
} | ||
else if(value?.external_reference?.permalink?.includes("https://calendar.google.com")){ | ||
window.localStorage.removeItem("google_calendar_timer_started"); | ||
} | ||
|
||
} | ||
} | ||
}; | ||
|
||
function createStartTimerButton(type, elementToAppend, harvestTimerID, id, name, storyPermaLink) { | ||
|
||
let buttonElement = document.getElementById("start_time_btn"); | ||
if(!buttonElement) { | ||
buttonElement = document.createElement('button'); | ||
} | ||
|
||
buttonElement.id = "start_time_btn"; | ||
buttonElement.className = "harvest-timer"; | ||
buttonElement.setAttribute('data-id', id); | ||
buttonElement.style = "background: transparent; cursor: pointer; padding: 8px; border-radius: 8px; border: 1px solid grey; color: #fff; font-weight: 700; font-size: 14px; border: none;"; | ||
buttonElement.innerHTML = | ||
type === "shortcut" ? checkIfTimerStartedForStory(id) ? "Stop Timer" : "Start Timer" | ||
: checkIfTimerStartedForGoogleCalendarEvent(id) ? "Stop Timer" : "Start Timer"; | ||
buttonElement.style.backgroundColor = | ||
type === "shortcut" ? checkIfTimerStartedForStory(id) ? "rgb(189 54 54)" : "#188433" | ||
: checkIfTimerStartedForGoogleCalendarEvent(id) ? "rgb(189 54 54)" : "#188433"; | ||
checkIfTimerStartedForStory(id) ? "rgb(189 54 54)" : "#188433"; | ||
|
||
switch(type) { | ||
case "shortcut": | ||
buttonElement.dataset.item = JSON.stringify({ | ||
"id": id, | ||
"name": `${id} - ${name}`, | ||
"permalink": storyPermaLink || '' | ||
}); | ||
|
||
break; | ||
case "google-calendar": | ||
|
||
buttonElement.dataset.item = JSON.stringify({ | ||
"id": id, | ||
"name": `Meeting - ${name}`, | ||
"permalink": "https://calendar.google.com/" | ||
}); | ||
|
||
} | ||
|
||
timerElement = document.getElementById("polaris-harvest-timer") || document.createElement('div'); | ||
|
||
switch(type) { | ||
case "shortcut": | ||
timerElement.style = "float:right; margin: 0px 20px;"; | ||
break; | ||
case "google-calendar": | ||
timerElement.style = "float:right; margin: 0px 20px; position: absolute; left: 0; margin-top: 4px;"; | ||
break; | ||
} | ||
|
||
timerElement.id = harvestTimerID; | ||
timerElement.innerHTML = ` | ||
<div class="harvest_timer_btn_div"> </div> | ||
`; | ||
|
||
timerElement.appendChild(buttonElement); | ||
|
||
elementToAppend.appendChild(timerElement); | ||
|
||
} | ||
|
||
|
||
function checkIfTimerStartedForStory(storyId) { | ||
const startedTimer = window.localStorage.getItem("harvest_timer_started"); | ||
const startedTimerObj = startedTimer ? JSON.parse(startedTimer) : {}; | ||
|
||
if(startedTimerObj?.external_reference?.id === storyId) { | ||
return true; | ||
} | ||
|
||
return false; | ||
} | ||
|
||
function checkIfTimerStartedForGoogleCalendarEvent(eventId) { | ||
const startedTimer = window.localStorage.getItem("google_calendar_timer_started"); | ||
const startedTimerObj = startedTimer ? JSON.parse(startedTimer) : {}; | ||
|
||
|
||
if(startedTimerObj?.external_reference?.id === eventId) { | ||
return true; | ||
} | ||
|
||
return false; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mayank2424 @ankesh7 I think this file can be removed and added to
.gitignore
after removal