diff --git a/Linear.omnifocusjs/Resources/pull.ts b/Linear.omnifocusjs/Resources/pull.ts new file mode 100644 index 0000000..94f053e --- /dev/null +++ b/Linear.omnifocusjs/Resources/pull.ts @@ -0,0 +1,105 @@ +(() => { + let creds = new Credentials(); + + var action = new PlugIn.Action(async () => { + try { + let url = "https://api.linear.app/graphql"; + let req = URL.FetchRequest.fromString(url); + if (req === null) { + throw "could not parse the URL for Linear's API" + } + + ///////////////////////////////////// + // Step 1: make sure we have creds // + ///////////////////////////////////// + let stored = creds.read(url); + + let key = null; + if (stored === null || app.optionKeyDown) { + let credsForm = new Form(); + credsForm.addField(new Form.Field.Password("key", "API Key")); + + await credsForm.show("Please create a personal API key in the Linear settings and paste it here\n(hold option while activating this workflow in the future to reset this)", "Save Key"); + key = (credsForm.values as { key: string }).key; + + creds.write(url, "-", key); + } else { + key = stored.password; + } + + /////////////////////////// + // Step 2: get the tasks // + /////////////////////////// + req.method = "POST"; + req.bodyString = '{"query":"{ viewer { assignedIssues(filter: {state: {type: {nin: [\\"completed\\",\\"canceled\\"]}}}) { nodes { identifier title url team { name } project { name url } } } } }"}'; + req.headers = { + "Content-Type": "application/json", + "Authorization": key, + }; + + let resp = await (req.fetch().catch((err) => { + console.error("Problem fetching tasks:", err); + let alert = new Alert("Problem fetching from Linear", err); + alert.show(); + throw err; + })); + + if (resp.bodyString === null) { + throw "body string was null. Did the request succeed?" + } + + let body = JSON.parse(resp.bodyString); + + ////////////////////////////////// + // Step 3: make the tasks in OF // + ////////////////////////////////// + let toFocus: Array = []; + for (let linearTask of body.data.viewer.assignedIssues.nodes) { + let teamsTag = flattenedTags.byName("teams") || new Tag("teams"); + let teamTag = teamsTag.tagNamed(linearTask.team.name) || new Tag(linearTask.team.name, teamsTag); + + let linearTag = flattenedTags.byName("from Linear") || new Tag("from Linear"); + + let projectName = `${linearTask.team.name} Non-Project Tasks`; + if (linearTask.project !== null) { + projectName = linearTask.project.name; + } + + let project = flattenedProjects.byName(projectName) || new Project(projectName); + project.addTag(teamTag); + project.addTag(linearTag); + project.containsSingletonActions = true; + toFocus.push(project); + if (linearTask.project && project.note.indexOf(linearTask.project.url) === -1) { + if (project.note !== "") { + project.appendStringToNote(`\n\n${linearTask.project.url}`); + } else { + project.appendStringToNote(linearTask.project.url) + } + } + + let taskName = `${linearTask.identifier}: ${linearTask.title}`; + let task = project.taskNamed(taskName) || new Task(taskName, project); + task.addTag(teamTag); + task.addTag(linearTag); + if (task.note.indexOf(linearTask.url) === -1) { + if (task.note !== "") { + task.appendStringToNote(`\n\n${linearTask.url}`); + } else { + task.appendStringToNote(linearTask.url) + } + } + } + + if (app.platformName === "macOS") { + document.windows[0].perspective = Perspective.BuiltIn.Projects; + document.windows[0].focus = toFocus as SectionArray; + } + } catch (err) { + console.error(err); + throw err; + } + }); + + return action; +})(); diff --git a/types/omnifocus-globals.d.ts b/types/omnifocus-globals.d.ts index 9c70527..a6e4501 100644 --- a/types/omnifocus-globals.d.ts +++ b/types/omnifocus-globals.d.ts @@ -1,3 +1,9 @@ +declare const app: Application + declare const console: Console declare const document: DatabaseDocument + +declare const flattenedProjects : ProjectArray + +declare const flattenedTags : TagArray diff --git a/types/omnifocus.d.ts b/types/omnifocus.d.ts index dc2f6f9..e624cd0 100644 --- a/types/omnifocus.d.ts +++ b/types/omnifocus.d.ts @@ -16,7 +16,7 @@ declare class Alert { constructor (title: string, message: string); - show(callback: Function | null): Promise; + show(callback?: Function | null): Promise; addOption(string: string); } @@ -199,7 +199,7 @@ declare class Console { declare class Credentials { constructor (); - read(service: string): object | null; + read(service: string): { user: string, password: string } | null; write(service: string, username: string, password: string); remove(service: string); readBookmark(service: string): URL.Bookmark | null; @@ -319,7 +319,7 @@ declare namespace Tag { } declare class Tag extends ActiveObject { - constructor (name: string, position: Tag | Tag.ChildInsertionLocation | null); + constructor (name: string, position?: Tag | Tag.ChildInsertionLocation | null); tagNamed(name: string): Tag | null; childNamed(name: string): Tag | null; apply(f: Function): ApplyResult | null; @@ -447,7 +447,7 @@ declare namespace Project { } declare class Project extends DatabaseObject { - constructor (name: string, position: Folder | Folder.ChildInsertionLocation | null); + constructor (name: string, position?: Folder | Folder.ChildInsertionLocation | null); taskNamed(name: string): Task | null; appendStringToNote(stringToAppend: string); addAttachment(attachment: FileWrapper); @@ -798,7 +798,7 @@ declare namespace Form.Field { declare namespace Form.Field { class Password extends Form.Field { - constructor (key: string, displayName: string | null, value: string | null); + constructor (key: string, displayName?: string | null, value?: string | null); } }