Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
jamiebrynes7 committed Sep 1, 2023
1 parent bb6fb4a commit 45c6d5e
Show file tree
Hide file tree
Showing 25 changed files with 707 additions and 527 deletions.
2 changes: 1 addition & 1 deletion src/api/domain/section.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ProjectId } from "./project";
import type { ProjectId } from "./project";

export type SectionId = string;

Expand Down
42 changes: 40 additions & 2 deletions src/api/domain/task.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ProjectId } from "./project";
import { SectionId } from "./section";
import moment from "moment";
import type { ProjectId } from "./project";
import type { SectionId } from "./section";

export type TaskId = string;

Expand Down Expand Up @@ -27,6 +28,43 @@ export type DueDate = {
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 = {
Expand Down
10 changes: 5 additions & 5 deletions src/api/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Label } from "./domain/label";
import { Project } from "./domain/project";
import { Section } from "./domain/section";
import { CreateTaskParams, Task, TaskId } from "./domain/task";
import { RequestParams, WebFetcher, WebResponse } from "./fetcher";
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";

Expand Down
73 changes: 0 additions & 73 deletions src/apiCache.ts

This file was deleted.

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
151 changes: 151 additions & 0 deletions src/data/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import type { TodoistApiClient } from "../api";
import type { Label, LabelId } from "../api/domain/label";
import type { Project, ProjectId } from "../api/domain/project";
import type { Section, SectionId } from "../api/domain/section";
import { Repository } from "./repository";
import type { Task } from "./task";
import type { Task as ApiTask, CreateTaskParams, TaskId } from "../api/domain/task";

type UnsubscribeCallback = () => void;
type Refresh = () => Promise<void>;

type SubscriptionId = number;

type DataAccessor = {
projects: {
all: () => IterableIterator<Project>,
byId: (id: ProjectId) => Project | undefined,
},
sections: {
all: () => IterableIterator<Section>,
byId: (id: SectionId) => Section | undefined,
}
labels: {
all: () => IterableIterator<Label>,
byId: (id: LabelId) => Label | undefined,
}
}

type Actions = {
closeTask: (id: TaskId) => Promise<void>,
createTask: (content: string, params: CreateTaskParams) => Promise<void>,
};

export class TodoistAdapter {

public data: DataAccessor = {
projects: {
all: () => this.projects.iter(),
byId: (id) => this.projects.getById(id),
},
sections: {
all: () => this.sections.iter(),
byId: (id) => this.sections.getById(id),
},
labels: {
all: () => this.labels.iter(),
byId: (id) => this.labels.getById(id),
}
}

public actions: Actions = {
closeTask: async (id) => await this.api.closeTask(id),
createTask: async (content, params) => await this.api.createTask(content, params),
}

private api: TodoistApiClient | undefined;

private readonly projects: Repository<ProjectId, Project>;
private readonly sections: Repository<SectionId, Section>;
private readonly labels: Repository<LabelId, Label>;

private readonly subscriptions: Map<SubscriptionId, Refresh> = new Map();
private subscriptionIdGen: Generator<SubscriptionId> = subscriptionIdGenerator();

constructor() {
this.projects = new Repository(() => this.api.getProjects());
this.sections = new Repository(() => this.api.getSections());
this.labels = new Repository(() => this.api.getLabels());
}

public async initialize(api: TodoistApiClient) {
this.api = api;
await this.sync();
}

public isReady(): boolean {
return this.api !== undefined;
}

public async sync(): Promise<void> {
if (!this.isReady()) {
return;
}

this.projects.sync();
this.sections.sync();
this.labels.sync();

for (const refresh of this.subscriptions.values()) {
await refresh();
}
}

public subscribe(query: string, callback: (tasks: Task[]) => void): [UnsubscribeCallback, Refresh] {
const id = this.subscriptionIdGen.next().value;
const refresh = this.buildRefresher(query, callback);
this.subscriptions.set(id, refresh);

return [
() => { this.subscriptions.delete(id) },
refresh
];
}

private buildRefresher(query: string, callback: (tasks: Task[]) => void): Refresh {
return async () => {
if (!this.isReady()) {
return;
}
try {
const data = await this.api.getTasks(query);
const hydrated = data.map(t => this.hydrate(t));

callback(hydrated);
}
catch (error: any) {
// TODO: Error handling
}
};
}

private hydrate(apiTask: ApiTask): Task {
const project = this.projects.getById(apiTask.projectId);
const section = apiTask.sectionId ? this.sections.getById(apiTask.sectionId) : undefined;

return {
id: apiTask.id,

content: apiTask.content,
description: apiTask.description,

project: project,
section: section,
parentId: apiTask.parentId,

labels: apiTask.labels,
priority: apiTask.priority,

due: apiTask.due,
order: apiTask.order
};
}
}

function* subscriptionIdGenerator(): Generator<SubscriptionId> {
let next = 0;

while (true) {
yield next++;
}
}
29 changes: 29 additions & 0 deletions src/data/repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
export class Repository<T, U extends { id: T }> {
private readonly data: Map<T, U> = new Map();
private readonly fetchData: () => Promise<U[]>;

constructor(refreshData: () => Promise<U[]>) {
this.fetchData = refreshData;
}

public async sync(): Promise<void> {
try {
const items = await this.fetchData();

this.data.clear();
for (const elem of items) {
this.data.set(elem.id, elem);
}
} catch (error: any) {
// TODO: Error handling
}
}

public getById(id: T): U | undefined {
return this.data.get(id);
}

public iter(): IterableIterator<U> {
return this.data.values();
}
}
20 changes: 20 additions & 0 deletions src/data/task.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { Project } from "../api/domain/project";
import type { Section } from "../api/domain/section";
import type { DueDate, Priority, TaskId } from "../api/domain/task";

export type Task = {
id: TaskId,

content: string,
description: string,

project?: Project,
section?: Section,
parentId?: TaskId,

labels: string[],
priority: Priority,

due?: DueDate,
order: number,
};
Loading

0 comments on commit 45c6d5e

Please sign in to comment.