Skip to content
Merged
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
12 changes: 3 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "taskade",
"version": "1.0.6",
"version": "1.1.0",
"description": "Taskade is a collaboration platform for remote teams to organize and manage projects.",
"main": "index.js",
"scripts": {
Expand All @@ -14,26 +14,20 @@
"npm": ">=5.6.0"
},
"dependencies": {
"@apollo/client": "^3.6.9",
"@taskade/eslint-plugin": "0.4.0",
"cross-fetch": "^3.1.5",
"graphql": "^16.6.0",
"lodash": "^4.17.21",
"moment": "^2.29.1",
"moment-timezone": "^0.5.37",
"uuid": "^9.0.0",
"zapier-platform-core": "17.4.0"
},
"devDependencies": {
"@types/jest": "^26.0.23",
"@types/lodash": "^4.17.13",
"@types/luxon": "^3.0.1",
"@types/node": "^18.12.0",
"@types/node-fetch": "^2.6.12",
"@types/uuid": "^8.3.4",
"jest": "^26.6.3",
"rimraf": "^3.0.2",
"typescript": "5.3.3"
"typescript": "5.3.3",
"zapier-platform-schema": "17.4.0"
},
"private": true,
"zapier": {
Expand Down
49 changes: 49 additions & 0 deletions src/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Bundle, ZObject } from 'zapier-platform-core';

export const BASE_V1 = 'https://www.taskade.com/api/v1';
export const BASE_V2 = 'https://www.taskade.com/api/v2';

export interface ApiResponse {
ok?: boolean;
message?: string;
item?: Record<string, unknown>;
items?: Array<Record<string, unknown>>;
[key: string]: unknown;
}

interface RequestOptions {
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
/** A path relative to the API base (e.g. `/projects/123/tasks`) or a full URL. */
path: string;
version?: 'v1' | 'v2';
body?: unknown;
params?: Record<string, string | undefined>;
}

/**
* Single Taskade request helper used by every create/search. Mirrors the auth +
* header pattern already used in creates/createTask.ts so behaviour is identical.
*/
export const request = async (z: ZObject, bundle: Bundle, opts: RequestOptions) => {
const base = opts.version === 'v2' ? BASE_V2 : BASE_V1;
const url = opts.path.startsWith('http') ? opts.path : `${base}${opts.path}`;
return z.request({
url,
method: opts.method,
headers: {
Accept: 'application/json',
'content-type': 'application/json',
authorization: `Bearer ${bundle.authData.access_token}`,
},
params: opts.params,
body: opts.body != null ? JSON.stringify(opts.body) : undefined,
});
};

/** Throws the Taskade error message when the API returns `{ ok: false }`, else returns the body. */
export const parseOk = (z: ZObject, data: ApiResponse): ApiResponse => {
if (data && data.ok === false) {
throw new z.errors.Error(data.message || 'Taskade API request failed', 'invalid_input', 400);
}
return data;
};
51 changes: 51 additions & 0 deletions src/creates/completeTask.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Bundle, ZObject } from 'zapier-platform-core';

import { ApiResponse, parseOk, request } from '../client';
import { projectField, spaceField, taskField } from '../fields';

const perform = async (z: ZObject, bundle: Bundle) => {
// Zapier may deliver booleans as the strings 'true'/'false', so check both.
const completed = bundle.inputData.completed;
const action = completed === false || completed === 'false' ? 'uncomplete' : 'complete';

const response = await request(z, bundle, {
method: 'POST',
path: `/projects/${bundle.inputData.project_id}/tasks/${bundle.inputData.task_id}/${action}`,
body: {},
});

const data: ApiResponse = parseOk(z, response.json);

return data.item ?? { id: bundle.inputData.task_id, completed: action === 'complete' };
};

export default {
key: 'complete_task',
noun: 'Task',
display: {
label: 'Complete Task',
description: 'Marks a task as complete, or reopens it.',
hidden: false,
},
operation: {
inputFields: [
spaceField(false),
projectField(true),
taskField('task_id', 'Task', true),
{
key: 'completed',
label: 'Mark as Complete',
type: 'boolean',
default: 'true',
required: false,
helpText: 'Set to No to reopen (uncomplete) the task instead.',
},
],
perform,
sample: { id: '099630d4-267e-4b22-894b-08b69f3a4d79', completed: true },
outputFields: [
{ key: 'id', label: 'Task ID' },
{ key: 'completed', type: 'boolean' },
],
},
};
56 changes: 56 additions & 0 deletions src/creates/createProject.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { Bundle, ZObject } from 'zapier-platform-core';

import { ApiResponse, parseOk, request } from '../client';
import { spaceField } from '../fields';

const perform = async (z: ZObject, bundle: Bundle) => {
const response = await request(z, bundle, {
method: 'POST',
path: '/projects',
body: {
folderId: bundle.inputData.space_id,
contentType: (bundle.inputData.content_type as string) ?? 'text/markdown',
content: bundle.inputData.content as string,
},
});

const data: ApiResponse = parseOk(z, response.json);

return data.item ?? data;
};

