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

almost done #14

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,15 @@ This addon extends the functionality of "Basecamp 3" by the following features:
- quick-delay to-dos (by days defined in options, in assigments by 1 day)
- create to-dos directly from messages

### How-to use
### How to use
1. download build folder
2. navigate to chrome://extensions
3. turn on developer mode
4. click on "load unpacked" and choose the build folder
sudo

### How to test in development
- This addon cannot be tested locally since it needs data from basecamp
- use the standard 'npm run build' to build the project upon code changes
- refresh the plugin in chrome://extensions page or turn the plugin off and on again
- refresh the tab with basecamp
2 changes: 1 addition & 1 deletion build/asset-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"main.js": "/static/js/bundle.js",
"main.js.map": "/static/js/bundle.js.map",
"index.html": "/index.html",
"precache-manifest.9221fb59784b077114c7728033b3bbb0.js": "/precache-manifest.9221fb59784b077114c7728033b3bbb0.js",
"precache-manifest.c366a9e7a9d7bc098d8f3a3036180775.js": "/precache-manifest.c366a9e7a9d7bc098d8f3a3036180775.js",
"service-worker.js": "/service-worker.js",
"static/js/bundle.js.LICENSE": "/static/js/bundle.js.LICENSE"
}
1 change: 1 addition & 0 deletions build/img/hamburger.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion build/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,6 @@
"tabs",
"storage"
],
"web_accessible_resources": ["img/hamburger.svg"],
"content_security_policy": "script-src 'self' 'sha256-5As4+3YpY62+l38PsxCEkjB1R4YtyktBtRScTJ3fyLU='; object-src 'self'"
}

Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ self.__precacheManifest = (self.__precacheManifest || []).concat([
"url": "/index.html"
},
{
"revision": "e18696aac828c71fa2e0",
"revision": "8da3fc863236698dd669",
"url": "/static/js/bundle.js"
},
{
Expand Down
2 changes: 1 addition & 1 deletion build/service-worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
importScripts("https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js");

importScripts(
"/precache-manifest.9221fb59784b077114c7728033b3bbb0.js"
"/precache-manifest.c366a9e7a9d7bc098d8f3a3036180775.js"
);

self.addEventListener('message', (event) => {
Expand Down
2 changes: 1 addition & 1 deletion build/static/js/bundle.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion build/static/js/bundle.js.map

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions public/img/hamburger.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion public/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,6 @@
"tabs",
"storage"
],
"web_accessible_resources": ["img/hamburger.svg"],
"content_security_policy": "script-src 'self' 'sha256-5As4+3YpY62+l38PsxCEkjB1R4YtyktBtRScTJ3fyLU='; object-src 'self'"
}

94 changes: 76 additions & 18 deletions src/features/todo-quick-delay/TodoQuickDelayButton.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,56 @@
import React from 'react';
import axios, { AxiosResponse } from 'axios';
import { calculateNewDueDate, getBasecampFormattedDueDate } from '../../shared/date-helpers';
import {
calculateNewDueDate,
calculateNewDueMonth,
getBasecampFormattedDueDate,
getDelayString, getDelayStringMonths
} from '../../shared/date-helpers';

class TodoQuickDelayButton extends React.Component<TodoQuickDelayProps> {
constructor(props: TodoQuickDelayProps) {
state = {
menuOpened: false
}

constructor(props: TodoQuickDelayProps) {
super(props);
}

delayTodoOneDay = async () => {
const task = (await axios.get(`https://3.basecamp.com/${this.props.basecampID}/buckets/${this.props.bucketID}/todos/${this.props.todoID}.json`)).data;
await axios.put(`https://3.basecamp.com/${this.props.basecampID}/buckets/${this.props.bucketID}/todos/${this.props.todoID}.json`, {
...task,
assignee_ids: task.assignees.map((a: any) => a.id),
completion_subscriber_ids: task.completion_subscribers.map((s: any) => s.id),
due_on: calculateNewDueDate(task.due_on, 1)
}
).then((response: AxiosResponse) => {
this.updateDOMAfterDelay(response.data.due_on);
});
};
//only for days and weeks
delayTodo = async (delay: number) => {
const task = (await axios.get(`https://3.basecamp.com/${this.props.basecampID}/buckets/${this.props.bucketID}/todos/${this.props.todoID}.json`)).data;
await axios.put(`https://3.basecamp.com/${this.props.basecampID}/buckets/${this.props.bucketID}/todos/${this.props.todoID}.json`, {
...task,
assignee_ids: task.assignees.map((a: any) => a.id),
completion_subscriber_ids: task.completion_subscribers.map((s: any) => s.id),
due_on: calculateNewDueDate(task.due_on, delay)
}
).then((response: AxiosResponse) => {
this.updateDOMAfterDelay(response.data.due_on);
});
};

//for adding delay of months
delayTodoMonth = async (delayMonth: number) => {
const task = (await axios.get(`https://3.basecamp.com/${this.props.basecampID}/buckets/${this.props.bucketID}/todos/${this.props.todoID}.json`)).data;
await axios.put(`https://3.basecamp.com/${this.props.basecampID}/buckets/${this.props.bucketID}/todos/${this.props.todoID}.json`, {
...task,
assignee_ids: task.assignees.map((a: any) => a.id),
completion_subscriber_ids: task.completion_subscribers.map((s: any) => s.id),
due_on: calculateNewDueMonth(task.due_on, delayMonth)
}
).then((response: AxiosResponse) => {
this.updateDOMAfterDelay(response.data.due_on);
});
};

toggleMenu = async () => {
if (this.state.menuOpened === false) {
this.setState({menuOpened: true});
} else {
this.setState({menuOpened: false});
}
}

updateDOMAfterDelay = (newDueDate: string) => {
const newDueDateFormatted: string = getBasecampFormattedDueDate(newDueDate);
Expand All @@ -29,10 +61,36 @@ class TodoQuickDelayButton extends React.Component<TodoQuickDelayProps> {
render() {
return (
<div>
<span onClick={this.delayTodoOneDay}
style={{ color: 'rgba(0,0,0,0.3)', lineHeight: '0rem', position: 'absolute', left: '0.4em', top: '0.95em', cursor: 'pointer' }}>
+1
</span>
<div>
<img className={"todo-delay-menu-button"}
style={{width: '1.43em', height: '1.43em', position: 'absolute', top: '0.25em', left: '0', cursor:'move'}}
src ={chrome.extension.getURL('img/hamburger.svg')}
onClick={this.toggleMenu}>
</img>
</div>
{this.state.menuOpened &&
<div className={"todo-delay-menu"} style={{width: 'auto', height:'auto', position: 'absolute', top:'0px', left:'-110px', background: 'white', zIndex: 6,
padding: '0.5rem 1.5rem' , borderRadius: '4px', border: '1px solid rgba(0,0,0,0.1)', boxShadow: '0 0 4px rgb(0 0 0 / 10%), 0 5px 20px rgb(0 0 0 / 5%)'}}>
<div className={"todo-delay-content"}>
<ul className="action-list todo-quick-delay">
{this.props.quickDelayDays.map(delay =>
<li className="action-list__item">
<a className="action-list__action" style={{ cursor: 'pointer' }} onClick={() => this.delayTodo(delay)}>
{getDelayString(delay)}
</a>
</li>
)}
{this.props.quickDelayMonths.map(delayM =>
<li className="action-list__item">
<a className="action-list__action" style={{ cursor: 'pointer' }} onClick={() => this.delayTodoMonth(delayM)}>
{getDelayStringMonths(delayM)}
</a>
</li>
)}
</ul>
</div>
</div>
}
</div>
);
}
Expand Down
8 changes: 4 additions & 4 deletions src/features/todo-quick-delay/TodoQuickDelayInsert.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { todoQuickDelayFeatureID, todoQuickDelayFeatureIDForAssigments } from '.
import TodoQuickDelayMenu from './TodoQuickDelayMenu';
import TodoQuickDelayButton from './TodoQuickDelayButton';

export function addTodoQuickDelayFeature(node: Element, basecampID: string, quickDelayDays: number[]) {
export function addTodoQuickDelayFeature(node: Element, basecampID: string, quickDelayDays: number[], quickDelayMonths: number[]) {
if (!todoQuickDelayFeatureAddable(node)) return;

const bucketID: string = node.parentElement!.parentElement!.getAttribute('data-url')!.split('buckets/')[1].split('/')[0];
Expand All @@ -14,7 +14,7 @@ export function addTodoQuickDelayFeature(node: Element, basecampID: string, quic
container.id = todoQuickDelayFeatureID;
node.getElementsByClassName('action-menu__content')[0].appendChild(container);
render(
<TodoQuickDelayMenu basecampID={basecampID} bucketID={bucketID} todoID={todoID} quickDelayDays={quickDelayDays}/>,
<TodoQuickDelayMenu basecampID={basecampID} bucketID={bucketID} todoID={todoID} quickDelayDays={quickDelayDays} quickDelayMonths={quickDelayMonths}/>,
document.getElementById(todoQuickDelayFeatureID)
);
}
Expand All @@ -32,7 +32,7 @@ function todoQuickDelayFeatureAddable(node: Element): boolean {
&& node.parentElement!.querySelector('.checkbox .checkbox__content .todo__date') !== null;
}

export function addTodoQuickDelayFeatureForAssigments(basecampID: string, quickDelayDays: number[]) {
export function addTodoQuickDelayFeatureForAssigments(basecampID: string, quickDelayDays: number[], quickDelayMonths: number[]) {
const assignmentTodolists: NodeListOf<Element> = document.querySelectorAll('article.todolist--assignments');
if (!assignmentTodolists) return;

Expand All @@ -59,7 +59,7 @@ export function addTodoQuickDelayFeatureForAssigments(basecampID: string, quickD
insertEl.insertBefore(container, insertEl.childNodes[0]);

render(
<TodoQuickDelayButton basecampID={basecampID} bucketID={bucketID} todoID={todoID} quickDelayDays={quickDelayDays}/>,
<TodoQuickDelayButton basecampID={basecampID} bucketID={bucketID} todoID={todoID} quickDelayDays={quickDelayDays} quickDelayMonths={quickDelayMonths}/>,
document.getElementById(`${todoQuickDelayFeatureIDForAssigments}-${todoID}`)
);
});
Expand Down
43 changes: 35 additions & 8 deletions src/features/todo-quick-delay/TodoQuickDelayMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import React from 'react';
import axios, { AxiosResponse } from 'axios';
import { calculateNewDueDate, getBasecampFormattedDueDate, getDelayString } from '../../shared/date-helpers';
import {
calculateNewDueDate, calculateNewDueMonth,
getBasecampFormattedDueDate,
getDelayString,
getDelayStringMonths
} from '../../shared/date-helpers';

class TodoQuickDelayMenu extends React.Component<TodoQuickDelayProps> {
constructor(props: TodoQuickDelayProps) {
super(props);
}

//for adding delay of days and weeks
delayTodo = async (delay: number) => {
const task = (await axios.get(`https://3.basecamp.com/${this.props.basecampID}/buckets/${this.props.bucketID}/todos/${this.props.todoID}.json`)).data;
await axios.put(`https://3.basecamp.com/${this.props.basecampID}/buckets/${this.props.bucketID}/todos/${this.props.todoID}.json`, {
Expand All @@ -20,6 +26,20 @@ class TodoQuickDelayMenu extends React.Component<TodoQuickDelayProps> {
});
};

//for adding delay of months
delayTodoMonth = async (delayMonth: number) => {
const task = (await axios.get(`https://3.basecamp.com/${this.props.basecampID}/buckets/${this.props.bucketID}/todos/${this.props.todoID}.json`)).data;
await axios.put(`https://3.basecamp.com/${this.props.basecampID}/buckets/${this.props.bucketID}/todos/${this.props.todoID}.json`, {
...task,
assignee_ids: task.assignees.map((a: any) => a.id),
completion_subscriber_ids: task.completion_subscribers.map((s: any) => s.id),
due_on: calculateNewDueMonth(task.due_on, delayMonth)
}
).then((response: AxiosResponse) => {
this.updateDOMAfterDelay(response.data.due_on);
});
};

updateDOMAfterDelay = (newDueDate: string) => {
const newDueDateFormatted: string = getBasecampFormattedDueDate(newDueDate);
const dueDateNode: Node = document.querySelector(`li[data-recording-id='${this.props.todoID}'] span.todo__date a`)!.childNodes[2];
Expand All @@ -30,13 +50,20 @@ class TodoQuickDelayMenu extends React.Component<TodoQuickDelayProps> {
return (
<div>
<ul className="action-list todo-quick-delay" style={{ borderTop: '1px solid #e5e5e5' }}>
{this.props.quickDelayDays.map(delay =>
<li className="action-list__item">
<a className="action-list__action" style={{ cursor: 'pointer' }} onClick={() => this.delayTodo(delay)}>
{getDelayString(delay)}
</a>
</li>
)}
{this.props.quickDelayDays.map(delay =>
<li className="action-list__item">
<a className="action-list__action" style={{ cursor: 'pointer' }} onClick={() => this.delayTodo(delay)}>
{getDelayString(delay)}
</a>
</li>
)}
{this.props.quickDelayMonths.map(delayM =>
<li className="action-list__item">
<a className="action-list__action" style={{ cursor: 'pointer' }} onClick={() => this.delayTodoMonth(delayM)}>
{getDelayStringMonths(delayM)}
</a>
</li>
)}
</ul>
</div>
);
Expand Down
1 change: 1 addition & 0 deletions src/features/todo-quick-delay/TodoQuickDelayProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ interface TodoQuickDelayProps {
bucketID: string;
todoID: string;
quickDelayDays: number[];
quickDelayMonths: number[];
}
3 changes: 2 additions & 1 deletion src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ serviceWorker.unregister();
chrome.runtime.onMessage.addListener((msg: IExtensionMessage) => {
const basecampID: string = msg.basecampID;
const quickDelayDays: number[] = msg.options.quickDelayDays ? parseDelayDayOptionsString(msg.options.quickDelayDays as any as string) : [1,3,7];
const options: IExtensionOptions = { ...msg.options, quickDelayDays };
const quickDelayMonths: number[] = [1];
const options: IExtensionOptions = { ...msg.options, quickDelayDays, quickDelayMonths };

addFeatures(basecampID, options, todoQuickDelayFeatureID, todoFromMessageFeatureID);
});
1 change: 1 addition & 0 deletions src/shared/IExtensionOptions.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
interface IExtensionOptions {
quickDelayDays: number[];
quickDelayMonths: number[];
}
6 changes: 4 additions & 2 deletions src/shared/add-features.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,20 @@ export function addFeatures(basecampID: string, options: IExtensionOptions, ...f
addDynamicDOMFeatures(basecampID, options, ...features);
}

// for add to-dos from message conversation and add delay on my assignments page
export function addFixedDOMFeatures(basecampID: string, options: IExtensionOptions, ...features: string[]) {
if (features.includes(todoFromMessageFeatureID)) addTodoFromMessageFeature(basecampID);
if (features.includes(todoQuickDelayFeatureID)) addTodoQuickDelayFeatureForAssigments(basecampID, options.quickDelayDays);
if (features.includes(todoQuickDelayFeatureID)) addTodoQuickDelayFeatureForAssigments(basecampID, options.quickDelayDays, options.quickDelayMonths);
}

// for adding delay in general to-dos page
export function addDynamicDOMFeatures(basecampID: string, options: IExtensionOptions, ...features: string[]) {
const observer = new MutationObserver((mutations: MutationRecord[]) => {
(mutations as any).forEach((mutation: MutationRecord) => {
if (!mutation.addedNodes) return;
for (let i = 0; i < mutation.addedNodes.length; i++) {
const node: Node = mutation.addedNodes[i];
if (features.includes(todoQuickDelayFeatureID)) addTodoQuickDelayFeature(node as Element, basecampID, options.quickDelayDays);
if (features.includes(todoQuickDelayFeatureID)) addTodoQuickDelayFeature(node as Element, basecampID, options.quickDelayDays, options.quickDelayMonths);
}
});
});
Expand Down
23 changes: 19 additions & 4 deletions src/shared/date-helpers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { addDays, format } from 'date-fns';
import {addDays, addMonths, format} from 'date-fns';

export function getDateFromString(d: string): Date {
const array: string[] = d.split('-');
Expand All @@ -9,7 +9,7 @@ export function getDateFromString(d: string): Date {
}

// Feature "TodoQuickDelay"

// Preparing string for adding delay by a days and weeks
export function getDelayString(d: number): string {
const prefix: string = d >= 0 ? '+ ' : '- ';
let value: number = Math.abs(d);
Expand All @@ -19,14 +19,29 @@ export function getDelayString(d: number): string {
return prefix + value + suffix;
}

// Preparing string for adding delay by a month
export function getDelayStringMonths(m: number) : string {
const prefix: string = m >= 0 ? '+ ' : '- ';
let value: number = Math.abs(m);
let suffix: string = ' month';
if (value > 1) suffix += 's';
return prefix + value + suffix;
}

export function calculateNewDueDate(d: string, delay: number): string {
const oldDueDate: Date = getDateFromString(d);
return format(addDays(oldDueDate, delay),'YYYY-MM-DD');
return format(addDays(oldDueDate, delay),'yyyy-MM-dd');
}

// to calculate new month
export function calculateNewDueMonth(d: string, delay: number): string {
const oldDueDate :Date = getDateFromString(d);
return format(addMonths(oldDueDate, delay), 'yyyy-MM-dd');
}

export function getBasecampFormattedDueDate(d: string): string {
const date: Date = getDateFromString(d);
return format(date, ' ddd, MMM D');
return format(date, ' EEE, MMM d');
}

export function parseDelayDayOptionsString(s: string): number[] {
Expand Down