diff --git a/components/everhour/actions/create-task/create-task.mjs b/components/everhour/actions/create-task/create-task.mjs new file mode 100644 index 0000000000000..af6ce6cea5bdb --- /dev/null +++ b/components/everhour/actions/create-task/create-task.mjs @@ -0,0 +1,84 @@ +import { STATUS_OPTIONS } from "../../common/constants.mjs"; +import { parseObject } from "../../common/utils.mjs"; +import everhour from "../../everhour.app.mjs"; + +export default { + key: "everhour-create-task", + name: "Create Task", + description: "Creates a new task in Everhour. [See the documentation](https://everhour.docs.apiary.io/)", + version: "0.0.1", + type: "action", + props: { + everhour, + projectId: { + propDefinition: [ + everhour, + "projectId", + ], + }, + name: { + type: "string", + label: "Task Name", + description: "The name of the task to be created.", + }, + sectionId: { + propDefinition: [ + everhour, + "sectionId", + ({ projectId }) => ({ + projectId, + }), + ], + }, + tags: { + propDefinition: [ + everhour, + "tags", + ], + optional: true, + }, + position: { + type: "integer", + label: "Position", + description: "The position of the task", + optional: true, + }, + description: { + type: "string", + label: "Description", + description: "A description of the task", + optional: true, + }, + dueOn: { + type: "string", + label: "Due Date", + description: "The due date of the task. **Format: YYYY-MM-DD**", + optional: true, + }, + status: { + type: "string", + label: "Status", + description: "The status of the task", + options: STATUS_OPTIONS, + optional: true, + }, + }, + async run({ $ }) { + const response = await this.everhour.createTask({ + $, + projectId: this.projectId, + data: { + name: this.name, + section: this.sectionId, + tags: this.tags && parseObject(this.tags), + position: this.position, + description: this.description, + dueOn: this.dueOn, + status: this.status, + }, + }); + + $.export("$summary", `Successfully created task with ID: ${response.id}`); + return response; + }, +}; diff --git a/components/everhour/actions/start-timer/start-timer.mjs b/components/everhour/actions/start-timer/start-timer.mjs new file mode 100644 index 0000000000000..81d203782731c --- /dev/null +++ b/components/everhour/actions/start-timer/start-timer.mjs @@ -0,0 +1,52 @@ +import everhour from "../../everhour.app.mjs"; + +export default { + key: "everhour-start-timer", + name: "Start Timer", + description: "Begins a new timer for a task. [See the documentation](https://everhour.docs.apiary.io/#reference/0/timers/start-timer)", + version: "0.0.1", + type: "action", + props: { + everhour, + projectId: { + propDefinition: [ + everhour, + "projectId", + ], + }, + taskId: { + propDefinition: [ + everhour, + "taskId", + ({ projectId }) => ({ + projectId, + }), + ], + }, + userDate: { + type: "string", + label: "User Date", + description: "Date string to associate with the timer. Format as 'YYYY-MM-DD'", + optional: true, + }, + comment: { + type: "string", + label: "Comment", + description: "An optional comment to associate with the timer", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.everhour.startTimer({ + $, + data: { + task: this.taskId, + userDate: this.userDate, + comment: this.comment, + }, + }); + + $.export("$summary", `Successfully started a timer for task ID: ${this.taskId}`); + return response; + }, +}; diff --git a/components/everhour/actions/stop-timer/stop-timer.mjs b/components/everhour/actions/stop-timer/stop-timer.mjs new file mode 100644 index 0000000000000..e533e1afe3c65 --- /dev/null +++ b/components/everhour/actions/stop-timer/stop-timer.mjs @@ -0,0 +1,17 @@ +import everhour from "../../everhour.app.mjs"; + +export default { + key: "everhour-stop-timer", + name: "Stop Timer", + description: "Halts the current running timer. [See the documentation](https://everhour.docs.apiary.io/#reference/timers/stop-timer)", + version: "0.0.1", + type: "action", + props: { + everhour, + }, + async run({ $ }) { + const response = await this.everhour.stopTimer(); + $.export("$summary", "Successfully stopped the timer"); + return response; + }, +}; diff --git a/components/everhour/common/constants.mjs b/components/everhour/common/constants.mjs new file mode 100644 index 0000000000000..5f6b8b3d178be --- /dev/null +++ b/components/everhour/common/constants.mjs @@ -0,0 +1,12 @@ +export const LIMIT = 100; + +export const STATUS_OPTIONS = [ + { + label: "Open", + value: "open", + }, + { + label: "Closed", + value: "closed", + }, +]; diff --git a/components/everhour/common/utils.mjs b/components/everhour/common/utils.mjs new file mode 100644 index 0000000000000..dcc9cc61f6f41 --- /dev/null +++ b/components/everhour/common/utils.mjs @@ -0,0 +1,24 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; diff --git a/components/everhour/everhour.app.mjs b/components/everhour/everhour.app.mjs index f17704bba377a..c87e3d01422f5 100644 --- a/components/everhour/everhour.app.mjs +++ b/components/everhour/everhour.app.mjs @@ -1,11 +1,176 @@ +import { axios } from "@pipedream/platform"; +import { LIMIT } from "./common/constants.mjs"; + export default { type: "app", app: "everhour", - propDefinitions: {}, + propDefinitions: { + projectId: { + type: "string", + label: "Project ID", + description: "The ID of the project", + async options({ page }) { + const projects = await this.listProjects({ + params: { + limit: LIMIT, + page: page + 1, + }, + }); + + return projects.map(({ + name: label, id: value, + }) => ({ + label, + value, + })); + }, + }, + sectionId: { + type: "string", + label: "Section ID", + description: "The section id of the task", + async options({ projectId }) { + const sections = await this.listSections({ + projectId, + }); + + return sections.map(({ + name: label, id: value, + }) => ({ + label, + value, + })); + }, + }, + tags: { + type: "string[]", + label: "Tag IDs", + description: "The tag ids of the task", + async options() { + const tags = await this.listTags(); + + return tags.map(({ + name: label, id: value, + }) => ({ + label, + value, + })); + }, + }, + labels: { + type: "string[]", + label: "Tags", + description: "An array of tags associated with the task", + async options({ projectId }) { + const sections = await this.listSections({ + projectId, + }); + + return sections.map(({ + name: label, id: value, + }) => ({ + label, + value, + })); + }, + }, + taskId: { + type: "string", + label: "Task ID", + description: "The ID of the task", + async options({ projectId }) { + const tasks = await this.getProjectTasks({ + projectId, + }); + return tasks.map(({ + name: label, id: value, + }) => ({ + label, + value, + })); + }, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://api.everhour.com"; + }, + _headers() { + return { + "X-Api-Key": `${this.$auth.api_token}`, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + listProjects(opts = {}) { + return this._makeRequest({ + path: "/projects", + ...opts, + }); + }, + listSections({ + projectId, opts, + }) { + return this._makeRequest({ + path: `/projects/${projectId}/sections`, + ...opts, + }); + }, + listTags() { + return this._makeRequest({ + path: "/tags", + }); + }, + getProjectTasks({ + projectId, ...opts + }) { + return this._makeRequest({ + path: `/projects/${projectId}/tasks`, + ...opts, + }); + }, + createTask({ + projectId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/projects/${projectId}/tasks`, + ...opts, + }); + }, + startTimer(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/timers", + ...opts, + }); + }, + stopTimer(opts = {}) { + return this._makeRequest({ + method: "DELETE", + path: "/timers/current", + ...opts, + }); + }, + createWebhook(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/hooks", + ...opts, + }); + }, + deleteWebhook(webhookId) { + return this._makeRequest({ + method: "DELETE", + path: `/hooks/${webhookId}`, + }); }, }, }; diff --git a/components/everhour/package.json b/components/everhour/package.json new file mode 100644 index 0000000000000..9093fbb1e87e5 --- /dev/null +++ b/components/everhour/package.json @@ -0,0 +1,18 @@ +{ + "name": "@pipedream/everhour", + "version": "0.1.0", + "description": "Pipedream Everhour Components", + "main": "everhour.app.mjs", + "keywords": [ + "pipedream", + "everhour" + ], + "homepage": "https://pipedream.com/apps/everhour", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +} diff --git a/components/everhour/sources/common/base.mjs b/components/everhour/sources/common/base.mjs new file mode 100644 index 0000000000000..40eeb696c6bd3 --- /dev/null +++ b/components/everhour/sources/common/base.mjs @@ -0,0 +1,58 @@ +import everhour from "../../everhour.app.mjs"; + +export default { + props: { + everhour, + http: { + type: "$.interface.http", + customResponse: true, + }, + db: "$.service.db", + }, + methods: { + _getHookId() { + return this.db.get("hookId"); + }, + _setHookId(hookId) { + this.db.set("hookId", hookId); + }, + getExtraData() { + return {}; + }, + }, + hooks: { + async activate() { + const response = await this.everhour.createWebhook({ + data: { + targetUrl: this.http.endpoint, + events: this.getEventType(), + ...this.getExtraData(), + }, + }); + this._setHookId(response.id); + }, + async deactivate() { + const webhookId = this._getHookId(); + await this.everhour.deleteWebhook(webhookId); + }, + }, + async run({ + body, headers, + }) { + if (headers["x-hook-secret"]) { + return this.http.respond({ + status: 200, + headers: { + "X-Hook-Secret": headers["x-hook-secret"], + }, + }); + } + + const ts = Date.parse(new Date()); + this.$emit(body, { + id: `${body.resource}-${ts}`, + summary: this.getSummary(body), + ts: ts, + }); + }, +}; diff --git a/components/everhour/sources/new-client-instant/new-client-instant.mjs b/components/everhour/sources/new-client-instant/new-client-instant.mjs new file mode 100644 index 0000000000000..5ed199c80ee61 --- /dev/null +++ b/components/everhour/sources/new-client-instant/new-client-instant.mjs @@ -0,0 +1,24 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "everhour-new-client-instant", + name: "New Client (Instant)", + description: "Emit new event when a client is added.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEventType() { + return [ + "api:client:created", + ]; + }, + getSummary(body) { + return `New Client: ${body.payload.data.name}`; + }, + }, + sampleEmit, +}; diff --git a/components/everhour/sources/new-client-instant/test-event.mjs b/components/everhour/sources/new-client-instant/test-event.mjs new file mode 100644 index 0000000000000..31104ffe31bf7 --- /dev/null +++ b/components/everhour/sources/new-client-instant/test-event.mjs @@ -0,0 +1,25 @@ +export default { + "event": "api:client:created", + "payload": { + "id": "9381500", + "data": { + "projects": [], + "id": 9381500, + "name": "Client Name", + "createdAt": "2024-10-22 14:25:29", + "lineItemMask": "%MEMBER% :: %PROJECT% :: for %PERIOD%", + "paymentDueDays": 0, + "reference": "", + "businessDetails": "", + "email": [ + "client@email.com" + ], + "invoicePublicNotes": "", + "excludedLabels": [], + "status": "active", + "enableResourcePlanner": false, + "favorite": false + } + }, + "createdAt": "2024-10-22 14:25:29" +} \ No newline at end of file diff --git a/components/everhour/sources/new-task-instant/new-task-instant.mjs b/components/everhour/sources/new-task-instant/new-task-instant.mjs new file mode 100644 index 0000000000000..385cc425bfb68 --- /dev/null +++ b/components/everhour/sources/new-task-instant/new-task-instant.mjs @@ -0,0 +1,38 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "everhour-new-task-instant", + name: "New Task Created (Instant)", + description: "Emit new event when a task is created.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + projectId: { + propDefinition: [ + common.props.everhour, + "projectId", + ], + }, + }, + methods: { + ...common.methods, + getExtraData() { + return { + project: this.projectId, + }; + }, + getEventType() { + return [ + "api:task:created", + ]; + }, + getSummary(body) { + return `New Task Created: ${body.payload.data.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/everhour/sources/new-task-instant/test-event.mjs b/components/everhour/sources/new-task-instant/test-event.mjs new file mode 100644 index 0000000000000..c324485881340 --- /dev/null +++ b/components/everhour/sources/new-task-instant/test-event.mjs @@ -0,0 +1,24 @@ +export default { + "event": "api:task:created", + "payload": { + "id": "ev:188217209666811", + "data": { + "createdBy": 1362384, + "iteration": "Section Name", + "position": 5, + "projects": [ + "ev:188193235916605" + ], + "section": 1164091, + "comments": 0, + "completed": false, + "id": "ev:188217209666811", + "type": "task", + "name": "Task name", + "status": "open", + "labels": [], + "createdAt": "2024-10-18 14:01:36" + } + }, + "createdAt": "2024-10-18 14:01:36" +} \ No newline at end of file diff --git a/components/everhour/sources/task-time-updated-instant/task-time-updated-instant.mjs b/components/everhour/sources/task-time-updated-instant/task-time-updated-instant.mjs new file mode 100644 index 0000000000000..4bf9034b807b3 --- /dev/null +++ b/components/everhour/sources/task-time-updated-instant/task-time-updated-instant.mjs @@ -0,0 +1,38 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "everhour-task-time-updated-instant", + name: "New Task Time Updated (Instant)", + description: "Emit new event when a task's time spent is modified in Everhour.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + projectId: { + propDefinition: [ + common.props.everhour, + "projectId", + ], + }, + }, + methods: { + ...common.methods, + getExtraData() { + return { + project: this.projectId, + }; + }, + getEventType() { + return [ + "api:time:updated", + ]; + }, + getSummary(body) { + return `Task Time Updated: ${body.payload.data.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/everhour/sources/task-time-updated-instant/test-event.mjs b/components/everhour/sources/task-time-updated-instant/test-event.mjs new file mode 100644 index 0000000000000..5d4caa2116255 --- /dev/null +++ b/components/everhour/sources/task-time-updated-instant/test-event.mjs @@ -0,0 +1,55 @@ +export default { + "event": "api:time:updated", + "payload": { + "id": "ev:188193235916608", + "data": { + "user": 1362384, + "history": [ + { + "id": 370578249, + "time": 60, + "previousTime": 0, + "action": "TIMER", + "source": "internal", + "createdAt": "2024-10-18 13:58:23", + "createdBy": 1362384 + } + ], + "lockReasons": [], + "cost": 42, + "isLocked": false, + "manualTime": 0, + "id": 214588860, + "date": "2024-10-18", + "time": 60, + "timerTime": 60, + "pastDateTime": 0, + "task": { + "createdBy": 1362384, + "position": 4, + "projects": [ + "ev:188193235916605" + ], + "section": 1163621, + "comments": 0, + "completed": false, + "id": "ev:188193235916608", + "type": "task", + "name": "Project Management", + "status": "open", + "labels": [], + "createdAt": "2024-10-15 19:25:59", + "time": { + "total": 60, + "users": { + "1362384": 60 + }, + "timerTime": 60 + } + }, + "createdAt": "2024-10-18 13:58:23", + "costRate": 2500 + } + }, + "createdAt": "2024-10-18 13:59:20" +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cdf597432a0a3..3c5731854acad 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3173,6 +3173,12 @@ importers: components/eventee: specifiers: {} + components/everhour: + specifiers: + '@pipedream/platform': ^3.0.3 + dependencies: + '@pipedream/platform': 3.0.3 + components/eversign: specifiers: '@pipedream/platform': ^1.1.1