diff --git a/packages/backend/src/apps/todoist/actions/create-task/index.ts b/packages/backend/src/apps/todoist/actions/create-task/index.ts new file mode 100644 index 00000000..257d847c --- /dev/null +++ b/packages/backend/src/apps/todoist/actions/create-task/index.ts @@ -0,0 +1,100 @@ +import defineAction from '../../../../helpers/define-action'; + +export default defineAction({ + name: 'Create Task', + key: 'createTask', + description: 'Creates a Task in Todoist', + arguments: [ + { + label: 'Project ID', + key: 'projectId', + type: 'dropdown' as const, + required: false, + variables: false, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listProjects', + }, + ], + }, + }, + { + label: 'Section ID', + key: 'sectionId', + type: 'dropdown' as const, + required: false, + variables: false, + dependsOn: ['parameters.projectId'], + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listSections', + }, + { + name: 'parameters.projectId', + value: '{parameters.projectId}', + }, + ], + }, + }, + { + label: 'Labels', + key: 'labels', + type: 'string' as const, + required: false, + variables: true, + description: + 'Labels to add to task (comma separated). Examples: "work" "work,imported"', + }, + { + label: 'Content', + key: 'content', + type: 'string' as const, + required: true, + variables: true, + description: + 'Task content, may be markdown. Example: "Foo"', + }, + { + label: 'Description', + key: 'description', + type: 'string' as const, + required: false, + variables: true, + description: + 'Task description, may be markdown. Example: "Foo"', + }, + ], + + async run($) { + const requestPath = `/tasks`; + const { + projectId, + sectionId, + labels, + content, + description + } = $.step.parameters; + + const labelsArray = (labels as string).split(',') + + const payload = { + content, + description: description || null, + project_id: projectId || null, + labels: labelsArray || null, + section_id: sectionId || null, + } + + const response = await $.http.post(requestPath, payload); + + $.setActionItem({ raw: response.data }); + }, +}); diff --git a/packages/backend/src/apps/todoist/actions/index.ts b/packages/backend/src/apps/todoist/actions/index.ts new file mode 100644 index 00000000..14f24a09 --- /dev/null +++ b/packages/backend/src/apps/todoist/actions/index.ts @@ -0,0 +1,3 @@ +import createTask from './create-task'; + +export default [createTask]; diff --git a/packages/backend/src/apps/todoist/assets/favicon.svg b/packages/backend/src/apps/todoist/assets/favicon.svg new file mode 100644 index 00000000..679cdc63 --- /dev/null +++ b/packages/backend/src/apps/todoist/assets/favicon.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/backend/src/apps/todoist/auth/index.ts b/packages/backend/src/apps/todoist/auth/index.ts new file mode 100644 index 00000000..fdf3d552 --- /dev/null +++ b/packages/backend/src/apps/todoist/auth/index.ts @@ -0,0 +1,34 @@ +import verifyCredentials from './verify-credentials'; +import isStillVerified from './is-still-verified'; + +export default { + fields: [ + { + key: 'screenName', + label: 'Screen Name', + type: 'string' as const, + required: true, + readOnly: false, + value: null, + placeholder: null, + description: + 'Name your connection (only used for Automatisch UI).', + clickToCopy: false, + }, + { + key: 'apiToken', + label: 'API Token', + type: 'string' as const, + required: true, + readOnly: false, + value: null, + placeholder: null, + description: + 'Your Todoist API token. See https://todoist.com/app/settings/integrations/developer', + clickToCopy: false, + }, + ], + + verifyCredentials, + isStillVerified, +}; diff --git a/packages/backend/src/apps/todoist/auth/is-still-verified.ts b/packages/backend/src/apps/todoist/auth/is-still-verified.ts new file mode 100644 index 00000000..66bb963e --- /dev/null +++ b/packages/backend/src/apps/todoist/auth/is-still-verified.ts @@ -0,0 +1,9 @@ +import { IGlobalVariable } from '@automatisch/types'; +import verifyCredentials from './verify-credentials'; + +const isStillVerified = async ($: IGlobalVariable) => { + await verifyCredentials($); + return true; +}; + +export default isStillVerified; diff --git a/packages/backend/src/apps/todoist/auth/verify-credentials.ts b/packages/backend/src/apps/todoist/auth/verify-credentials.ts new file mode 100644 index 00000000..7b8c386a --- /dev/null +++ b/packages/backend/src/apps/todoist/auth/verify-credentials.ts @@ -0,0 +1,7 @@ +import { IGlobalVariable } from '@automatisch/types'; + +const verifyCredentials = async ($: IGlobalVariable) => { + await $.http.get('/projects'); +}; + +export default verifyCredentials; diff --git a/packages/backend/src/apps/todoist/common/add-auth-header.ts b/packages/backend/src/apps/todoist/common/add-auth-header.ts new file mode 100644 index 00000000..dfb6a786 --- /dev/null +++ b/packages/backend/src/apps/todoist/common/add-auth-header.ts @@ -0,0 +1,12 @@ +import { TBeforeRequest } from '@automatisch/types'; + +const addAuthHeader: TBeforeRequest = ($, requestConfig) => { + if ($.auth.data?.apiToken) { + const authorizationHeader = `Bearer ${$.auth.data.apiToken}`; + requestConfig.headers.Authorization = authorizationHeader; + } + + return requestConfig; +}; + +export default addAuthHeader; diff --git a/packages/backend/src/apps/todoist/dynamic-data/index.ts b/packages/backend/src/apps/todoist/dynamic-data/index.ts new file mode 100644 index 00000000..cc6c6000 --- /dev/null +++ b/packages/backend/src/apps/todoist/dynamic-data/index.ts @@ -0,0 +1,5 @@ +import listProjects from './list-projects'; +import listSections from './list-sections'; +import listLabels from './list-labels'; + +export default [listProjects, listSections, listLabels]; diff --git a/packages/backend/src/apps/todoist/dynamic-data/list-labels/index.ts b/packages/backend/src/apps/todoist/dynamic-data/list-labels/index.ts new file mode 100644 index 00000000..46fb5257 --- /dev/null +++ b/packages/backend/src/apps/todoist/dynamic-data/list-labels/index.ts @@ -0,0 +1,19 @@ +import { IGlobalVariable } from '@automatisch/types'; + +export default { + name: 'List labels', + key: 'listLabels', + + async run($: IGlobalVariable) { + const response = await $.http.get('/labels'); + + response.data = response.data.map((label: { name: string }) => { + return { + value: label.name, + name: label.name, + }; + }); + + return response; + }, +}; diff --git a/packages/backend/src/apps/todoist/dynamic-data/list-projects/index.ts b/packages/backend/src/apps/todoist/dynamic-data/list-projects/index.ts new file mode 100644 index 00000000..ca10bf72 --- /dev/null +++ b/packages/backend/src/apps/todoist/dynamic-data/list-projects/index.ts @@ -0,0 +1,19 @@ +import { IGlobalVariable } from '@automatisch/types'; + +export default { + name: 'List projects', + key: 'listProjects', + + async run($: IGlobalVariable) { + const response = await $.http.get('/projects'); + + response.data = response.data.map((project: { id: string, name: string }) => { + return { + value: project.id, + name: project.name, + }; + }); + + return response; + }, +}; diff --git a/packages/backend/src/apps/todoist/dynamic-data/list-sections/index.ts b/packages/backend/src/apps/todoist/dynamic-data/list-sections/index.ts new file mode 100644 index 00000000..cb4c23d9 --- /dev/null +++ b/packages/backend/src/apps/todoist/dynamic-data/list-sections/index.ts @@ -0,0 +1,23 @@ +import { IGlobalVariable } from '@automatisch/types'; + +export default { + name: 'List sections', + key: 'listSections', + + async run($: IGlobalVariable) { + const params = { + project_id: ($.step.parameters.projectId as string), + }; + + const response = await $.http.get('/sections', {params}); + + response.data = response.data.map((section: { id: string, name: string }) => { + return { + value: section.id, + name: section.name, + }; + }); + + return response; + }, +}; diff --git a/packages/backend/src/apps/todoist/index.d.ts b/packages/backend/src/apps/todoist/index.d.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/backend/src/apps/todoist/index.ts b/packages/backend/src/apps/todoist/index.ts new file mode 100644 index 00000000..a1850597 --- /dev/null +++ b/packages/backend/src/apps/todoist/index.ts @@ -0,0 +1,22 @@ +import defineApp from '../../helpers/define-app'; +import addAuthHeader from './common/add-auth-header'; +import auth from './auth'; +import triggers from './triggers'; +import actions from './actions'; +import dynamicData from './dynamic-data'; + +export default defineApp({ + name: 'Todoist', + key: 'todoist', + iconUrl: '{BASE_URL}/apps/todoist/assets/favicon.svg', + authDocUrl: 'https://automatisch.io/docs/apps/todoist/connection', + supportsConnections: true, + baseUrl: 'https://todoist.com', + apiBaseUrl: 'https://api.todoist.com/rest/v2', + primaryColor: 'e44332', + beforeRequest: [addAuthHeader], + auth, + triggers, + actions, + dynamicData, +}); diff --git a/packages/backend/src/apps/todoist/triggers/get-tasks/get-tasks.ts b/packages/backend/src/apps/todoist/triggers/get-tasks/get-tasks.ts new file mode 100644 index 00000000..089d02fe --- /dev/null +++ b/packages/backend/src/apps/todoist/triggers/get-tasks/get-tasks.ts @@ -0,0 +1,30 @@ +import { IGlobalVariable } from '@automatisch/types'; + +const getActiveTasks = async ($: IGlobalVariable) => { + + const params = { + project_id: ($.step.parameters.projectId as string)?.trim(), + section_id: ($.step.parameters.sectionId as string)?.trim(), + label: ($.step.parameters.label as string)?.trim(), + filter: ($.step.parameters.filter as string)?.trim(), + }; + + const response = await $.http.get('/tasks', { params }); + + // todoist api doesn't offer sorting, so we inverse sort on id here + response.data.sort((a: { id: number; }, b: { id: number; }) => { + return b.id - a.id; + }) + + for (const task of response.data) { + $.pushTriggerItem({ + raw: task, + meta:{ + internalId: task.id as string, + } + }); + } +}; + + +export default getActiveTasks; diff --git a/packages/backend/src/apps/todoist/triggers/get-tasks/index.ts b/packages/backend/src/apps/todoist/triggers/get-tasks/index.ts new file mode 100644 index 00000000..29ea1ac3 --- /dev/null +++ b/packages/backend/src/apps/todoist/triggers/get-tasks/index.ts @@ -0,0 +1,80 @@ +import defineTrigger from '../../../../helpers/define-trigger'; +import getActiveTasks from './get-tasks'; + +export default defineTrigger({ + name: 'Get Active Tasks', + key: 'getActiveTasks', + pollInterval: 15, + description: 'Triggers when new Task(s) are found', + arguments: [ + { + label: 'Project ID', + key: 'projectId', + type: 'dropdown' as const, + required: false, + variables: false, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listProjects', + }, + ], + }, + }, + { + label: 'Section ID', + key: 'sectionId', + type: 'dropdown' as const, + required: false, + variables: false, + dependsOn: ['parameters.projectId'], + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listSections', + }, + { + name: 'parameters.projectId', + value: '{parameters.projectId}', + }, + ], + }, + }, + { + label: 'Label', + key: 'label', + type: 'dropdown' as const, + required: false, + variables: false, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listLabels', + }, + ], + }, + }, + { + label: 'Filter', + key: 'filter', + type: 'string' as const, + required: false, + variables: false, + description: + 'Limit queried tasks to this filter. Example: "Meeting & today"', + }, + ], + + async run($) { + await getActiveTasks($); + }, +}); diff --git a/packages/backend/src/apps/todoist/triggers/index.ts b/packages/backend/src/apps/todoist/triggers/index.ts new file mode 100644 index 00000000..a52a15d7 --- /dev/null +++ b/packages/backend/src/apps/todoist/triggers/index.ts @@ -0,0 +1,3 @@ +import getTasks from './get-tasks'; + +export default [getTasks]; diff --git a/packages/docs/pages/apps/todoist/actions.md b/packages/docs/pages/apps/todoist/actions.md new file mode 100644 index 00000000..6f433845 --- /dev/null +++ b/packages/docs/pages/apps/todoist/actions.md @@ -0,0 +1,14 @@ +--- +favicon: /favicons/todoist.svg +items: + - name: Get Tasks + desc: Finds tasks in Todoist, optionally matching specified parameters. + - name: Create Task + desc: Creates a task in Todoist. +--- + + + + diff --git a/packages/docs/pages/apps/todoist/connection.md b/packages/docs/pages/apps/todoist/connection.md new file mode 100644 index 00000000..6e6366ed --- /dev/null +++ b/packages/docs/pages/apps/todoist/connection.md @@ -0,0 +1,10 @@ +# Todoist + +:::info +This page explains the steps you need to follow to set up the Todoist connection in Automatisch. If any of the steps are outdated, please let us know! +::: + +1. Go to the account [Integrations page](https://todoist.com/app/settings/integrations/developer) to copy your **API token**. +1. Paste the **API token** value into Automatisch. +1. Enter a memorable name for your connection in the **Screen Name** field. +1. Click the **Submit** button on Automatisch. diff --git a/packages/docs/pages/public/favicons/todoist.svg b/packages/docs/pages/public/favicons/todoist.svg new file mode 100644 index 00000000..679cdc63 --- /dev/null +++ b/packages/docs/pages/public/favicons/todoist.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file