Skip to content
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

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added .gitignore
Empty file.
23 changes: 23 additions & 0 deletions ReadMe.md
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]).
Binary file added assets/.DS_Store
Copy link
Member

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

Binary file not shown.
Binary file added assets/fonts/WorkSans-Medium.ttf
Binary file not shown.
Binary file added assets/icons/default.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
62 changes: 62 additions & 0 deletions background.js
Copy link
Member

Choose a reason for hiding this comment

The 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 .prettierrc.js file if it helps. You can copy it from an existing gist online.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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);
}
}
288 changes: 288 additions & 0 deletions content.js
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",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mayank2424 @ankesh7 Can we move these to a common config.js so it's easier for other open source users to modify for their own needs?

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;
}
Loading