From 197eede752fd148599f5188200d62105b6f04fb2 Mon Sep 17 00:00:00 2001 From: Luan Cazarine Date: Mon, 30 Dec 2024 15:40:58 -0300 Subject: [PATCH 1/7] richpanel init --- .../add-ticket-message/add-ticket-message.mjs | 36 ++ .../actions/create-ticket/create-ticket.mjs | 67 +++ .../update-ticket-status.mjs | 30 ++ components/richpanel/package.json | 2 +- components/richpanel/richpanel.app.mjs | 387 +++++++++++++++++- .../sources/new-message/new-message.mjs | 88 ++++ .../new-ticket-status-change.mjs | 104 +++++ .../sources/new-ticket/new-ticket.mjs | 115 ++++++ 8 files changed, 825 insertions(+), 4 deletions(-) create mode 100644 components/richpanel/actions/add-ticket-message/add-ticket-message.mjs create mode 100644 components/richpanel/actions/create-ticket/create-ticket.mjs create mode 100644 components/richpanel/actions/update-ticket-status/update-ticket-status.mjs create mode 100644 components/richpanel/sources/new-message/new-message.mjs create mode 100644 components/richpanel/sources/new-ticket-status-change/new-ticket-status-change.mjs create mode 100644 components/richpanel/sources/new-ticket/new-ticket.mjs diff --git a/components/richpanel/actions/add-ticket-message/add-ticket-message.mjs b/components/richpanel/actions/add-ticket-message/add-ticket-message.mjs new file mode 100644 index 0000000000000..a5766710f3bd2 --- /dev/null +++ b/components/richpanel/actions/add-ticket-message/add-ticket-message.mjs @@ -0,0 +1,36 @@ +import richpanel from "../../richpanel.app.mjs"; +import { axios } from "@pipedream/platform"; + +export default { + key: "richpanel-add-ticket-message", + name: "Add Ticket Message", + description: "Adds a message to an existing ticket. [See the documentation]()", + version: "0.0.{{ts}}", + type: "action", + props: { + richpanel, + addMessageId: { + propDefinition: [ + richpanel, + "addMessageId", + ], + }, + addMessageBody: { + propDefinition: [ + richpanel, + "addMessageBody", + ], + }, + addMessageSenderType: { + propDefinition: [ + richpanel, + "addMessageSenderType", + ], + }, + }, + async run({ $ }) { + const response = await this.richpanel.addMessageToTicket(); + $.export("$summary", `Added message to ticket ${this.addMessageId} successfully`); + return response; + }, +}; diff --git a/components/richpanel/actions/create-ticket/create-ticket.mjs b/components/richpanel/actions/create-ticket/create-ticket.mjs new file mode 100644 index 0000000000000..682f623b26664 --- /dev/null +++ b/components/richpanel/actions/create-ticket/create-ticket.mjs @@ -0,0 +1,67 @@ +import richpanel from "../../richpanel.app.mjs"; +import { axios } from "@pipedream/platform"; + +export default { + key: "richpanel-create-ticket", + name: "Create Ticket", + description: "Creates a new support ticket in Richpanel. [See the documentation]().", + version: "0.0.{{ts}}", + type: "action", + props: { + richpanel, + id: { + propDefinition: [ + richpanel, + "createId", + ], + }, + status: { + propDefinition: [ + richpanel, + "createStatus", + ], + }, + commentBody: { + propDefinition: [ + richpanel, + "createCommentBody", + ], + }, + commentSenderType: { + propDefinition: [ + richpanel, + "createCommentSenderType", + ], + }, + viaChannel: { + propDefinition: [ + richpanel, + "createViaChannel", + ], + }, + viaSourceFrom: { + propDefinition: [ + richpanel, + "createViaSourceFrom", + ], + }, + viaSourceTo: { + propDefinition: [ + richpanel, + "createViaSourceTo", + ], + }, + tags: { + propDefinition: [ + richpanel, + "createTags", + ], + }, + }, + async run({ $ }) { + const response = await this.richpanel.createTicket(); + + $.export("$summary", `Created ticket ${response.id}`); + return response; + }, +}; diff --git a/components/richpanel/actions/update-ticket-status/update-ticket-status.mjs b/components/richpanel/actions/update-ticket-status/update-ticket-status.mjs new file mode 100644 index 0000000000000..abb7e8848f59b --- /dev/null +++ b/components/richpanel/actions/update-ticket-status/update-ticket-status.mjs @@ -0,0 +1,30 @@ +import richpanel from "../../richpanel.app.mjs"; +import { axios } from "@pipedream/platform"; + +export default { + key: "richpanel-update-ticket-status", + name: "Update Ticket Status", + description: "Updates the status of an existing ticket in Richpanel. [See the documentation]().", + version: "0.0.{{ts}}", + type: "action", + props: { + richpanel, + updateTicketId: { + propDefinition: [ + richpanel, + "updateTicketId", + ], + }, + updateStatus: { + propDefinition: [ + richpanel, + "updateStatus", + ], + }, + }, + async run({ $ }) { + const response = await this.richpanel.updateTicketStatus(); + $.export("$summary", `Updated ticket ${this.updateTicketId} to status ${this.updateStatus}`); + return response; + }, +}; diff --git a/components/richpanel/package.json b/components/richpanel/package.json index ac5f1dd576c7f..a2d4c3c3ac818 100644 --- a/components/richpanel/package.json +++ b/components/richpanel/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/richpanel/richpanel.app.mjs b/components/richpanel/richpanel.app.mjs index 977736076b668..970fee9380fa4 100644 --- a/components/richpanel/richpanel.app.mjs +++ b/components/richpanel/richpanel.app.mjs @@ -1,11 +1,392 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "richpanel", - propDefinitions: {}, + propDefinitions: { + // Event Filters for ticket creation + eventFilterStatus: { + type: "string[]", + label: "Filter by Status", + description: "Filter emitted events by ticket status", + optional: true, + options: [ + { + label: "Open", + value: "OPEN", + }, + { + label: "Closed", + value: "CLOSED", + }, + { + label: "Snoozed", + value: "SNOOZED", + }, + ], + }, + eventFilterPriority: { + type: "string[]", + label: "Filter by Priority", + description: "Filter emitted events by ticket priority", + optional: true, + options: [ + { + label: "Low", + value: "LOW", + }, + { + label: "Medium", + value: "MEDIUM", + }, + { + label: "High", + value: "HIGH", + }, + ], + }, + eventFilterAssignedAgent: { + type: "string[]", + label: "Filter by Assigned Agent", + description: "Filter emitted events by assigned agent", + optional: true, + async options() { + const tickets = await this.paginate(this.listTickets, { + per_page: 100, + }); + const agents = tickets.map((ticket) => ticket.assignee_id).filter(Boolean); + const uniqueAgents = [ + ...new Set(agents), + ]; + return uniqueAgents.map((agent) => ({ + label: agent, + value: agent, + })); + }, + }, + // Event Filters for message events + eventFilterChannel: { + type: "string[]", + label: "Filter by Channel", + description: "Filter emitted events by communication channel", + optional: true, + options: [ + { + label: "Email", + value: "email", + }, + { + label: "Chat", + value: "chat", + }, + ], + }, + // Event Filters for status updates + eventFilterDesiredStatuses: { + type: "string[]", + label: "Desired Statuses to Monitor", + description: "Specify desired statuses to monitor for status updates", + optional: true, + options: [ + { + label: "Open", + value: "OPEN", + }, + { + label: "Pending", + value: "PENDING", + }, + { + label: "Resolved", + value: "RESOLVED", + }, + ], + }, + // Props for creating a ticket + createId: { + type: "string", + label: "Ticket ID", + description: "The ID of the ticket to create", + optional: true, + }, + createStatus: { + type: "string", + label: "Status", + description: "The status of the new ticket", + optional: true, + options: [ + { + label: "Open", + value: "OPEN", + }, + { + label: "Closed", + value: "CLOSED", + }, + { + label: "Snoozed", + value: "SNOOZED", + }, + ], + }, + createCommentBody: { + type: "string", + label: "Comment Body", + description: "The body of the comment for the new ticket", + optional: true, + }, + createCommentSenderType: { + type: "string", + label: "Comment Sender Type", + description: "The sender type of the comment", + optional: true, + options: [ + { + label: "Customer", + value: "customer", + }, + { + label: "Operator", + value: "operator", + }, + ], + }, + createViaChannel: { + type: "string", + label: "Via Channel", + description: "The channel via which the ticket is created", + optional: true, + options: [ + { + label: "Email", + value: "email", + }, + { + label: "Aircall", + value: "aircall", + }, + ], + }, + createViaSourceFrom: { + type: "string", + label: "Via Source From", + description: "The source from which the ticket was created", + optional: true, + }, + createViaSourceTo: { + type: "string", + label: "Via Source To", + description: "The source to which the ticket was created", + optional: true, + }, + createTags: { + type: "string[]", + label: "Tags", + description: "Tags associated with the new ticket", + optional: true, + }, + // Props for adding a message to a ticket + addMessageId: { + type: "string", + label: "Ticket ID", + description: "ID of the ticket to add a message to", + async options() { + const tickets = await this.paginate(this.listTickets, { + per_page: 100, + }); + return tickets.map((ticket) => ({ + label: ticket.subject || ticket.id, + value: ticket.id, + })); + }, + }, + addMessageBody: { + type: "string", + label: "Comment Body", + description: "Body of the comment to add", + }, + addMessageSenderType: { + type: "string", + label: "Comment Sender Type", + description: "The sender type of the comment", + options: [ + { + label: "Customer", + value: "customer", + }, + { + label: "Operator", + value: "operator", + }, + ], + }, + // Props for updating ticket status + updateTicketId: { + type: "string", + label: "Ticket ID", + description: "ID of the ticket to update", + async options() { + const tickets = await this.paginate(this.listTickets, { + per_page: 100, + }); + return tickets.map((ticket) => ({ + label: ticket.subject || ticket.id, + value: ticket.id, + })); + }, + }, + updateStatus: { + type: "string", + label: "Status", + description: "New status of the ticket", + options: [ + { + label: "Open", + value: "OPEN", + }, + { + label: "Closed", + value: "CLOSED", + }, + { + label: "Snoozed", + value: "SNOOZED", + }, + { + label: "Pending", + value: "PENDING", + }, + { + label: "Resolved", + value: "RESOLVED", + }, + ], + }, + }, methods: { - // this.$auth contains connected account data + // Log authentication keys authKeys() { console.log(Object.keys(this.$auth)); }, + // Base URL for Richpanel API + _baseUrl() { + return "https://api.richpanel.com/v1"; + }, + // Make an HTTP request to Richpanel API + async _makeRequest(opts = {}) { + const { + $ = this, method = "GET", path = "/", headers = {}, ...otherOpts + } = opts; + return axios($, { + method, + url: this._baseUrl() + path, + headers: { + ...headers, + "x-richpanel-key": this.$auth.api_key, + "Content-Type": "application/json", + }, + ...otherOpts, + }); + }, + // List all tickets with optional filters + async listTickets(opts = {}) { + const params = {}; + if (this.eventFilterStatus) params.status = this.eventFilterStatus.join(","); + if (this.eventFilterPriority) params.priority = this.eventFilterPriority.join(","); + if (this.eventFilterAssignedAgent) params.assignee_id = this.eventFilterAssignedAgent.join(","); + if (this.eventFilterChannel) params.channel = this.eventFilterChannel.join(","); + if (this.eventFilterDesiredStatuses) params.desired_statuses = this.eventFilterDesiredStatuses.join(","); + return this._makeRequest({ + method: "GET", + path: "/tickets", + params, + }); + }, + // Create a new ticket + async createTicket() { + const ticketData = {}; + if (this.createStatus) ticketData.status = this.createStatus; + if (this.createCommentBody || this.createCommentSenderType) { + ticketData.comment = {}; + if (this.createCommentBody) ticketData.comment.body = this.createCommentBody; + if (this.createCommentSenderType) ticketData.comment.sender_type = this.createCommentSenderType; + } + if (this.createViaChannel || this.createViaSourceFrom || this.createViaSourceTo) { + ticketData.via = {}; + if (this.createViaChannel) ticketData.via.channel = this.createViaChannel; + if (this.createViaSourceFrom || this.createViaSourceTo) { + ticketData.via.source = {}; + if (this.createViaSourceFrom) ticketData.via.source.from = { + address: this.createViaSourceFrom, + }; + if (this.createViaSourceTo) ticketData.via.source.to = { + address: this.createViaSourceTo, + }; + } + } + if (this.createTags && this.createTags.length > 0) { + ticketData.tags = this.createTags; + } + if (this.createId) { + ticketData.id = this.createId; + } + return this._makeRequest({ + method: "POST", + path: "/tickets", + data: { + ticket: ticketData, + }, + }); + }, + // Add a message to an existing ticket + async addMessageToTicket() { + const { + addMessageId, addMessageBody, addMessageSenderType, + } = this; + return this._makeRequest({ + method: "POST", + path: `/tickets/${addMessageId}/comments`, + data: { + comment: { + sender_type: addMessageSenderType, + body: addMessageBody, + }, + }, + }); + }, + // Update the status of an existing ticket + async updateTicketStatus() { + const { + updateTicketId, updateStatus, + } = this; + return this._makeRequest({ + method: "PUT", + path: `/tickets/${updateTicketId}`, + data: { + ticket: { + status: this.updateStatus, + }, + }, + }); + }, + // Pagination helper method + async paginate(fn, opts = {}) { + let results = []; + let page = 1; + let perPage = opts.per_page || 30; + while (true) { + const response = await fn({ + ...opts, + page, + per_page: perPage, + }); + if (!response || response.length === 0) break; + results = results.concat(response); + if (response.length < perPage) break; + page += 1; + } + return results; + }, }, -}; \ No newline at end of file + version: "0.0.{{ts}}", +}; diff --git a/components/richpanel/sources/new-message/new-message.mjs b/components/richpanel/sources/new-message/new-message.mjs new file mode 100644 index 0000000000000..89483a1e65dc7 --- /dev/null +++ b/components/richpanel/sources/new-message/new-message.mjs @@ -0,0 +1,88 @@ +import { + axios, DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, +} from "@pipedream/platform"; +import richpanel from "../../richpanel.app.mjs"; + +export default { + key: "richpanel-new-message", + name: "New Message in Richpanel Ticket", + description: "Emit new event when a customer sends a new message on an existing or new ticket. Optionally filter by channel (e.g., email, chat). [See the documentation]()", + version: "0.0.{{ts}}", + type: "source", + dedupe: "unique", + props: { + richpanel, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + eventFilterChannel: { + propDefinition: [ + richpanel, + "eventFilterChannel", + ], + }, + }, + hooks: { + async deploy() { + const currentTimestamp = Date.now(); + await this.db.set("lastProcessedTimestamp", currentTimestamp); + }, + async activate() { + // Optionally, log authentication keys or set up webhooks if required + this.richpanel.authKeys(); + }, + async deactivate() { + // Optionally, clean up any resources or log deactivation + console.log("Deactivated richpanel-new-message source."); + }, + }, + async run() { + const ticketsResponse = await this.richpanel.listTickets(); + + if (!ticketsResponse || !ticketsResponse.tickets) { + return; + } + + const tickets = ticketsResponse.tickets; + let lastProcessedTimestamp = await this.db.get("lastProcessedTimestamp") || 0; + let newMessages = []; + + for (const ticket of tickets) { + if (ticket.comments && Array.isArray(ticket.comments)) { + for (const comment of ticket.comments) { + const messageTimestamp = Date.parse(comment.created_at) || Date.now(); + + if (messageTimestamp > lastProcessedTimestamp && comment.sender_type === "customer") { + // Optionally, filter by channel + if (this.eventFilterChannel && !this.eventFilterChannel.includes(comment.via.channel)) { + continue; + } + + newMessages.push({ + ...comment, + ticket_id: ticket.id, + }); + } + } + } + } + + if (newMessages.length > 0) { + for (const message of newMessages) { + this.$emit(message, { + id: message.id || message.ts, + summary: `New message on ticket ${message.ticket_id}`, + ts: Date.parse(message.created_at) || Date.now(), + }); + } + + // Update lastProcessedTimestamp to the latest message's timestamp + const latestTimestamp = Math.max(...newMessages.map((msg) => Date.parse(msg.created_at) || Date.now())); + await this.db.set("lastProcessedTimestamp", latestTimestamp); + } + }, +}; diff --git a/components/richpanel/sources/new-ticket-status-change/new-ticket-status-change.mjs b/components/richpanel/sources/new-ticket-status-change/new-ticket-status-change.mjs new file mode 100644 index 0000000000000..9db1fb8edfed5 --- /dev/null +++ b/components/richpanel/sources/new-ticket-status-change/new-ticket-status-change.mjs @@ -0,0 +1,104 @@ +import richpanel from "../../richpanel.app.mjs"; +import { + axios, DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, +} from "@pipedream/platform"; + +export default { + key: "richpanel-new-ticket-status-change", + name: "New Ticket Status Change", + description: "Emit a new event when a ticket's status is updated. [See the documentation]()", + version: "0.0.{{ts}}", + type: "source", + dedupe: "unique", + props: { + richpanel, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + eventFilterDesiredStatuses: { + propDefinition: [ + "richpanel", + "eventFilterDesiredStatuses", + ], + }, + }, + methods: { + async getLastTimestamp() { + return this.db.get("last_timestamp") || new Date(0).toISOString(); + }, + async setLastTimestamp(timestamp) { + await this.db.set("last_timestamp", timestamp); + }, + }, + hooks: { + async deploy() { + const lastTimestamp = await this.getLastTimestamp(); + const tickets = await this.richpanel.listTickets({ + updated_after: lastTimestamp, + ...(this.eventFilterDesiredStatuses + ? { + desired_statuses: this.eventFilterDesiredStatuses.join(","), + } + : {}), + per_page: 50, + sortKey: "updated_at", + order: "DESC", + }); + + tickets.reverse().forEach((ticket) => { + this.$emit(ticket, { + id: ticket.id, + summary: `Ticket ${ticket.id} status changed to ${ticket.status}`, + ts: Date.parse(ticket.updated_at), + }); + }); + + const latestUpdatedAt = + tickets.length > 0 + ? tickets[0].updated_at + : new Date().toISOString(); + await this.setLastTimestamp(latestUpdatedAt); + }, + async activate() { + // No webhook setup for polling source + }, + async deactivate() { + // No webhook teardown for polling source + }, + }, + async run() { + const lastTimestamp = await this.getLastTimestamp(); + const tickets = await this.richpanel.listTickets({ + updated_after: lastTimestamp, + ...(this.eventFilterDesiredStatuses + ? { + desired_statuses: this.eventFilterDesiredStatuses.join(","), + } + : {}), + per_page: 100, + sortKey: "updated_at", + order: "ASC", + }); + + let newLastTimestamp = lastTimestamp; + + tickets.forEach((ticket) => { + if (ticket.updated_at > lastTimestamp) { + this.$emit(ticket, { + id: ticket.id, + summary: `Ticket ${ticket.id} status changed to ${ticket.status}`, + ts: Date.parse(ticket.updated_at), + }); + if (ticket.updated_at > newLastTimestamp) { + newLastTimestamp = ticket.updated_at; + } + } + }); + + await this.setLastTimestamp(newLastTimestamp); + }, +}; diff --git a/components/richpanel/sources/new-ticket/new-ticket.mjs b/components/richpanel/sources/new-ticket/new-ticket.mjs new file mode 100644 index 0000000000000..04f7edc727dee --- /dev/null +++ b/components/richpanel/sources/new-ticket/new-ticket.mjs @@ -0,0 +1,115 @@ +import { + axios, DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, +} from "@pipedream/platform"; +import richpanel from "../../richpanel.app.mjs"; + +export default { + key: "richpanel-new-ticket", + name: "New Support Ticket Created", + description: "Emit a new event when a support ticket is created. [See the documentation]()", + version: "0.0.{{ts}}", + type: "source", + dedupe: "unique", + props: { + richpanel, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + eventFilterStatus: { + propDefinition: [ + "richpanel", + "eventFilterStatus", + ], + }, + eventFilterPriority: { + propDefinition: [ + "richpanel", + "eventFilterPriority", + ], + }, + eventFilterAssignedAgent: { + propDefinition: [ + "richpanel", + "eventFilterAssignedAgent", + ], + }, + }, + hooks: { + async deploy() { + const tickets = await this.richpanel.listTickets({ + page: 1, + per_page: 50, + status: this.eventFilterStatus, + priority: this.eventFilterPriority, + assignee_id: this.eventFilterAssignedAgent, + }); + const sortedTickets = tickets.sort( + (a, b) => new Date(b.created_at) - new Date(a.created_at), + ); + for (const ticket of sortedTickets) { + this.$emit(ticket, { + id: ticket.id || Date.parse(ticket.created_at), + summary: `New Ticket: ${ticket.subject}`, + ts: Date.parse(ticket.created_at) || Date.now(), + }); + } + const latestTicket = tickets[0]; + const lastTimestamp = latestTicket + ? Date.parse(latestTicket.created_at) + : Date.now(); + await this.db.set("lastTimestamp", lastTimestamp); + }, + async activate() { + await this.richpanel.createWebhook({ + url: this.webhookUrl, + event: "ticket_created", + filters: { + status: this.eventFilterStatus, + priority: this.eventFilterPriority, + assignee_id: this.eventFilterAssignedAgent, + }, + }); + }, + async deactivate() { + await this.richpanel.deleteWebhook({ + event: "ticket_created", + }); + }, + }, + async run() { + const lastTimestamp = (await this.db.get("lastTimestamp")) || 0; + const tickets = await this.richpanel.listTickets({ + per_page: 100, + status: this.eventFilterStatus, + priority: this.eventFilterPriority, + assignee_id: this.eventFilterAssignedAgent, + since: new Date(lastTimestamp).toISOString(), + }); + + const newTickets = tickets.filter( + (ticket) => Date.parse(ticket.created_at) > lastTimestamp, + ); + + const sortedNewTickets = newTickets.sort( + (a, b) => new Date(a.created_at) - new Date(b.created_at), + ); + + for (const ticket of sortedNewTickets) { + this.$emit(ticket, { + id: ticket.id || Date.parse(ticket.created_at), + summary: `New Ticket: ${ticket.subject}`, + ts: Date.parse(ticket.created_at) || Date.now(), + }); + } + + if (sortedNewTickets.length > 0) { + const latestTicket = sortedNewTickets[sortedNewTickets.length - 1]; + const newLastTimestamp = Date.parse(latestTicket.created_at); + await this.db.set("lastTimestamp", newLastTimestamp); + } + }, +}; From 70cb6c300ff3b70b27bbbc4ec3bccaaeb8577d4e Mon Sep 17 00:00:00 2001 From: Luan Cazarine Date: Thu, 2 Jan 2025 11:32:54 -0300 Subject: [PATCH 2/7] [Components] richpanel #15105 Sources - New Ticket - New Message - New Ticket Status Change Actions - Create Ticket - Add Ticket Message - Update Ticket Status --- .../add-ticket-message/add-ticket-message.mjs | 30 +- .../actions/create-ticket/create-ticket.mjs | 54 +++- .../update-ticket-status.mjs | 22 +- components/richpanel/common/constants.mjs | 56 ++++ components/richpanel/common/utils.mjs | 24 ++ components/richpanel/package.json | 5 +- components/richpanel/richpanel.app.mjs | 290 +++++++----------- components/richpanel/sources/common/base.mjs | 69 +++++ .../sources/new-message/new-message.mjs | 90 +----- .../sources/new-message/test-event.mjs | 46 +++ .../new-ticket-status-change.mjs | 111 ++----- .../new-ticket-status-change/test-event.mjs | 46 +++ .../sources/new-ticket/new-ticket.mjs | 117 +------ .../sources/new-ticket/test-event.mjs | 46 +++ 14 files changed, 515 insertions(+), 491 deletions(-) create mode 100644 components/richpanel/common/constants.mjs create mode 100644 components/richpanel/common/utils.mjs create mode 100644 components/richpanel/sources/common/base.mjs create mode 100644 components/richpanel/sources/new-message/test-event.mjs create mode 100644 components/richpanel/sources/new-ticket-status-change/test-event.mjs create mode 100644 components/richpanel/sources/new-ticket/test-event.mjs diff --git a/components/richpanel/actions/add-ticket-message/add-ticket-message.mjs b/components/richpanel/actions/add-ticket-message/add-ticket-message.mjs index a5766710f3bd2..b94874258d075 100644 --- a/components/richpanel/actions/add-ticket-message/add-ticket-message.mjs +++ b/components/richpanel/actions/add-ticket-message/add-ticket-message.mjs @@ -1,36 +1,46 @@ import richpanel from "../../richpanel.app.mjs"; -import { axios } from "@pipedream/platform"; export default { key: "richpanel-add-ticket-message", name: "Add Ticket Message", - description: "Adds a message to an existing ticket. [See the documentation]()", + description: "Adds a message to an existing ticket. [See the documentation](https://developer.richpanel.com/reference/update-a-conversation)", version: "0.0.{{ts}}", type: "action", props: { richpanel, - addMessageId: { + conversationId: { propDefinition: [ richpanel, - "addMessageId", + "conversationId", ], }, - addMessageBody: { + commentBody: { propDefinition: [ richpanel, - "addMessageBody", + "commentBody", ], }, - addMessageSenderType: { + commentSenderType: { propDefinition: [ richpanel, - "addMessageSenderType", + "commentSenderType", ], }, }, async run({ $ }) { - const response = await this.richpanel.addMessageToTicket(); - $.export("$summary", `Added message to ticket ${this.addMessageId} successfully`); + const response = await this.richpanel.updateTicket({ + $, + conversationId: this.conversationId, + data: { + ticket: { + comment: { + body: this.commentBody, + sender_type: this.commentSenderType, + }, + }, + }, + }); + $.export("$summary", `Added message to ticket ${this.messageId} successfully`); return response; }, }; diff --git a/components/richpanel/actions/create-ticket/create-ticket.mjs b/components/richpanel/actions/create-ticket/create-ticket.mjs index 682f623b26664..aef19c8874764 100644 --- a/components/richpanel/actions/create-ticket/create-ticket.mjs +++ b/components/richpanel/actions/create-ticket/create-ticket.mjs @@ -1,11 +1,12 @@ +import { ConfigurationError } from "@pipedream/platform"; +import { parseObject } from "../../common/utils.mjs"; import richpanel from "../../richpanel.app.mjs"; -import { axios } from "@pipedream/platform"; export default { key: "richpanel-create-ticket", name: "Create Ticket", - description: "Creates a new support ticket in Richpanel. [See the documentation]().", - version: "0.0.{{ts}}", + description: "Creates a new support ticket in Richpanel. [See the documentation](https://developer.richpanel.com/reference/create-conversation).", + version: "0.0.1", type: "action", props: { richpanel, @@ -14,54 +15,81 @@ export default { richpanel, "createId", ], + optional: true, }, status: { propDefinition: [ richpanel, - "createStatus", + "status", ], }, commentBody: { propDefinition: [ richpanel, - "createCommentBody", + "commentBody", ], }, commentSenderType: { propDefinition: [ richpanel, - "createCommentSenderType", + "commentSenderType", ], }, viaChannel: { propDefinition: [ richpanel, - "createViaChannel", + "viaChannel", ], }, viaSourceFrom: { propDefinition: [ richpanel, - "createViaSourceFrom", + "viaSourceFrom", ], }, viaSourceTo: { propDefinition: [ richpanel, - "createViaSourceTo", + "viaSourceTo", ], + optional: true, }, tags: { propDefinition: [ richpanel, - "createTags", + "tags", ], + optional: true, }, }, async run({ $ }) { - const response = await this.richpanel.createTicket(); + try { + const response = await this.richpanel.createTicket({ + $, + data: { + ticket: { + id: this.id, + status: this.status, + comment: { + body: this.commentBody, + sender_type: this.commentSenderType, + }, + via: { + channel: this.viaChannel, + source: { + from: parseObject(this.viaSourceFrom), + to: parseObject(this.viaSourceTo), + }, + }, + tags: parseObject(this.tags), + }, + }, + }); - $.export("$summary", `Created ticket ${response.id}`); - return response; + $.export("$summary", `Created ticket ${response.ticket.id}`); + return response; + } catch ({ response }) { + throw new ConfigurationError(response?.data?.error?.message); + } }, }; diff --git a/components/richpanel/actions/update-ticket-status/update-ticket-status.mjs b/components/richpanel/actions/update-ticket-status/update-ticket-status.mjs index abb7e8848f59b..8bceed0e6e7e5 100644 --- a/components/richpanel/actions/update-ticket-status/update-ticket-status.mjs +++ b/components/richpanel/actions/update-ticket-status/update-ticket-status.mjs @@ -1,30 +1,36 @@ import richpanel from "../../richpanel.app.mjs"; -import { axios } from "@pipedream/platform"; export default { key: "richpanel-update-ticket-status", name: "Update Ticket Status", - description: "Updates the status of an existing ticket in Richpanel. [See the documentation]().", + description: "Updates the status of an existing ticket in Richpanel. [See the documentation](https://developer.richpanel.com/reference/update-a-conversation).", version: "0.0.{{ts}}", type: "action", props: { richpanel, - updateTicketId: { + conversationId: { propDefinition: [ richpanel, - "updateTicketId", + "conversationId", ], }, - updateStatus: { + status: { propDefinition: [ richpanel, - "updateStatus", + "status", ], }, }, async run({ $ }) { - const response = await this.richpanel.updateTicketStatus(); - $.export("$summary", `Updated ticket ${this.updateTicketId} to status ${this.updateStatus}`); + const response = await this.richpanel.updateTicket({ + $, + data: { + ticket: { + status: this.status, + }, + }, + }); + $.export("$summary", `Updated ticket ${this.conversationId} to status ${this.status}`); return response; }, }; diff --git a/components/richpanel/common/constants.mjs b/components/richpanel/common/constants.mjs new file mode 100644 index 0000000000000..109dcd3cc0e98 --- /dev/null +++ b/components/richpanel/common/constants.mjs @@ -0,0 +1,56 @@ +export const STATUS_OPTIONS = [ + { + label: "Open", + value: "OPEN", + }, + { + label: "Closed", + value: "CLOSED", + }, + { + label: "Snoozed", + value: "SNOOZED", + }, +]; + +export const COMMENT_SENDER_TYPE_OPTIONS = [ + { + label: "Customer", + value: "customer", + }, + { + label: "Operator", + value: "operator", + }, +]; + +export const VIA_CHANNEL_OPTIONS = [ + { + label: "Email", + value: "email", + }, + { + label: "Messenger", + value: "messenger", + }, + { + label: "Facebook Message", + value: "facebook_message", + }, + { + label: "Instagram", + value: "instagram", + }, + { + label: "Aircall", + value: "aircall", + }, + { + label: "Phone", + value: "phone", + }, + { + label: "WhatsApp", + value: "whatsapp", + }, +]; diff --git a/components/richpanel/common/utils.mjs b/components/richpanel/common/utils.mjs new file mode 100644 index 0000000000000..dcc9cc61f6f41 --- /dev/null +++ b/components/richpanel/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/richpanel/package.json b/components/richpanel/package.json index a2d4c3c3ac818..01290215ca3d2 100644 --- a/components/richpanel/package.json +++ b/components/richpanel/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/richpanel", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream Richpanel Components", "main": "richpanel.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" } } diff --git a/components/richpanel/richpanel.app.mjs b/components/richpanel/richpanel.app.mjs index 970fee9380fa4..7e353c0a2a2e8 100644 --- a/components/richpanel/richpanel.app.mjs +++ b/components/richpanel/richpanel.app.mjs @@ -1,15 +1,72 @@ import { axios } from "@pipedream/platform"; +import { + COMMENT_SENDER_TYPE_OPTIONS, + STATUS_OPTIONS, + VIA_CHANNEL_OPTIONS, +} from "./common/constants.mjs"; export default { type: "app", app: "richpanel", propDefinitions: { - // Event Filters for ticket creation + createId: { + type: "string", + label: "Ticket ID", + description: "The ID of the ticket to create", + }, + status: { + type: "string", + label: "Status", + description: "The status of the new ticket", + options: STATUS_OPTIONS, + }, + commentBody: { + type: "string", + label: "Comment Body", + description: "The body of the comment for the ticket", + }, + commentSenderType: { + type: "string", + label: "Comment Sender Type", + description: "The sender type of the comment", + options: COMMENT_SENDER_TYPE_OPTIONS, + }, + viaChannel: { + type: "string", + label: "Via Channel", + description: "The channel via which the ticket is created", + options: VIA_CHANNEL_OPTIONS, + }, + viaSourceFrom: { + type: "object", + label: "Via Source From", + description: "The object source from which the ticket was created. **Examples: {\"address\": \"abc@email.com\"} or {\"id\": \"+16692668044\"}. It depends on the selected channel**.", + }, + viaSourceTo: { + type: "object", + label: "Via Source To", + description: "The object source to which the ticket was created. **Examples: {\"address\": \"abc@email.com\"} or {\"id\": \"+16692668044\"}. It depends on the selected channel**.", + }, + tags: { + type: "string[]", + label: "Tags", + description: "Tags associated with the new ticket.", + async options() { + const { tag } = await this.listTags(); + + return tag.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + eventFilterStatus: { type: "string[]", label: "Filter by Status", description: "Filter emitted events by ticket status", - optional: true, options: [ { label: "Open", @@ -29,7 +86,6 @@ export default { type: "string[]", label: "Filter by Priority", description: "Filter emitted events by ticket priority", - optional: true, options: [ { label: "Low", @@ -49,7 +105,6 @@ export default { type: "string[]", label: "Filter by Assigned Agent", description: "Filter emitted events by assigned agent", - optional: true, async options() { const tickets = await this.paginate(this.listTickets, { per_page: 100, @@ -64,12 +119,10 @@ export default { })); }, }, - // Event Filters for message events eventFilterChannel: { type: "string[]", label: "Filter by Channel", description: "Filter emitted events by communication channel", - optional: true, options: [ { label: "Email", @@ -81,12 +134,10 @@ export default { }, ], }, - // Event Filters for status updates eventFilterDesiredStatuses: { type: "string[]", label: "Desired Statuses to Monitor", description: "Specify desired statuses to monitor for status updates", - optional: true, options: [ { label: "Open", @@ -102,90 +153,6 @@ export default { }, ], }, - // Props for creating a ticket - createId: { - type: "string", - label: "Ticket ID", - description: "The ID of the ticket to create", - optional: true, - }, - createStatus: { - type: "string", - label: "Status", - description: "The status of the new ticket", - optional: true, - options: [ - { - label: "Open", - value: "OPEN", - }, - { - label: "Closed", - value: "CLOSED", - }, - { - label: "Snoozed", - value: "SNOOZED", - }, - ], - }, - createCommentBody: { - type: "string", - label: "Comment Body", - description: "The body of the comment for the new ticket", - optional: true, - }, - createCommentSenderType: { - type: "string", - label: "Comment Sender Type", - description: "The sender type of the comment", - optional: true, - options: [ - { - label: "Customer", - value: "customer", - }, - { - label: "Operator", - value: "operator", - }, - ], - }, - createViaChannel: { - type: "string", - label: "Via Channel", - description: "The channel via which the ticket is created", - optional: true, - options: [ - { - label: "Email", - value: "email", - }, - { - label: "Aircall", - value: "aircall", - }, - ], - }, - createViaSourceFrom: { - type: "string", - label: "Via Source From", - description: "The source from which the ticket was created", - optional: true, - }, - createViaSourceTo: { - type: "string", - label: "Via Source To", - description: "The source to which the ticket was created", - optional: true, - }, - createTags: { - type: "string[]", - label: "Tags", - description: "Tags associated with the new ticket", - optional: true, - }, - // Props for adding a message to a ticket addMessageId: { type: "string", label: "Ticket ID", @@ -220,18 +187,24 @@ export default { }, ], }, - // Props for updating ticket status - updateTicketId: { + conversationId: { type: "string", label: "Ticket ID", description: "ID of the ticket to update", - async options() { - const tickets = await this.paginate(this.listTickets, { - per_page: 100, + async options({ page }) { + const ticket = await this.listTickets({ + params: { + page: page + 1, + }, }); - return tickets.map((ticket) => ({ - label: ticket.subject || ticket.id, - value: ticket.id, + + console.log("ticket: ", ticket); + + return ticket.map(({ + id: value, subject: label, + }) => ({ + label, + value, })); }, }, @@ -264,111 +237,55 @@ export default { }, }, methods: { - // Log authentication keys - authKeys() { - console.log(Object.keys(this.$auth)); - }, - // Base URL for Richpanel API _baseUrl() { return "https://api.richpanel.com/v1"; }, - // Make an HTTP request to Richpanel API - async _makeRequest(opts = {}) { - const { - $ = this, method = "GET", path = "/", headers = {}, ...otherOpts - } = opts; - return axios($, { - method, + _headers() { + return { + "x-richpanel-key": this.$auth.api_key, + "Content-Type": "application/json", + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + const config = { url: this._baseUrl() + path, - headers: { - ...headers, - "x-richpanel-key": this.$auth.api_key, - "Content-Type": "application/json", - }, - ...otherOpts, - }); + headers: this._headers(), + ...opts, + }; + console.log("config: ", config); + console.log("config: ", JSON.stringify(config)); + return axios($, config); }, - // List all tickets with optional filters - async listTickets(opts = {}) { - const params = {}; - if (this.eventFilterStatus) params.status = this.eventFilterStatus.join(","); - if (this.eventFilterPriority) params.priority = this.eventFilterPriority.join(","); - if (this.eventFilterAssignedAgent) params.assignee_id = this.eventFilterAssignedAgent.join(","); - if (this.eventFilterChannel) params.channel = this.eventFilterChannel.join(","); - if (this.eventFilterDesiredStatuses) params.desired_statuses = this.eventFilterDesiredStatuses.join(","); + createTicket(opts = {}) { return this._makeRequest({ - method: "GET", + method: "POST", path: "/tickets", - params, + ...opts, }); }, - // Create a new ticket - async createTicket() { - const ticketData = {}; - if (this.createStatus) ticketData.status = this.createStatus; - if (this.createCommentBody || this.createCommentSenderType) { - ticketData.comment = {}; - if (this.createCommentBody) ticketData.comment.body = this.createCommentBody; - if (this.createCommentSenderType) ticketData.comment.sender_type = this.createCommentSenderType; - } - if (this.createViaChannel || this.createViaSourceFrom || this.createViaSourceTo) { - ticketData.via = {}; - if (this.createViaChannel) ticketData.via.channel = this.createViaChannel; - if (this.createViaSourceFrom || this.createViaSourceTo) { - ticketData.via.source = {}; - if (this.createViaSourceFrom) ticketData.via.source.from = { - address: this.createViaSourceFrom, - }; - if (this.createViaSourceTo) ticketData.via.source.to = { - address: this.createViaSourceTo, - }; - } - } - if (this.createTags && this.createTags.length > 0) { - ticketData.tags = this.createTags; - } - if (this.createId) { - ticketData.id = this.createId; - } + listTags() { return this._makeRequest({ - method: "POST", - path: "/tickets", - data: { - ticket: ticketData, - }, + path: "/tags", }); }, - // Add a message to an existing ticket - async addMessageToTicket() { - const { - addMessageId, addMessageBody, addMessageSenderType, - } = this; + listTickets(opts = {}) { return this._makeRequest({ - method: "POST", - path: `/tickets/${addMessageId}/comments`, - data: { - comment: { - sender_type: addMessageSenderType, - body: addMessageBody, - }, - }, + path: "/tickets", + opts, }); }, - // Update the status of an existing ticket - async updateTicketStatus() { - const { - updateTicketId, updateStatus, - } = this; + updateTicket({ + conversationId, ...opts + }) { return this._makeRequest({ method: "PUT", - path: `/tickets/${updateTicketId}`, - data: { - ticket: { - status: this.updateStatus, - }, - }, + path: `/tickets/${conversationId}`, + ...opts, }); }, + // Pagination helper method async paginate(fn, opts = {}) { let results = []; @@ -388,5 +305,4 @@ export default { return results; }, }, - version: "0.0.{{ts}}", }; diff --git a/components/richpanel/sources/common/base.mjs b/components/richpanel/sources/common/base.mjs new file mode 100644 index 0000000000000..c6b7c9190b5b1 --- /dev/null +++ b/components/richpanel/sources/common/base.mjs @@ -0,0 +1,69 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import richpanel from "../../richpanel.app.mjs"; + +export default { + props: { + richpanel, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastDate() { + return this.db.get("lastDate") || 0; + }, + _setLastDate(lastDate) { + this.db.set("lastDate", lastDate); + }, + getParams() { + return {}; + }, + async emitEvent(maxResults = false) { + const lastDate = this._getLastDate(); + const dateField = this.getDateField(); + + const response = this.richpanel.paginate({ + fn: this.richpanel.listTickets, + params: { + ...this.getParams(), + sortKey: dateField, + order: "DESC", + }, + }); + + let responseArray = []; + for await (const item of response) { + if (Date.parse(item[dateField]) <= lastDate) break; + responseArray.push(item); + } + + if (responseArray.length) { + if (maxResults && (responseArray.length > maxResults)) { + responseArray.length = maxResults; + } + this._setLastDate(responseArray[0][dateField]); + } + + for (const item of responseArray.reverse()) { + const dateField = item[dateField]; + this.$emit(item, { + id: `${item.id}-${dateField}`, + summary: this.getSummary(item), + ts: Date.parse(dateField), + }); + } + }, + }, + hooks: { + async deploy() { + await this.emitEvent(25); + }, + }, + async run() { + await this.emitEvent(); + }, +}; diff --git a/components/richpanel/sources/new-message/new-message.mjs b/components/richpanel/sources/new-message/new-message.mjs index 89483a1e65dc7..a733c1d4cd9c2 100644 --- a/components/richpanel/sources/new-message/new-message.mjs +++ b/components/richpanel/sources/new-message/new-message.mjs @@ -1,88 +1,22 @@ -import { - axios, DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, -} from "@pipedream/platform"; -import richpanel from "../../richpanel.app.mjs"; +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; export default { + ...common, key: "richpanel-new-message", name: "New Message in Richpanel Ticket", - description: "Emit new event when a customer sends a new message on an existing or new ticket. Optionally filter by channel (e.g., email, chat). [See the documentation]()", - version: "0.0.{{ts}}", + description: "Emit new event when a customer sends a new message on an existing or new ticket. Optionally filter by channel (e.g., email, chat).", + version: "0.0.1", type: "source", dedupe: "unique", - props: { - richpanel, - db: "$.service.db", - timer: { - type: "$.interface.timer", - default: { - intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, - }, + methods: { + ...common.methods, + getDateField() { + return "updated_at"; }, - eventFilterChannel: { - propDefinition: [ - richpanel, - "eventFilterChannel", - ], + getSummary(item) { + return `New message on ticket ${item.id}`; }, }, - hooks: { - async deploy() { - const currentTimestamp = Date.now(); - await this.db.set("lastProcessedTimestamp", currentTimestamp); - }, - async activate() { - // Optionally, log authentication keys or set up webhooks if required - this.richpanel.authKeys(); - }, - async deactivate() { - // Optionally, clean up any resources or log deactivation - console.log("Deactivated richpanel-new-message source."); - }, - }, - async run() { - const ticketsResponse = await this.richpanel.listTickets(); - - if (!ticketsResponse || !ticketsResponse.tickets) { - return; - } - - const tickets = ticketsResponse.tickets; - let lastProcessedTimestamp = await this.db.get("lastProcessedTimestamp") || 0; - let newMessages = []; - - for (const ticket of tickets) { - if (ticket.comments && Array.isArray(ticket.comments)) { - for (const comment of ticket.comments) { - const messageTimestamp = Date.parse(comment.created_at) || Date.now(); - - if (messageTimestamp > lastProcessedTimestamp && comment.sender_type === "customer") { - // Optionally, filter by channel - if (this.eventFilterChannel && !this.eventFilterChannel.includes(comment.via.channel)) { - continue; - } - - newMessages.push({ - ...comment, - ticket_id: ticket.id, - }); - } - } - } - } - - if (newMessages.length > 0) { - for (const message of newMessages) { - this.$emit(message, { - id: message.id || message.ts, - summary: `New message on ticket ${message.ticket_id}`, - ts: Date.parse(message.created_at) || Date.now(), - }); - } - - // Update lastProcessedTimestamp to the latest message's timestamp - const latestTimestamp = Math.max(...newMessages.map((msg) => Date.parse(msg.created_at) || Date.now())); - await this.db.set("lastProcessedTimestamp", latestTimestamp); - } - }, + sampleEmit, }; diff --git a/components/richpanel/sources/new-message/test-event.mjs b/components/richpanel/sources/new-message/test-event.mjs new file mode 100644 index 0000000000000..6c4f7457f87bf --- /dev/null +++ b/components/richpanel/sources/new-message/test-event.mjs @@ -0,0 +1,46 @@ +export default { + "course_title": "Class title", + "course_subject": "Course subject", + "course_level": "Course level", + "school_id": 16385, + "start_date": "2024-10-21", + "end_date": "2024-10-28", + "recurrence": "weekly", + "payment_frequency": "free", + "id": 153319, + "photo": null, + "payment_fee": "0", + "course_description": "", + "course_full_title": "Class title [Online Lesson]", + "created": "2024-10-17 18:20:13", + "modified": "2024-10-17 18:20:15", + "color": "color1", + "course_started": false, + "course_ended": false, + "course_status": 1, + "num_enrolled_students": 0, + "teachers": "66792", + "classrooms": "39627", + "billing_month_start_date": "2024-10-01", + "billing_month_end_date": "2024-10-01", + "custom_payments": null, + "archived": false, + "awarding_body": "", + "course_code": "", + "book_code": "", + "total_lessons": 2, + "total_lessons_hrs": "02:00", + "skype_meeting_link": "", + "year": null, + "credit_hours": "", + "class_type": "", + "is_ended": null, + "teacher_hourly_fees": null, + "is_booking_class": false, + "subscription_plan_id": null, + "is_stripe_sub_allow": 0, + "created_by": 66114, + "modified_by": 66114, + "exception_dates": null, + "removed_exception_dates": null +} \ No newline at end of file diff --git a/components/richpanel/sources/new-ticket-status-change/new-ticket-status-change.mjs b/components/richpanel/sources/new-ticket-status-change/new-ticket-status-change.mjs index 9db1fb8edfed5..d3edf5ea5d222 100644 --- a/components/richpanel/sources/new-ticket-status-change/new-ticket-status-change.mjs +++ b/components/richpanel/sources/new-ticket-status-change/new-ticket-status-change.mjs @@ -1,104 +1,37 @@ -import richpanel from "../../richpanel.app.mjs"; -import { - axios, DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, -} from "@pipedream/platform"; +import { STATUS_OPTIONS } from "../../common/constants.mjs"; +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; export default { + ...common, key: "richpanel-new-ticket-status-change", name: "New Ticket Status Change", - description: "Emit a new event when a ticket's status is updated. [See the documentation]()", - version: "0.0.{{ts}}", + description: "Emit a new event when a ticket's status is updated.", + version: "0.0.1", type: "source", dedupe: "unique", props: { - richpanel, - db: "$.service.db", - timer: { - type: "$.interface.timer", - default: { - intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, - }, - }, - eventFilterDesiredStatuses: { - propDefinition: [ - "richpanel", - "eventFilterDesiredStatuses", - ], + ...common.props, + status: { + type: "string", + label: "Status", + description: "The status of the ticket", + options: STATUS_OPTIONS, }, }, methods: { - async getLastTimestamp() { - return this.db.get("last_timestamp") || new Date(0).toISOString(); - }, - async setLastTimestamp(timestamp) { - await this.db.set("last_timestamp", timestamp); - }, - }, - hooks: { - async deploy() { - const lastTimestamp = await this.getLastTimestamp(); - const tickets = await this.richpanel.listTickets({ - updated_after: lastTimestamp, - ...(this.eventFilterDesiredStatuses - ? { - desired_statuses: this.eventFilterDesiredStatuses.join(","), - } - : {}), - per_page: 50, - sortKey: "updated_at", - order: "DESC", - }); - - tickets.reverse().forEach((ticket) => { - this.$emit(ticket, { - id: ticket.id, - summary: `Ticket ${ticket.id} status changed to ${ticket.status}`, - ts: Date.parse(ticket.updated_at), - }); - }); - - const latestUpdatedAt = - tickets.length > 0 - ? tickets[0].updated_at - : new Date().toISOString(); - await this.setLastTimestamp(latestUpdatedAt); + ...common.methods, + getDateField() { + return "updated_at"; }, - async activate() { - // No webhook setup for polling source + getParams() { + return { + status: this.status, + }; }, - async deactivate() { - // No webhook teardown for polling source + getSummary(item) { + return `Ticket ${item.id} status changed to ${item.status}`; }, }, - async run() { - const lastTimestamp = await this.getLastTimestamp(); - const tickets = await this.richpanel.listTickets({ - updated_after: lastTimestamp, - ...(this.eventFilterDesiredStatuses - ? { - desired_statuses: this.eventFilterDesiredStatuses.join(","), - } - : {}), - per_page: 100, - sortKey: "updated_at", - order: "ASC", - }); - - let newLastTimestamp = lastTimestamp; - - tickets.forEach((ticket) => { - if (ticket.updated_at > lastTimestamp) { - this.$emit(ticket, { - id: ticket.id, - summary: `Ticket ${ticket.id} status changed to ${ticket.status}`, - ts: Date.parse(ticket.updated_at), - }); - if (ticket.updated_at > newLastTimestamp) { - newLastTimestamp = ticket.updated_at; - } - } - }); - - await this.setLastTimestamp(newLastTimestamp); - }, + sampleEmit, }; diff --git a/components/richpanel/sources/new-ticket-status-change/test-event.mjs b/components/richpanel/sources/new-ticket-status-change/test-event.mjs new file mode 100644 index 0000000000000..6c4f7457f87bf --- /dev/null +++ b/components/richpanel/sources/new-ticket-status-change/test-event.mjs @@ -0,0 +1,46 @@ +export default { + "course_title": "Class title", + "course_subject": "Course subject", + "course_level": "Course level", + "school_id": 16385, + "start_date": "2024-10-21", + "end_date": "2024-10-28", + "recurrence": "weekly", + "payment_frequency": "free", + "id": 153319, + "photo": null, + "payment_fee": "0", + "course_description": "", + "course_full_title": "Class title [Online Lesson]", + "created": "2024-10-17 18:20:13", + "modified": "2024-10-17 18:20:15", + "color": "color1", + "course_started": false, + "course_ended": false, + "course_status": 1, + "num_enrolled_students": 0, + "teachers": "66792", + "classrooms": "39627", + "billing_month_start_date": "2024-10-01", + "billing_month_end_date": "2024-10-01", + "custom_payments": null, + "archived": false, + "awarding_body": "", + "course_code": "", + "book_code": "", + "total_lessons": 2, + "total_lessons_hrs": "02:00", + "skype_meeting_link": "", + "year": null, + "credit_hours": "", + "class_type": "", + "is_ended": null, + "teacher_hourly_fees": null, + "is_booking_class": false, + "subscription_plan_id": null, + "is_stripe_sub_allow": 0, + "created_by": 66114, + "modified_by": 66114, + "exception_dates": null, + "removed_exception_dates": null +} \ No newline at end of file diff --git a/components/richpanel/sources/new-ticket/new-ticket.mjs b/components/richpanel/sources/new-ticket/new-ticket.mjs index 04f7edc727dee..7f5645c695cbd 100644 --- a/components/richpanel/sources/new-ticket/new-ticket.mjs +++ b/components/richpanel/sources/new-ticket/new-ticket.mjs @@ -1,115 +1,22 @@ -import { - axios, DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, -} from "@pipedream/platform"; -import richpanel from "../../richpanel.app.mjs"; +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; export default { + ...common, key: "richpanel-new-ticket", name: "New Support Ticket Created", - description: "Emit a new event when a support ticket is created. [See the documentation]()", - version: "0.0.{{ts}}", + description: "Emit new event when a support ticket is created.", + version: "0.0.1", type: "source", dedupe: "unique", - props: { - richpanel, - db: "$.service.db", - timer: { - type: "$.interface.timer", - default: { - intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, - }, + methods: { + ...common.methods, + getDateField() { + return "created_at"; }, - eventFilterStatus: { - propDefinition: [ - "richpanel", - "eventFilterStatus", - ], + getSummary(item) { + return `New Ticket: ${item.subject}`; }, - eventFilterPriority: { - propDefinition: [ - "richpanel", - "eventFilterPriority", - ], - }, - eventFilterAssignedAgent: { - propDefinition: [ - "richpanel", - "eventFilterAssignedAgent", - ], - }, - }, - hooks: { - async deploy() { - const tickets = await this.richpanel.listTickets({ - page: 1, - per_page: 50, - status: this.eventFilterStatus, - priority: this.eventFilterPriority, - assignee_id: this.eventFilterAssignedAgent, - }); - const sortedTickets = tickets.sort( - (a, b) => new Date(b.created_at) - new Date(a.created_at), - ); - for (const ticket of sortedTickets) { - this.$emit(ticket, { - id: ticket.id || Date.parse(ticket.created_at), - summary: `New Ticket: ${ticket.subject}`, - ts: Date.parse(ticket.created_at) || Date.now(), - }); - } - const latestTicket = tickets[0]; - const lastTimestamp = latestTicket - ? Date.parse(latestTicket.created_at) - : Date.now(); - await this.db.set("lastTimestamp", lastTimestamp); - }, - async activate() { - await this.richpanel.createWebhook({ - url: this.webhookUrl, - event: "ticket_created", - filters: { - status: this.eventFilterStatus, - priority: this.eventFilterPriority, - assignee_id: this.eventFilterAssignedAgent, - }, - }); - }, - async deactivate() { - await this.richpanel.deleteWebhook({ - event: "ticket_created", - }); - }, - }, - async run() { - const lastTimestamp = (await this.db.get("lastTimestamp")) || 0; - const tickets = await this.richpanel.listTickets({ - per_page: 100, - status: this.eventFilterStatus, - priority: this.eventFilterPriority, - assignee_id: this.eventFilterAssignedAgent, - since: new Date(lastTimestamp).toISOString(), - }); - - const newTickets = tickets.filter( - (ticket) => Date.parse(ticket.created_at) > lastTimestamp, - ); - - const sortedNewTickets = newTickets.sort( - (a, b) => new Date(a.created_at) - new Date(b.created_at), - ); - - for (const ticket of sortedNewTickets) { - this.$emit(ticket, { - id: ticket.id || Date.parse(ticket.created_at), - summary: `New Ticket: ${ticket.subject}`, - ts: Date.parse(ticket.created_at) || Date.now(), - }); - } - - if (sortedNewTickets.length > 0) { - const latestTicket = sortedNewTickets[sortedNewTickets.length - 1]; - const newLastTimestamp = Date.parse(latestTicket.created_at); - await this.db.set("lastTimestamp", newLastTimestamp); - } }, + sampleEmit, }; diff --git a/components/richpanel/sources/new-ticket/test-event.mjs b/components/richpanel/sources/new-ticket/test-event.mjs new file mode 100644 index 0000000000000..6c4f7457f87bf --- /dev/null +++ b/components/richpanel/sources/new-ticket/test-event.mjs @@ -0,0 +1,46 @@ +export default { + "course_title": "Class title", + "course_subject": "Course subject", + "course_level": "Course level", + "school_id": 16385, + "start_date": "2024-10-21", + "end_date": "2024-10-28", + "recurrence": "weekly", + "payment_frequency": "free", + "id": 153319, + "photo": null, + "payment_fee": "0", + "course_description": "", + "course_full_title": "Class title [Online Lesson]", + "created": "2024-10-17 18:20:13", + "modified": "2024-10-17 18:20:15", + "color": "color1", + "course_started": false, + "course_ended": false, + "course_status": 1, + "num_enrolled_students": 0, + "teachers": "66792", + "classrooms": "39627", + "billing_month_start_date": "2024-10-01", + "billing_month_end_date": "2024-10-01", + "custom_payments": null, + "archived": false, + "awarding_body": "", + "course_code": "", + "book_code": "", + "total_lessons": 2, + "total_lessons_hrs": "02:00", + "skype_meeting_link": "", + "year": null, + "credit_hours": "", + "class_type": "", + "is_ended": null, + "teacher_hourly_fees": null, + "is_booking_class": false, + "subscription_plan_id": null, + "is_stripe_sub_allow": 0, + "created_by": 66114, + "modified_by": 66114, + "exception_dates": null, + "removed_exception_dates": null +} \ No newline at end of file From 177779636c745d9888c1ee82f99f42930a3561d4 Mon Sep 17 00:00:00 2001 From: Luan Cazarine Date: Thu, 2 Jan 2025 11:34:22 -0300 Subject: [PATCH 3/7] pnpm update --- pnpm-lock.yaml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8b0526870c357..206c48b73d268 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8673,7 +8673,10 @@ importers: version: 1.6.6 components/richpanel: - specifiers: {} + dependencies: + '@pipedream/platform': + specifier: ^3.0.3 + version: 3.0.3 components/ringcentral: dependencies: @@ -24579,22 +24582,22 @@ packages: superagent@3.8.1: resolution: {integrity: sha512-VMBFLYgFuRdfeNQSMLbxGSLfmXL/xc+OO+BZp41Za/NRDBet/BNbkRJrYzCUu0u4GU0i/ml2dtT8b9qgkw9z6Q==} engines: {node: '>= 4.0'} - deprecated: Please upgrade to v7.0.2+ of superagent. We have fixed numerous issues with streams, form-data, attach(), filesystem errors not bubbling up (ENOENT on attach()), and all tests are now passing. See the releases tab for more information at . + deprecated: Please upgrade to v9.0.0+ as we have fixed a public vulnerability with formidable dependency. Note that v9.0.0+ requires Node.js v14.18.0+. See https://github.com/ladjs/superagent/pull/1800 for insight. This project is supported and maintained by the team at Forward Email @ https://forwardemail.net superagent@4.1.0: resolution: {integrity: sha512-FT3QLMasz0YyCd4uIi5HNe+3t/onxMyEho7C3PSqmti3Twgy2rXT4fmkTz6wRL6bTF4uzPcfkUCa8u4JWHw8Ag==} engines: {node: '>= 6.0'} - deprecated: Please upgrade to v7.0.2+ of superagent. We have fixed numerous issues with streams, form-data, attach(), filesystem errors not bubbling up (ENOENT on attach()), and all tests are now passing. See the releases tab for more information at . + deprecated: Please upgrade to v9.0.0+ as we have fixed a public vulnerability with formidable dependency. Note that v9.0.0+ requires Node.js v14.18.0+. See https://github.com/ladjs/superagent/pull/1800 for insight. This project is supported and maintained by the team at Forward Email @ https://forwardemail.net superagent@5.3.1: resolution: {integrity: sha512-wjJ/MoTid2/RuGCOFtlacyGNxN9QLMgcpYLDQlWFIhhdJ93kNscFonGvrpAHSCVjRVj++DGCglocF7Aej1KHvQ==} engines: {node: '>= 7.0.0'} - deprecated: Please upgrade to v7.0.2+ of superagent. We have fixed numerous issues with streams, form-data, attach(), filesystem errors not bubbling up (ENOENT on attach()), and all tests are now passing. See the releases tab for more information at . + deprecated: Please upgrade to v9.0.0+ as we have fixed a public vulnerability with formidable dependency. Note that v9.0.0+ requires Node.js v14.18.0+. See https://github.com/ladjs/superagent/pull/1800 for insight. This project is supported and maintained by the team at Forward Email @ https://forwardemail.net superagent@7.1.6: resolution: {integrity: sha512-gZkVCQR1gy/oUXr+kxJMLDjla434KmSOKbx5iGD30Ql+AkJQ/YlPKECJy2nhqOsHLjGHzoDTXNSjhnvWhzKk7g==} engines: {node: '>=6.4.0 <13 || >=14'} - deprecated: Please downgrade to v7.1.5 if you need IE/ActiveXObject support OR upgrade to v8.0.0 as we no longer support IE and published an incorrect patch version (see https://github.com/visionmedia/superagent/issues/1731) + deprecated: Please upgrade to v9.0.0+ as we have fixed a public vulnerability with formidable dependency. Note that v9.0.0+ requires Node.js v14.18.0+. See https://github.com/ladjs/superagent/pull/1800 for insight. This project is supported and maintained by the team at Forward Email @ https://forwardemail.net supports-color@2.0.0: resolution: {integrity: sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==} From a6cc2ee94a53542e658c70aa42f23a5169f384d3 Mon Sep 17 00:00:00 2001 From: Luan Cazarine Date: Mon, 6 Jan 2025 14:44:07 -0300 Subject: [PATCH 4/7] [Components] richpanel #15105 Sources - New Ticket - New Message Actions - Create Ticket - Add Ticket Message - Update Ticket Status --- .../add-ticket-message/add-ticket-message.mjs | 4 +- .../actions/create-ticket/create-ticket.mjs | 24 +- .../update-ticket-status.mjs | 3 +- components/richpanel/common/constants.mjs | 4 - components/richpanel/common/utils.mjs | 2 + components/richpanel/richpanel.app.mjs | 224 +++--------------- components/richpanel/sources/common/base.mjs | 31 +-- .../sources/new-message/new-message.mjs | 21 +- .../sources/new-message/test-event.mjs | 58 ++--- .../new-ticket-status-change.mjs | 37 --- .../new-ticket-status-change/test-event.mjs | 46 ---- .../sources/new-ticket/new-ticket.mjs | 2 +- .../sources/new-ticket/test-event.mjs | 72 +++--- 13 files changed, 127 insertions(+), 401 deletions(-) delete mode 100644 components/richpanel/sources/new-ticket-status-change/new-ticket-status-change.mjs delete mode 100644 components/richpanel/sources/new-ticket-status-change/test-event.mjs diff --git a/components/richpanel/actions/add-ticket-message/add-ticket-message.mjs b/components/richpanel/actions/add-ticket-message/add-ticket-message.mjs index b94874258d075..6bd4f210b0906 100644 --- a/components/richpanel/actions/add-ticket-message/add-ticket-message.mjs +++ b/components/richpanel/actions/add-ticket-message/add-ticket-message.mjs @@ -4,7 +4,7 @@ export default { key: "richpanel-add-ticket-message", name: "Add Ticket Message", description: "Adds a message to an existing ticket. [See the documentation](https://developer.richpanel.com/reference/update-a-conversation)", - version: "0.0.{{ts}}", + version: "0.0.1", type: "action", props: { richpanel, @@ -40,7 +40,7 @@ export default { }, }, }); - $.export("$summary", `Added message to ticket ${this.messageId} successfully`); + $.export("$summary", `Added message to ticket ${this.conversationId} successfully`); return response; }, }; diff --git a/components/richpanel/actions/create-ticket/create-ticket.mjs b/components/richpanel/actions/create-ticket/create-ticket.mjs index aef19c8874764..3ae089303ad37 100644 --- a/components/richpanel/actions/create-ticket/create-ticket.mjs +++ b/components/richpanel/actions/create-ticket/create-ticket.mjs @@ -1,6 +1,7 @@ import { ConfigurationError } from "@pipedream/platform"; import { parseObject } from "../../common/utils.mjs"; import richpanel from "../../richpanel.app.mjs"; +import { VIA_CHANNEL_OPTIONS } from "./common/constants.mjs"; export default { key: "richpanel-create-ticket", @@ -36,23 +37,20 @@ export default { ], }, viaChannel: { - propDefinition: [ - richpanel, - "viaChannel", - ], + type: "string", + label: "Via Channel", + description: "The channel via which the ticket is created", + options: VIA_CHANNEL_OPTIONS, }, viaSourceFrom: { - propDefinition: [ - richpanel, - "viaSourceFrom", - ], + type: "object", + label: "Via Source From", + description: "The object source from which the ticket was created. **Examples: {\"address\": \"abc@email.com\"} or {\"id\": \"+16692668044\"}. It depends on the selected channel**.", }, viaSourceTo: { - propDefinition: [ - richpanel, - "viaSourceTo", - ], - optional: true, + type: "object", + label: "Via Source To", + description: "The object source to which the ticket was created. **Examples: {\"address\": \"abc@email.com\"} or {\"id\": \"+16692668044\"}. It depends on the selected channel**.", }, tags: { propDefinition: [ diff --git a/components/richpanel/actions/update-ticket-status/update-ticket-status.mjs b/components/richpanel/actions/update-ticket-status/update-ticket-status.mjs index 8bceed0e6e7e5..00a7637941308 100644 --- a/components/richpanel/actions/update-ticket-status/update-ticket-status.mjs +++ b/components/richpanel/actions/update-ticket-status/update-ticket-status.mjs @@ -4,7 +4,7 @@ export default { key: "richpanel-update-ticket-status", name: "Update Ticket Status", description: "Updates the status of an existing ticket in Richpanel. [See the documentation](https://developer.richpanel.com/reference/update-a-conversation).", - version: "0.0.{{ts}}", + version: "0.0.1", type: "action", props: { richpanel, @@ -24,6 +24,7 @@ export default { async run({ $ }) { const response = await this.richpanel.updateTicket({ $, + conversationId: this.conversationId, data: { ticket: { status: this.status, diff --git a/components/richpanel/common/constants.mjs b/components/richpanel/common/constants.mjs index 109dcd3cc0e98..27f1dac482253 100644 --- a/components/richpanel/common/constants.mjs +++ b/components/richpanel/common/constants.mjs @@ -7,10 +7,6 @@ export const STATUS_OPTIONS = [ label: "Closed", value: "CLOSED", }, - { - label: "Snoozed", - value: "SNOOZED", - }, ]; export const COMMENT_SENDER_TYPE_OPTIONS = [ diff --git a/components/richpanel/common/utils.mjs b/components/richpanel/common/utils.mjs index dcc9cc61f6f41..e7e1c8305652b 100644 --- a/components/richpanel/common/utils.mjs +++ b/components/richpanel/common/utils.mjs @@ -22,3 +22,5 @@ export const parseObject = (obj) => { } return obj; }; + +export const camelToSnakeCase = (str) => str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`); diff --git a/components/richpanel/richpanel.app.mjs b/components/richpanel/richpanel.app.mjs index 7e353c0a2a2e8..82811996dc383 100644 --- a/components/richpanel/richpanel.app.mjs +++ b/components/richpanel/richpanel.app.mjs @@ -2,7 +2,6 @@ import { axios } from "@pipedream/platform"; import { COMMENT_SENDER_TYPE_OPTIONS, STATUS_OPTIONS, - VIA_CHANNEL_OPTIONS, } from "./common/constants.mjs"; export default { @@ -17,7 +16,7 @@ export default { status: { type: "string", label: "Status", - description: "The status of the new ticket", + description: "The status of the ticket", options: STATUS_OPTIONS, }, commentBody: { @@ -31,22 +30,6 @@ export default { description: "The sender type of the comment", options: COMMENT_SENDER_TYPE_OPTIONS, }, - viaChannel: { - type: "string", - label: "Via Channel", - description: "The channel via which the ticket is created", - options: VIA_CHANNEL_OPTIONS, - }, - viaSourceFrom: { - type: "object", - label: "Via Source From", - description: "The object source from which the ticket was created. **Examples: {\"address\": \"abc@email.com\"} or {\"id\": \"+16692668044\"}. It depends on the selected channel**.", - }, - viaSourceTo: { - type: "object", - label: "Via Source To", - description: "The object source to which the ticket was created. **Examples: {\"address\": \"abc@email.com\"} or {\"id\": \"+16692668044\"}. It depends on the selected channel**.", - }, tags: { type: "string[]", label: "Tags", @@ -62,137 +45,12 @@ export default { })); }, }, - - eventFilterStatus: { - type: "string[]", - label: "Filter by Status", - description: "Filter emitted events by ticket status", - options: [ - { - label: "Open", - value: "OPEN", - }, - { - label: "Closed", - value: "CLOSED", - }, - { - label: "Snoozed", - value: "SNOOZED", - }, - ], - }, - eventFilterPriority: { - type: "string[]", - label: "Filter by Priority", - description: "Filter emitted events by ticket priority", - options: [ - { - label: "Low", - value: "LOW", - }, - { - label: "Medium", - value: "MEDIUM", - }, - { - label: "High", - value: "HIGH", - }, - ], - }, - eventFilterAssignedAgent: { - type: "string[]", - label: "Filter by Assigned Agent", - description: "Filter emitted events by assigned agent", - async options() { - const tickets = await this.paginate(this.listTickets, { - per_page: 100, - }); - const agents = tickets.map((ticket) => ticket.assignee_id).filter(Boolean); - const uniqueAgents = [ - ...new Set(agents), - ]; - return uniqueAgents.map((agent) => ({ - label: agent, - value: agent, - })); - }, - }, - eventFilterChannel: { - type: "string[]", - label: "Filter by Channel", - description: "Filter emitted events by communication channel", - options: [ - { - label: "Email", - value: "email", - }, - { - label: "Chat", - value: "chat", - }, - ], - }, - eventFilterDesiredStatuses: { - type: "string[]", - label: "Desired Statuses to Monitor", - description: "Specify desired statuses to monitor for status updates", - options: [ - { - label: "Open", - value: "OPEN", - }, - { - label: "Pending", - value: "PENDING", - }, - { - label: "Resolved", - value: "RESOLVED", - }, - ], - }, - addMessageId: { - type: "string", - label: "Ticket ID", - description: "ID of the ticket to add a message to", - async options() { - const tickets = await this.paginate(this.listTickets, { - per_page: 100, - }); - return tickets.map((ticket) => ({ - label: ticket.subject || ticket.id, - value: ticket.id, - })); - }, - }, - addMessageBody: { - type: "string", - label: "Comment Body", - description: "Body of the comment to add", - }, - addMessageSenderType: { - type: "string", - label: "Comment Sender Type", - description: "The sender type of the comment", - options: [ - { - label: "Customer", - value: "customer", - }, - { - label: "Operator", - value: "operator", - }, - ], - }, conversationId: { type: "string", label: "Ticket ID", description: "ID of the ticket to update", async options({ page }) { - const ticket = await this.listTickets({ + const { ticket } = await this.listTickets({ params: { page: page + 1, }, @@ -208,33 +66,6 @@ export default { })); }, }, - updateStatus: { - type: "string", - label: "Status", - description: "New status of the ticket", - options: [ - { - label: "Open", - value: "OPEN", - }, - { - label: "Closed", - value: "CLOSED", - }, - { - label: "Snoozed", - value: "SNOOZED", - }, - { - label: "Pending", - value: "PENDING", - }, - { - label: "Resolved", - value: "RESOLVED", - }, - ], - }, }, methods: { _baseUrl() { @@ -249,14 +80,11 @@ export default { _makeRequest({ $ = this, path, ...opts }) { - const config = { + return axios($, { url: this._baseUrl() + path, headers: this._headers(), ...opts, - }; - console.log("config: ", config); - console.log("config: ", JSON.stringify(config)); - return axios($, config); + }); }, createTicket(opts = {}) { return this._makeRequest({ @@ -273,7 +101,7 @@ export default { listTickets(opts = {}) { return this._makeRequest({ path: "/tickets", - opts, + ...opts, }); }, updateTicket({ @@ -286,23 +114,35 @@ export default { }); }, - // Pagination helper method - async paginate(fn, opts = {}) { - let results = []; - let page = 1; - let perPage = opts.per_page || 30; - while (true) { - const response = await fn({ + async *paginate({ + fn, params = {}, maxResults = null, ...opts + }) { + let hasMore = false; + let count = 0; + let page = 0; + + do { + params.page = ++page; + const { ticket } = await fn({ + params, ...opts, - page, - per_page: perPage, }); - if (!response || response.length === 0) break; - results = results.concat(response); - if (response.length < perPage) break; - page += 1; - } - return results; + + if (!ticket) { + return; + } + + for (const d of ticket) { + yield d; + + if (maxResults && ++count === maxResults) { + return count; + } + } + + hasMore = ticket && ticket.length; + + } while (hasMore); }, }, }; diff --git a/components/richpanel/sources/common/base.mjs b/components/richpanel/sources/common/base.mjs index c6b7c9190b5b1..4a04b05f489a2 100644 --- a/components/richpanel/sources/common/base.mjs +++ b/components/richpanel/sources/common/base.mjs @@ -1,4 +1,5 @@ import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import { camelToSnakeCase } from "../../common/utils.mjs"; import richpanel from "../../richpanel.app.mjs"; export default { @@ -14,11 +15,18 @@ export default { }, methods: { _getLastDate() { - return this.db.get("lastDate") || 0; + return this.db.get("lastDate") || "1970-01-01"; }, _setLastDate(lastDate) { this.db.set("lastDate", lastDate); }, + async prepareData(data) { + const response = []; + for await (const item of data) { + response.push(item); + } + return response; + }, getParams() { return {}; }, @@ -28,32 +36,27 @@ export default { const response = this.richpanel.paginate({ fn: this.richpanel.listTickets, + maxResults, params: { ...this.getParams(), + startDate: lastDate, sortKey: dateField, - order: "DESC", + sortOrder: "DESC", }, }); - let responseArray = []; - for await (const item of response) { - if (Date.parse(item[dateField]) <= lastDate) break; - responseArray.push(item); - } + let responseArray = await this.prepareData(response, lastDate); if (responseArray.length) { - if (maxResults && (responseArray.length > maxResults)) { - responseArray.length = maxResults; - } - this._setLastDate(responseArray[0][dateField]); + this._setLastDate(responseArray[0][camelToSnakeCase(dateField)]); } for (const item of responseArray.reverse()) { - const dateField = item[dateField]; + const dateVal = item[camelToSnakeCase(dateField)]; this.$emit(item, { - id: `${item.id}-${dateField}`, + id: `${item.id}-${dateVal}`, summary: this.getSummary(item), - ts: Date.parse(dateField), + ts: Date.parse(dateVal), }); } }, diff --git a/components/richpanel/sources/new-message/new-message.mjs b/components/richpanel/sources/new-message/new-message.mjs index a733c1d4cd9c2..92d6a9161f680 100644 --- a/components/richpanel/sources/new-message/new-message.mjs +++ b/components/richpanel/sources/new-message/new-message.mjs @@ -4,19 +4,34 @@ import sampleEmit from "./test-event.mjs"; export default { ...common, key: "richpanel-new-message", - name: "New Message in Richpanel Ticket", - description: "Emit new event when a customer sends a new message on an existing or new ticket. Optionally filter by channel (e.g., email, chat).", + name: "New Message in Ticket", + description: "Emit new event when a new message is sent on an existing or new ticket.", version: "0.0.1", type: "source", dedupe: "unique", methods: { ...common.methods, getDateField() { - return "updated_at"; + return "updatedAt"; }, getSummary(item) { return `New message on ticket ${item.id}`; }, + async prepareData(data, lastDate) { + const response = []; + for await (const item of data) { + for (const message of item.comments) { + if (Date.parse(message.created_at) > Date.parse(lastDate)) { + response.push({ + ...message, + updated_at: message.created_at, + ticket_id: item.id, + }); + } + } + } + return response; + }, }, sampleEmit, }; diff --git a/components/richpanel/sources/new-message/test-event.mjs b/components/richpanel/sources/new-message/test-event.mjs index 6c4f7457f87bf..899cbd81e9a61 100644 --- a/components/richpanel/sources/new-message/test-event.mjs +++ b/components/richpanel/sources/new-message/test-event.mjs @@ -1,46 +1,16 @@ export default { - "course_title": "Class title", - "course_subject": "Course subject", - "course_level": "Course level", - "school_id": 16385, - "start_date": "2024-10-21", - "end_date": "2024-10-28", - "recurrence": "weekly", - "payment_frequency": "free", - "id": 153319, - "photo": null, - "payment_fee": "0", - "course_description": "", - "course_full_title": "Class title [Online Lesson]", - "created": "2024-10-17 18:20:13", - "modified": "2024-10-17 18:20:15", - "color": "color1", - "course_started": false, - "course_ended": false, - "course_status": 1, - "num_enrolled_students": 0, - "teachers": "66792", - "classrooms": "39627", - "billing_month_start_date": "2024-10-01", - "billing_month_end_date": "2024-10-01", - "custom_payments": null, - "archived": false, - "awarding_body": "", - "course_code": "", - "book_code": "", - "total_lessons": 2, - "total_lessons_hrs": "02:00", - "skype_meeting_link": "", - "year": null, - "credit_hours": "", - "class_type": "", - "is_ended": null, - "teacher_hourly_fees": null, - "is_booking_class": false, - "subscription_plan_id": null, - "is_stripe_sub_allow": 0, - "created_by": 66114, - "modified_by": 66114, - "exception_dates": null, - "removed_exception_dates": null + "attachments": [], + "author_id": "0bb92ea8-1ea0-4f85-8d7c-90f183b2636c", + "created_at": "2021-04-14T16:22:51.000Z", + "updated_at": "2021-04-14T16:22:51.000Z", + "body": "made by Anushka Goyal", + "id": "453347427_idontknow436_186779", + "metadata": {}, + "plain_body": "made by Anushka Goyal", + "type": "VoiceComment", + "via": { + "channel": "aircall" + }, + "isOperator": true, + "ticket_id": "453347427_idontknow436", } \ No newline at end of file diff --git a/components/richpanel/sources/new-ticket-status-change/new-ticket-status-change.mjs b/components/richpanel/sources/new-ticket-status-change/new-ticket-status-change.mjs deleted file mode 100644 index d3edf5ea5d222..0000000000000 --- a/components/richpanel/sources/new-ticket-status-change/new-ticket-status-change.mjs +++ /dev/null @@ -1,37 +0,0 @@ -import { STATUS_OPTIONS } from "../../common/constants.mjs"; -import common from "../common/base.mjs"; -import sampleEmit from "./test-event.mjs"; - -export default { - ...common, - key: "richpanel-new-ticket-status-change", - name: "New Ticket Status Change", - description: "Emit a new event when a ticket's status is updated.", - version: "0.0.1", - type: "source", - dedupe: "unique", - props: { - ...common.props, - status: { - type: "string", - label: "Status", - description: "The status of the ticket", - options: STATUS_OPTIONS, - }, - }, - methods: { - ...common.methods, - getDateField() { - return "updated_at"; - }, - getParams() { - return { - status: this.status, - }; - }, - getSummary(item) { - return `Ticket ${item.id} status changed to ${item.status}`; - }, - }, - sampleEmit, -}; diff --git a/components/richpanel/sources/new-ticket-status-change/test-event.mjs b/components/richpanel/sources/new-ticket-status-change/test-event.mjs deleted file mode 100644 index 6c4f7457f87bf..0000000000000 --- a/components/richpanel/sources/new-ticket-status-change/test-event.mjs +++ /dev/null @@ -1,46 +0,0 @@ -export default { - "course_title": "Class title", - "course_subject": "Course subject", - "course_level": "Course level", - "school_id": 16385, - "start_date": "2024-10-21", - "end_date": "2024-10-28", - "recurrence": "weekly", - "payment_frequency": "free", - "id": 153319, - "photo": null, - "payment_fee": "0", - "course_description": "", - "course_full_title": "Class title [Online Lesson]", - "created": "2024-10-17 18:20:13", - "modified": "2024-10-17 18:20:15", - "color": "color1", - "course_started": false, - "course_ended": false, - "course_status": 1, - "num_enrolled_students": 0, - "teachers": "66792", - "classrooms": "39627", - "billing_month_start_date": "2024-10-01", - "billing_month_end_date": "2024-10-01", - "custom_payments": null, - "archived": false, - "awarding_body": "", - "course_code": "", - "book_code": "", - "total_lessons": 2, - "total_lessons_hrs": "02:00", - "skype_meeting_link": "", - "year": null, - "credit_hours": "", - "class_type": "", - "is_ended": null, - "teacher_hourly_fees": null, - "is_booking_class": false, - "subscription_plan_id": null, - "is_stripe_sub_allow": 0, - "created_by": 66114, - "modified_by": 66114, - "exception_dates": null, - "removed_exception_dates": null -} \ No newline at end of file diff --git a/components/richpanel/sources/new-ticket/new-ticket.mjs b/components/richpanel/sources/new-ticket/new-ticket.mjs index 7f5645c695cbd..4f353840ef2b1 100644 --- a/components/richpanel/sources/new-ticket/new-ticket.mjs +++ b/components/richpanel/sources/new-ticket/new-ticket.mjs @@ -12,7 +12,7 @@ export default { methods: { ...common.methods, getDateField() { - return "created_at"; + return "createdAt"; }, getSummary(item) { return `New Ticket: ${item.subject}`; diff --git a/components/richpanel/sources/new-ticket/test-event.mjs b/components/richpanel/sources/new-ticket/test-event.mjs index 6c4f7457f87bf..4c6f009ff8d81 100644 --- a/components/richpanel/sources/new-ticket/test-event.mjs +++ b/components/richpanel/sources/new-ticket/test-event.mjs @@ -1,46 +1,30 @@ export default { - "course_title": "Class title", - "course_subject": "Course subject", - "course_level": "Course level", - "school_id": 16385, - "start_date": "2024-10-21", - "end_date": "2024-10-28", - "recurrence": "weekly", - "payment_frequency": "free", - "id": 153319, - "photo": null, - "payment_fee": "0", - "course_description": "", - "course_full_title": "Class title [Online Lesson]", - "created": "2024-10-17 18:20:13", - "modified": "2024-10-17 18:20:15", - "color": "color1", - "course_started": false, - "course_ended": false, - "course_status": 1, - "num_enrolled_students": 0, - "teachers": "66792", - "classrooms": "39627", - "billing_month_start_date": "2024-10-01", - "billing_month_end_date": "2024-10-01", - "custom_payments": null, - "archived": false, - "awarding_body": "", - "course_code": "", - "book_code": "", - "total_lessons": 2, - "total_lessons_hrs": "02:00", - "skype_meeting_link": "", - "year": null, - "credit_hours": "", - "class_type": "", - "is_ended": null, - "teacher_hourly_fees": null, - "is_booking_class": false, - "subscription_plan_id": null, - "is_stripe_sub_allow": 0, - "created_by": 66114, - "modified_by": 66114, - "exception_dates": null, - "removed_exception_dates": null + "id": "453347427_idontknow436_186779", + "created_at": "2021-04-14T16:22:51.000Z", + "updated_at": "2021-04-14T16:22:51.000Z", + "assignee_id": "0bb92ea8-1ea0-4f85-8d7c-90f183b2636c", + "organization_id": "idontknow436", + "priority": "LOW", + "recipient": "186779", + "status": "OPEN", + "subject": "Outgoing call to +1 844-902-3197", + "via": {}, + "comments": [ + { + "attachments": [], + "author_id": "0bb92ea8-1ea0-4f85-8d7c-90f183b2636c", + "created_at": "2021-04-14T16:22:51.000Z", + "body": "made by Anushka Goyal", + "id": "453347427_idontknow436_186779", + "metadata": {}, + "plain_body": "made by Anushka Goyal", + "type": "VoiceComment", + "via": { + "channel": "aircall" + }, + "isOperator": true + } + ], + "url": "https://app.richpanel.com/453347427_idontknow436_186779", + "tags": [] } \ No newline at end of file From 100b8bbff515b4d38aa94555846edb1a6aae596d Mon Sep 17 00:00:00 2001 From: Luan Cazarine Date: Mon, 6 Jan 2025 14:57:13 -0300 Subject: [PATCH 5/7] pnpm update --- pnpm-lock.yaml | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e2ccd332e47d1..2950e440f4bf4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1005,8 +1005,7 @@ importers: specifier: ^9.0.1 version: 9.0.1 - components/azure_storage: - specifiers: {} + components/azure_storage: {} components/backblaze: {} @@ -3156,8 +3155,7 @@ importers: specifier: ^4.0.0 version: 4.0.1 - components/elevio: - specifiers: {} + components/elevio: {} components/elmah_io: dependencies: @@ -4820,8 +4818,7 @@ importers: specifier: ^1.6.0 version: 1.6.6 - components/homerun: - specifiers: {} + components/homerun: {} components/hookdeck: dependencies: @@ -8316,8 +8313,7 @@ importers: specifier: ^1.1.1 version: 1.6.6 - components/ragie: - specifiers: {} + components/ragie: {} components/railsr: {} @@ -8510,8 +8506,7 @@ importers: specifier: ^1.3.0 version: 1.6.6 - components/refiner: - specifiers: {} + components/refiner: {} components/reflect: dependencies: @@ -11050,8 +11045,7 @@ importers: specifier: ^6.2.13 version: 6.2.13 - components/typefully: - specifiers: {} + components/typefully: {} components/typless: {} @@ -11576,8 +11570,7 @@ importers: components/weworkbook: {} - components/what_are_those: - specifiers: {} + components/what_are_those: {} components/whatconverts: {} From 7a2b4671f820852c4a20201e3190aee1e930c67b Mon Sep 17 00:00:00 2001 From: Luan Cazarine Date: Mon, 6 Jan 2025 15:04:07 -0300 Subject: [PATCH 6/7] fix import --- components/richpanel/actions/create-ticket/create-ticket.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/richpanel/actions/create-ticket/create-ticket.mjs b/components/richpanel/actions/create-ticket/create-ticket.mjs index 3ae089303ad37..a56254480a990 100644 --- a/components/richpanel/actions/create-ticket/create-ticket.mjs +++ b/components/richpanel/actions/create-ticket/create-ticket.mjs @@ -1,7 +1,7 @@ import { ConfigurationError } from "@pipedream/platform"; +import { VIA_CHANNEL_OPTIONS } from "../../common/constants.mjs"; import { parseObject } from "../../common/utils.mjs"; import richpanel from "../../richpanel.app.mjs"; -import { VIA_CHANNEL_OPTIONS } from "./common/constants.mjs"; export default { key: "richpanel-create-ticket", From 0f161505ceb242401c1beecb97531521374677f9 Mon Sep 17 00:00:00 2001 From: Luan Cazarine Date: Tue, 7 Jan 2025 11:44:38 -0300 Subject: [PATCH 7/7] Add some props --- .../actions/create-ticket/create-ticket.mjs | 123 +++++++++++++++++- 1 file changed, 118 insertions(+), 5 deletions(-) diff --git a/components/richpanel/actions/create-ticket/create-ticket.mjs b/components/richpanel/actions/create-ticket/create-ticket.mjs index a56254480a990..2f4e7b8518036 100644 --- a/components/richpanel/actions/create-ticket/create-ticket.mjs +++ b/components/richpanel/actions/create-ticket/create-ticket.mjs @@ -24,12 +24,28 @@ export default { "status", ], }, + subject: { + type: "string", + label: "Subject", + description: "The subject of the ticket.", + optional: true, + }, commentBody: { propDefinition: [ richpanel, "commentBody", ], }, + priority: { + type: "string", + label: "Priority", + description: "The priority of the ticket.", + options: [ + "HIGH", + "LOW", + ], + optional: true, + }, commentSenderType: { propDefinition: [ richpanel, @@ -39,18 +55,57 @@ export default { viaChannel: { type: "string", label: "Via Channel", - description: "The channel via which the ticket is created", + description: "The channel via which the ticket is created.", + reloadProps: true, options: VIA_CHANNEL_OPTIONS, }, + viaSourceFromAddress: { + type: "string", + label: "Via Source From Address", + description: "The email address of the source from.", + hidden: true, + }, + viaSourceFromName: { + type: "string", + label: "Via Source From Name", + description: "The name of the source from.", + hidden: true, + }, + viaSourceToAddress: { + type: "string", + label: "Via Source To Address", + description: "The email address of the source to.", + hidden: true, + }, + viaSourceToName: { + type: "string", + label: "Via Source To Name", + description: "The name of the source to.", + hidden: true, + }, + viaSourceFromNumber: { + type: "string", + label: "Via Source From Number", + description: "The phone number of the source from.", + hidden: true, + }, + viaSourceToNumber: { + type: "string", + label: "Via Source To Number", + description: "The phone number of the source to.", + hidden: true, + }, viaSourceFrom: { type: "object", label: "Via Source From", description: "The object source from which the ticket was created. **Examples: {\"address\": \"abc@email.com\"} or {\"id\": \"+16692668044\"}. It depends on the selected channel**.", + hidden: true, }, viaSourceTo: { type: "object", label: "Via Source To", description: "The object source to which the ticket was created. **Examples: {\"address\": \"abc@email.com\"} or {\"id\": \"+16692668044\"}. It depends on the selected channel**.", + hidden: true, }, tags: { propDefinition: [ @@ -60,24 +115,82 @@ export default { optional: true, }, }, + async additionalProps(props) { + switch (this.viaChannel) { + case "email" : + props.viaSourceFromAddress.hidden = false; + props.viaSourceFromName.hidden = false; + props.viaSourceToAddress.hidden = false; + props.viaSourceToName.hidden = false; + props.viaSourceFrom.hidden = true; + props.viaSourceTo.hidden = true; + props.viaSourceFromNumber.hidden = true; + props.viaSourceToNumber.hidden = true; + break; + case "aircall" : + props.viaSourceFromNumber.hidden = false; + props.viaSourceToNumber.hidden = false; + props.viaSourceFrom.hidden = true; + props.viaSourceTo.hidden = true; + props.viaSourceFromAddress.hidden = true; + props.viaSourceFromName.hidden = true; + props.viaSourceToAddress.hidden = true; + props.viaSourceToName.hidden = true; + break; + default: + props.viaSourceFrom.hidden = false; + props.viaSourceTo.hidden = false; + props.viaSourceFromAddress.hidden = true; + props.viaSourceFromName.hidden = true; + props.viaSourceToAddress.hidden = true; + props.viaSourceToName.hidden = true; + props.viaSourceFromNumber.hidden = true; + props.viaSourceToNumber.hidden = true; + } + return {}; + }, async run({ $ }) { try { + const source = {}; + switch (this.viaChannel) { + case "email" : + source.from = { + address: this.viaSourceFromAddress, + name: this.viaSourceFromName, + }; + source.to = { + address: this.viaSourceToAddress, + name: this.viaSourceToName, + }; + break; + case "aircall" : + source.from = { + id: this.viaSourceFromNumber, + }; + source.to = { + id: this.viaSourceToNumber, + }; + break; + default: + source.from = parseObject(this.viaSourceFrom); + source.to = parseObject(this.viaSourceTo); + } + const response = await this.richpanel.createTicket({ $, data: { ticket: { id: this.id, status: this.status, + subject: this.subject, comment: { body: this.commentBody, sender_type: this.commentSenderType, }, + priority: this.priority, via: { channel: this.viaChannel, - source: { - from: parseObject(this.viaSourceFrom), - to: parseObject(this.viaSourceTo), - }, + source, }, tags: parseObject(this.tags), },