Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
jamiebrynes7 committed Nov 26, 2023
1 parent 71bc89d commit a2e7d0a
Show file tree
Hide file tree
Showing 31 changed files with 878 additions and 460 deletions.
9 changes: 6 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"name": "todoist-plugin",
"name": "obsidian-todoist-plugin",
"version": "1.11.1",
"description": "A Todoist plugin for Obsidian",
"main": "src/index.js",
"scripts": {
"dev": "npm run build && cp -R dist/* test-vault/.obsidian/plugins/todoist-sync-plugin/",
"dev": "npm run build && cp -R dist/* ../../test-vault/.obsidian/plugins/todoist-sync-plugin/",
"build": "svelte-check && rollup -c",
"test": "mocha -r ts-node/register src/**/*.test.ts",
"format": "prettier --write src/**/*",
Expand All @@ -13,11 +13,13 @@
"author": "Jamie Brynes",
"license": "ISC",
"dependencies": {
"@types/node": "^18.11.17",
"camelize": "^1.0.1",
"moment": "^2.29.4",
"obsidian": "0.15",
"snakeize": "^0.1.0",
"svelte": "^3.55.0",
"svelte-select": "^5.0.1",
"todoist-api": "^1.0.0",
"tslib": "^2.4.1",
"yaml": "^2.1.3"
},
Expand All @@ -28,6 +30,7 @@
"@tsconfig/svelte": "^3.0.0",
"@types/chai": "^4.3.4",
"@types/mocha": "^10.0.1",
"@types/node": "^18.11.17",
"chai": "^4.3.7",
"mocha": "^10.2.0",
"prettier": "^2.8.1",
Expand Down
6 changes: 6 additions & 0 deletions src/api/domain/label.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export type LabelId = string;

export type Label = {
id: LabelId,
name: string,
}
8 changes: 8 additions & 0 deletions src/api/domain/project.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export type ProjectId = string;

export type Project = {
id: ProjectId,
parentId: ProjectId | null,
name: string,
order: number,
}
10 changes: 10 additions & 0 deletions src/api/domain/section.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { ProjectId } from "./project";

export type SectionId = string;

export type Section = {
id: SectionId,
projectId: ProjectId,
name: string,
order: number,
}
77 changes: 77 additions & 0 deletions src/api/domain/task.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import moment from "moment";
import type { ProjectId } from "./project";
import type { SectionId } from "./section";

export type TaskId = string;

export type Task = {
id: TaskId,

content: string,
description: string,

projectId: ProjectId,
sectionId: SectionId | null,
parentId: TaskId | null,

labels: string[],
priority: Priority,

due: DueDate | null,

order: number,
};

export type DueDate = {
recurring: boolean,
date: string,
datetime?: string,
};

export type DueDateInfo = {
hasDate: boolean,
hasTime: boolean,

isOverdue: boolean,
isToday: boolean,

m?: moment.Moment,
}

export function getDueDateInfo(dueDate: DueDate | undefined): DueDateInfo {
if (dueDate === undefined) {
return {
hasDate: false,
hasTime: false,

isOverdue: false,
isToday: false,
};
}

const hasTime = dueDate.datetime !== undefined;
const date = moment(dueDate.datetime ?? dueDate.date);

const isToday = date.isSame(new Date(), "day");
const isOverdue = hasTime ? date.isBefore() : date.clone().add(1, "day").isBefore()


return {
hasDate: true,
hasTime: hasTime,
isToday: isToday,
isOverdue: isOverdue,
m: date,
};
}

export type Priority = 1 | 2 | 3 | 4;

export type CreateTaskParams = {
description?: string;
priority?: number;
projectId?: ProjectId,
sectionId?: SectionId,
dueDate?: string,
labels?: string[],
};
15 changes: 15 additions & 0 deletions src/api/fetcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export interface WebFetcher {
fetch(params: RequestParams): Promise<WebResponse>,
}

export type RequestParams = {
url: string,
method: string,
headers?: Record<string, string>,
body?: string,
};

export type WebResponse = {
statusCode: number,
body: string,
}
85 changes: 85 additions & 0 deletions src/api/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import type { Label } from "./domain/label";
import type { Project } from "./domain/project";
import type { Section } from "./domain/section";
import type { CreateTaskParams, Task, TaskId } from "./domain/task";
import type { RequestParams, WebFetcher, WebResponse } from "./fetcher";
import camelize from "camelize";
import snakeize from "snakeize";

// TODO: Consider validating API response shape w/ zod or similar.
export class TodoistApiClient {
private token: string;
private fetcher: WebFetcher;

constructor(token: string, fetcher: WebFetcher) {
this.token = token;
this.fetcher = fetcher;
}

public async getTasks(filter?: string): Promise<Task[]> {
let path = "/tasks";

if (filter !== undefined) {
path += `?filter=${encodeURIComponent(filter)}`;
}

const response = await this.do(path, "GET");

return camelize(JSON.parse(response.body)) as Task[];
}

public async createTask(content: string, options?: CreateTaskParams): Promise<void> {
const body = snakeize({ content: content, ...(options ?? {}) });
await this.do("/tasks", "POST", body);
}

public async closeTask(id: TaskId): Promise<void> {
await this.do(`/tasks/${id}/close`, "POST");
}

public async getProjects(): Promise<Project[]> {
const response = await this.do("/projects", "GET");
return camelize(JSON.parse(response.body)) as Project[];
}

public async getSections(): Promise<Section[]> {
const response = await this.do("/sections", "GET");
return camelize(JSON.parse(response.body)) as Section[];
}

public async getLabels(): Promise<Label[]> {
const response = await this.do("/labels", "GET");
return camelize(JSON.parse(response.body)) as Label[];
}

private async do(path: string, method: string, json?: object): Promise<WebResponse> {
const params: RequestParams = {
url: `https://api.todoist.com/rest/v2${path}`,
method: method,
headers: {
"Authorization": `Bearer ${this.token}`,
},
};

if (json !== undefined) {
params.body = JSON.stringify(json);
params.headers["Content-Type"] = "application/json";
}

const response = await this.fetcher.fetch(params);

if (response.statusCode >= 400) {
throw new TodoistApiError(params, response);
}

return response;
}
}

class TodoistApiError extends Error {
constructor(request: RequestParams, response: WebResponse) {
const message = `[${request.method}] ${request.url} returned '${response.statusCode}: ${response.body}`;
super(message)
}
}

File renamed without changes.
File renamed without changes.
File renamed without changes.
9 changes: 5 additions & 4 deletions src/contextMenu.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Menu, Notice } from "obsidian";
import type { Point } from "obsidian";
import type { Task } from "./api/models";
import type { Task } from "./data/task";
import type { TaskId } from "./api/domain/task";

interface TaskContext {
task: Task;
onClickTask: (task: Task) => Promise<void>;
closeTask: (id: TaskId) => Promise<void>;
}

export function showTaskContext(
Expand All @@ -16,7 +17,7 @@ export function showTaskContext(
menuItem
.setTitle("Complete task")
.setIcon("check-small")
.onClick(async () => taskCtx.onClickTask(taskCtx.task))
.onClick(async () => taskCtx.closeTask(taskCtx.task.id))
)
.addItem((menuItem) =>
menuItem
Expand All @@ -32,7 +33,7 @@ export function showTaskContext(
.setIcon("popup-open")
.onClick(() =>
openExternal(
`https://todoist.com/app/project/${taskCtx.task.projectID}/task/${taskCtx.task.id}`
`https://todoist.com/app/project/${taskCtx.task.project.id}/task/${taskCtx.task.id}`
)
)
)
Expand Down
Loading

0 comments on commit a2e7d0a

Please sign in to comment.