export default {
key: 'create_project',
noun: 'Project',
display: {
label: 'Create Project',
description: 'Creates a new project from Markdown content in a folder.',
hidden: false,
},
operation: {
inputFields: [
{ ...spaceField(true), label: 'Folder' },
{
key: 'content',
label: 'Content',
type: 'text',
required: true,
helpText: 'Markdown content for the project, e.g. a title line followed by tasks.',
},
{
key: 'content_type',
label: 'Format',
type: 'string',
choices: { 'text/markdown': 'Markdown', 'text/plain': 'Plain text' },
default: 'text/markdown',
required: false,
},
],
perform,
sample: { id: '8hoA5PtYfKroDifZ', name: 'New Project' },
outputFields: [
{ key: 'id', label: 'Project ID' },
{ key: 'name', label: 'Name' },
],
},
};
49 changes: 49 additions & 0 deletions src/creates/createProjectFromTemplate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Bundle, ZObject } from 'zapier-platform-core';

import { ApiResponse, parseOk, request } from '../client';
import { spaceField } from '../fields';

const perform = async (z: ZObject, bundle: Bundle) => {
const response = await request(z, bundle, {
method: 'POST',
path: '/projects/from-template',
body: {
folderId: bundle.inputData.space_id,
templateId: bundle.inputData.template_id,
},
});

const data: ApiResponse = parseOk(z, response.json);

return data.item ?? data;
};

export default {
key: 'create_project_from_template',
noun: 'Project',
display: {
label: 'Create Project from Template',
description: 'Creates a new project from an existing template in a folder.',
hidden: false,
},
operation: {
inputFields: [
{ ...spaceField(true), label: 'Folder', altersDynamicFields: true },
{
key: 'template_id',
label: 'Template',
type: 'string',
dynamic: 'get_all_project_templates.id.title',
required: true,
list: false,
altersDynamicFields: false,
},
],
perform,
sample: { id: '8hoA5PtYfKroDifZ', name: 'New Project from Template' },
outputFields: [
{ key: 'id', label: 'Project ID' },
{ key: 'name', label: 'Name' },
],
},
};
67 changes: 67 additions & 0 deletions src/creates/customApiCall.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { Bundle, ZObject } from 'zapier-platform-core';

const perform = async (z: ZObject, bundle: Bundle) => {
const raw = bundle.inputData.url as string;
const url = raw.startsWith('http')
? raw
: `https://www.taskade.com${raw.startsWith('/') ? '' : '/'}${raw}`;

const method =
(bundle.inputData.http_method as 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH') || 'GET';

const response = await z.request({
url,
method,
headers: {
Accept: 'application/json',
'content-type': 'application/json',
authorization: `Bearer ${bundle.authData.access_token}`,
},
body: bundle.inputData.body ? (bundle.inputData.body as string) : undefined,
// Don't auto-throw on 4xx/5xx: this escape hatch must surface the real
// status + error body so power users can inspect failed calls.
skipThrowForStatus: true,
});

return { status: response.status, data: response.json ?? null };
};

export default {
key: 'custom_api_call',
noun: 'API Call',
display: {
label: 'Custom API Call',
description:
'Make an authenticated request to any Taskade API endpoint. Advanced — see developer.taskade.com.',
hidden: false,
},
operation: {
inputFields: [
{
key: 'http_method',
label: 'Method',
type: 'string',
choices: { GET: 'GET', POST: 'POST', PUT: 'PUT', DELETE: 'DELETE', PATCH: 'PATCH' },
default: 'GET',
required: true,
},
{
key: 'url',
label: 'URL or Path',
type: 'string',
required: true,
helpText: 'A full URL, or a path such as `/api/v1/me/projects`.',
},
{
key: 'body',
label: 'Body',
type: 'text',
required: false,
helpText: 'Raw JSON request body (for POST/PUT/PATCH).',
},
],
perform,
sample: { status: 200, data: {} },
outputFields: [{ key: 'status', label: 'HTTP Status', type: 'integer' }],
},
};
34 changes: 34 additions & 0 deletions src/creates/deleteTask.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Bundle, ZObject } from 'zapier-platform-core';

import { parseOk, request } from '../client';
import { projectField, spaceField, taskField } from '../fields';

const perform = async (z: ZObject, bundle: Bundle) => {
const response = await request(z, bundle, {
method: 'DELETE',
path: `/projects/${bundle.inputData.project_id}/tasks/${bundle.inputData.task_id}`,
});

parseOk(z, response.json);

return { id: bundle.inputData.task_id as string, deleted: true };
};

export default {
key: 'delete_task',
noun: 'Task',
display: {
label: 'Delete Task',
description: 'Deletes a task from a project.',
hidden: false,
},
operation: {
inputFields: [spaceField(false), projectField(true), taskField('task_id', 'Task', true)],
perform,
sample: { id: '099630d4-267e-4b22-894b-08b69f3a4d79', deleted: true },
outputFields: [
{ key: 'id', label: 'Task ID' },
{ key: 'deleted', type: 'boolean' },
],
},
};
Loading