diff --git a/.all-contributorsrc b/.all-contributorsrc index 1d38a9036a..a150133190 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1010,6 +1010,33 @@ "contributions": [ "plugin" ] + }, + { + "login": "thirstycode", + "name": "Pratik Kinage", + "avatar_url": "https://avatars.githubusercontent.com/u/37847256?v=4", + "profile": "https://www.gamespecifications.com/", + "contributions": [ + "plugin" + ] + }, + { + "login": "LevwTech", + "name": "Abdelrahman Mostafa ", + "avatar_url": "https://avatars.githubusercontent.com/u/69399787?v=4", + "profile": "https://github.com/LevwTech", + "contributions": [ + "plugin" + ] + }, + { + "login": "HamzaZagha", + "name": "Hamza Zagha", + "avatar_url": "https://avatars.githubusercontent.com/u/45468866?v=4", + "profile": "https://github.com/HamzaZagha", + "contributions": [ + "bug" + ] } ], "commitType": "docs" diff --git a/README.md b/README.md index 2a1b94b277..d0d68dab45 100644 --- a/README.md +++ b/README.md @@ -274,6 +274,11 @@ Not into coding but still interested in contributing? Come join our [Discord](ht Javier HM
Javier HM

🔌 Mohamed Hassan
Mohamed Hassan

🐛 Christian Schab
Christian Schab

🔌 + Pratik Kinage
Pratik Kinage

🔌 + Abdelrahman Mostafa
Abdelrahman Mostafa

🔌 + + + Hamza Zagha
Hamza Zagha

🐛 diff --git a/package-lock.json b/package-lock.json index 754d9f4884..f352ea06cd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -147,7 +147,7 @@ "openai": "4.17.5", "papaparse": "5.4.1", "pdf-parse": "1.1.1", - "pdf-text-reader": "4.0.1", + "pdf-text-reader": "4.1.0", "pg": "8.11.3", "pickleparser": "0.1.0", "pino-loki": "2.1.3", @@ -11544,11 +11544,6 @@ "npm": ">= 8.6.0" } }, - "node_modules/@slack/web-api/node_modules/eventemitter3": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", - "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==" - }, "node_modules/@slack/web-api/node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -24227,9 +24222,9 @@ } }, "node_modules/eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==" }, "node_modules/events": { "version": "3.3.0", @@ -26593,6 +26588,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/http-proxy/node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true + }, "node_modules/http-server": { "version": "14.1.1", "resolved": "https://registry.npmjs.org/http-server/-/http-server-14.1.1.tgz", @@ -30768,12 +30769,6 @@ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true }, - "node_modules/listr2/node_modules/eventemitter3": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", - "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", - "dev": true - }, "node_modules/listr2/node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -34152,6 +34147,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-queue/node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" + }, "node_modules/p-retry": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", @@ -34473,25 +34473,13 @@ "node": ">=8" } }, - "node_modules/path2d": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/path2d/-/path2d-0.1.1.tgz", - "integrity": "sha512-/+S03c8AGsDYKKBtRDqieTJv2GlkMb0bWjnqOgtF6MkjdUQ9a8ARAtxWf9NgKLGm2+WQr6+/tqJdU8HNGsIDoA==", - "optional": true, - "engines": { - "node": ">=6" - } - }, "node_modules/path2d-polyfill": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/path2d-polyfill/-/path2d-polyfill-2.1.1.tgz", - "integrity": "sha512-4Rka5lN+rY/p0CdD8+E+BFv51lFaFvJOrlOhyQ+zjzyQrzyh3ozmxd1vVGGDdIbUFSBtIZLSnspxTgPT0iJhvA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path2d-polyfill/-/path2d-polyfill-2.0.1.tgz", + "integrity": "sha512-ad/3bsalbbWhmBo0D6FZ4RNMwsLsPpL6gnvhuSaU5Vm7b06Kr5ubSltQQ0T7YKsiJQO+g22zJ4dJKNTXIyOXtA==", "optional": true, - "dependencies": { - "path2d": "0.1.1" - }, "engines": { - "node": ">=18" + "node": ">=8" } }, "node_modules/pause-stream": { @@ -34524,9 +34512,9 @@ } }, "node_modules/pdf-text-reader": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pdf-text-reader/-/pdf-text-reader-4.0.1.tgz", - "integrity": "sha512-AuXTpAnZORgq6c1CblRZ6j+X6GTV1ap8iDmOd4koefXwCzqdzJjmRkXUVpuAxHdHjYji2j4kN98vJvK5PJE3PA==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/pdf-text-reader/-/pdf-text-reader-4.1.0.tgz", + "integrity": "sha512-De2EnUEI+w626PNHMY01Yqdafr6GG5Il8cIplgKsthsc4Uoailq6LNbBhAoFcvLCwzgS/HX/5jajXSL0A0tTcw==", "dependencies": { "pdfjs-dist": "3.9.179" } diff --git a/package.json b/package.json index 90a068826f..ec2b5121d6 100644 --- a/package.json +++ b/package.json @@ -160,7 +160,7 @@ "openai": "4.17.5", "papaparse": "5.4.1", "pdf-parse": "1.1.1", - "pdf-text-reader": "4.0.1", + "pdf-text-reader": "4.1.0", "pg": "8.11.3", "pickleparser": "0.1.0", "pino-loki": "2.1.3", diff --git a/packages/ee/ui/platform/src/lib/components/audit-event-table/audit-event-table.component.html b/packages/ee/ui/platform/src/lib/components/audit-event-table/audit-event-table.component.html index 8c8a0790ec..a1a145189a 100644 --- a/packages/ee/ui/platform/src/lib/components/audit-event-table/audit-event-table.component.html +++ b/packages/ee/ui/platform/src/lib/components/audit-event-table/audit-event-table.component.html @@ -1,7 +1,8 @@
@if(featureLocked) { + featureKey="AUDIT_LOGS" + > } @else { diff --git a/packages/ee/ui/project-members/src/lib/project-members-table/project-members-table.component.html b/packages/ee/ui/project-members/src/lib/project-members-table/project-members-table.component.html index c249c22480..f5f6580a97 100644 --- a/packages/ee/ui/project-members/src/lib/project-members-table/project-members-table.component.html +++ b/packages/ee/ui/project-members/src/lib/project-members-table/project-members-table.component.html @@ -2,6 +2,7 @@ @if(isFeatureLocked) {
diff --git a/packages/pieces/community/github/package.json b/packages/pieces/community/github/package.json index 2970bc75dd..9d25f1fc13 100644 --- a/packages/pieces/community/github/package.json +++ b/packages/pieces/community/github/package.json @@ -1,4 +1,4 @@ { "name": "@activepieces/piece-github", - "version": "0.3.9" + "version": "0.3.10" } diff --git a/packages/pieces/community/github/src/lib/common/index.ts b/packages/pieces/community/github/src/lib/common/index.ts index 3cfeb9f92b..51eae540ce 100644 --- a/packages/pieces/community/github/src/lib/common/index.ts +++ b/packages/pieces/community/github/src/lib/common/index.ts @@ -1,170 +1,180 @@ import { Property, OAuth2PropertyValue } from '@activepieces/pieces-framework'; import { - HttpRequest, - HttpMethod, - AuthenticationType, - httpClient, + HttpRequest, + HttpMethod, + AuthenticationType, + httpClient, } from '@activepieces/pieces-common'; export const githubCommon = { - baseUrl: 'https://api.github.com', - repositoryDropdown: Property.Dropdown<{ repo: string; owner: string }>({ - displayName: 'Repository', - refreshers: [], - required: true, - options: async ({ auth }) => { - if (!auth) { - return { - disabled: true, - options: [], - placeholder: 'please authenticate first', - }; - } - const authProp: OAuth2PropertyValue = auth as OAuth2PropertyValue; - const repositories = await getUserRepo(authProp); - return { - disabled: false, - options: repositories.map((repo) => { - return { - label: repo.owner.login + '/' + repo.name, - value: { - owner: repo.owner.login, - repo: repo.name, - }, - }; - }), - }; - }, - }), - assigneeDropDown: (required = false) => - Property.MultiSelectDropdown({ - displayName: 'Assignees', - description: 'Assignees for the Issue', - refreshers: ['repository'], + baseUrl: 'https://api.github.com', + repositoryDropdown: Property.Dropdown<{ repo: string; owner: string }>({ + displayName: 'Repository', + refreshers: [], + required: true, + options: async ({ auth }) => { + if (!auth) { + return { + disabled: true, + options: [], + placeholder: 'please authenticate first', + }; + } + const authProp: OAuth2PropertyValue = auth as OAuth2PropertyValue; + const repositories = await getUserRepo(authProp); + return { + disabled: false, + options: repositories.map((repo) => { + return { + label: repo.owner.login + '/' + repo.name, + value: { + owner: repo.owner.login, + repo: repo.name, + }, + }; + }), + }; + }, + }), + assigneeDropDown: (required = false) => + Property.MultiSelectDropdown({ + displayName: 'Assignees', + description: 'Assignees for the Issue', + refreshers: ['repository'], - required, - options: async ({ auth, repository }) => { - if (!auth || !repository) { - return { - disabled: true, - options: [], - placeholder: 'please authenticate first and select repo', - }; - } - const authProp: OAuth2PropertyValue = auth as OAuth2PropertyValue; - const { owner, repo } = repository as RepositoryProp; - const assignees = await getAssignee(authProp, owner, repo); - return { - disabled: false, - options: assignees.map((assignee) => { - return { - label: assignee.login, - value: assignee.login, - }; - }), - }; - }, - }), - labelDropDown: (required = false) => - Property.MultiSelectDropdown({ - displayName: 'Labels', - description: 'Labels for the Issue', - refreshers: ['repository'], - required, - options: async ({ auth, repository }) => { - if (!auth || !repository) { - return { - disabled: true, - options: [], - placeholder: 'please authenticate first and select repo', - }; - } - const authProp: OAuth2PropertyValue = auth as OAuth2PropertyValue; - const { owner, repo } = repository as RepositoryProp; - const labels = await listIssueLabels(authProp, owner, repo); - return { - disabled: false, - options: labels.map((label) => { - return { - label: label.name, - value: label.name, - }; - }), - }; - }, - }), + required, + options: async ({ auth, repository }) => { + if (!auth || !repository) { + return { + disabled: true, + options: [], + placeholder: 'please authenticate first and select repo', + }; + } + const authProp: OAuth2PropertyValue = auth as OAuth2PropertyValue; + const { owner, repo } = repository as RepositoryProp; + const assignees = await getAssignee(authProp, owner, repo); + return { + disabled: false, + options: assignees.map((assignee) => { + return { + label: assignee.login, + value: assignee.login, + }; + }), + }; + }, + }), + labelDropDown: (required = false) => + Property.MultiSelectDropdown({ + displayName: 'Labels', + description: 'Labels for the Issue', + refreshers: ['repository'], + required, + options: async ({ auth, repository }) => { + if (!auth || !repository) { + return { + disabled: true, + options: [], + placeholder: 'please authenticate first and select repo', + }; + } + const authProp: OAuth2PropertyValue = auth as OAuth2PropertyValue; + const { owner, repo } = repository as RepositoryProp; + const labels = await listIssueLabels(authProp, owner, repo); + return { + disabled: false, + options: labels.map((label) => { + return { + label: label.name, + value: label.name, + }; + }), + }; + }, + }), }; -async function getUserRepo( - authProp: OAuth2PropertyValue -): Promise { - const request: HttpRequest = { - method: HttpMethod.GET, - url: `${githubCommon.baseUrl}/user/repos`, - queryParams: { - per_page: '200', - }, - authentication: { - type: AuthenticationType.BEARER_TOKEN, - token: authProp.access_token, - }, - }; - const response = await httpClient.sendRequest(request); - return response.body; +async function getUserRepo(authProp: OAuth2PropertyValue): Promise { + let page = 1; + let hasNext; + const repos: GithubRepository[] = []; + do { + const request: HttpRequest = { + method: HttpMethod.GET, + url: `${githubCommon.baseUrl}/user/repos`, + queryParams: { + page: page.toString(), + per_page: '100', + }, + authentication: { + type: AuthenticationType.BEARER_TOKEN, + token: authProp.access_token, + }, + }; + + const response = await httpClient.sendRequest(request); + repos.push(...response.body); + + hasNext = response.headers?.link?.includes('rel="next"'); + page += 1; + } while (hasNext); + + return repos; } async function getAssignee( - authProp: OAuth2PropertyValue, - owner: string, - repo: string + authProp: OAuth2PropertyValue, + owner: string, + repo: string, ): Promise { - const request: HttpRequest = { - method: HttpMethod.GET, - url: `${githubCommon.baseUrl}/repos/${owner}/${repo}/assignees`, - queryParams: { - per_page: '30', - }, - authentication: { - type: AuthenticationType.BEARER_TOKEN, - token: authProp.access_token, - }, - }; - const response = await httpClient.sendRequest(request); - return response.body; + const request: HttpRequest = { + method: HttpMethod.GET, + url: `${githubCommon.baseUrl}/repos/${owner}/${repo}/assignees`, + queryParams: { + per_page: '30', + }, + authentication: { + type: AuthenticationType.BEARER_TOKEN, + token: authProp.access_token, + }, + }; + const response = await httpClient.sendRequest(request); + return response.body; } async function listIssueLabels( - authProp: OAuth2PropertyValue, - owner: string, - repo: string + authProp: OAuth2PropertyValue, + owner: string, + repo: string, ): Promise { - const request: HttpRequest = { - method: HttpMethod.GET, - url: `${githubCommon.baseUrl}/repos/${owner}/${repo}/labels`, - queryParams: { - per_page: '30', - }, - authentication: { - type: AuthenticationType.BEARER_TOKEN, - token: authProp.access_token, - }, - }; - const response = await httpClient.sendRequest(request); - return response.body; + const request: HttpRequest = { + method: HttpMethod.GET, + url: `${githubCommon.baseUrl}/repos/${owner}/${repo}/labels`, + queryParams: { + per_page: '30', + }, + authentication: { + type: AuthenticationType.BEARER_TOKEN, + token: authProp.access_token, + }, + }; + const response = await httpClient.sendRequest(request); + return response.body; } export interface GithubRepository { - name: string; - owner: { - login: string; - }; + name: string; + owner: { + login: string; + }; } export interface GithubAssignee { - login: string; + login: string; } export interface GithubIssueLabel { - id: string; - name: string; + id: string; + name: string; } export interface RepositoryProp { - repo: string; - owner: string; + repo: string; + owner: string; } diff --git a/packages/pieces/community/notion/package.json b/packages/pieces/community/notion/package.json index a55daadcf0..0fcf17e575 100644 --- a/packages/pieces/community/notion/package.json +++ b/packages/pieces/community/notion/package.json @@ -1,4 +1,4 @@ { "name": "@activepieces/piece-notion", - "version": "0.2.12" + "version": "0.2.13" } \ No newline at end of file diff --git a/packages/pieces/community/notion/src/lib/triggers/updated-database-item.ts b/packages/pieces/community/notion/src/lib/triggers/updated-database-item.ts index ba86360b3b..72fff717f3 100644 --- a/packages/pieces/community/notion/src/lib/triggers/updated-database-item.ts +++ b/packages/pieces/community/notion/src/lib/triggers/updated-database-item.ts @@ -128,9 +128,9 @@ const polling: Polling< lastFetchEpochMS === 0 ? null : dayjs(lastFetchEpochMS).toISOString() ); return items.results.map((item) => { - const object = item as { created_time: string }; + const object = item as { last_edited_time: string }; return { - epochMilliSeconds: dayjs(object.created_time).valueOf(), + epochMilliSeconds: dayjs(object.last_edited_time).valueOf(), data: item, }; }); diff --git a/packages/pieces/community/openai/package.json b/packages/pieces/community/openai/package.json index 4f1c94d95a..f4068e4900 100644 --- a/packages/pieces/community/openai/package.json +++ b/packages/pieces/community/openai/package.json @@ -1,4 +1,4 @@ { "name": "@activepieces/piece-openai", - "version": "0.3.22" + "version": "0.3.24" } \ No newline at end of file diff --git a/packages/pieces/community/openai/src/index.ts b/packages/pieces/community/openai/src/index.ts index 3dc5c44794..2359fc6152 100644 --- a/packages/pieces/community/openai/src/index.ts +++ b/packages/pieces/community/openai/src/index.ts @@ -1,13 +1,10 @@ import { - AuthenticationType, - HttpMethod, - createCustomApiCallAction, - httpClient, + AuthenticationType, + HttpMethod, + createCustomApiCallAction, + httpClient, } from '@activepieces/pieces-common'; -import { - PieceAuth, - createPiece, -} from '@activepieces/pieces-framework'; +import { PieceAuth, createPiece } from '@activepieces/pieces-framework'; import { PieceCategory } from '@activepieces/shared'; import { askAssistant } from './lib/actions/ask-assistant'; import { generateImage } from './lib/actions/generate-image'; @@ -17,6 +14,7 @@ import { transcribeAction } from './lib/actions/transcriptions'; import { translateAction } from './lib/actions/translation'; import { visionPrompt } from './lib/actions/vision-prompt'; import { baseUrl } from './lib/common/common'; +import { extractStructuredDataAction } from './lib/actions/extract-structure-data.action'; const markdownDescription = ` Follow these instructions to get your OpenAI API Key: @@ -28,58 +26,69 @@ It is strongly recommended that you add your credit card information to your Ope `; export const openaiAuth = PieceAuth.SecretText({ - description: markdownDescription, - displayName: 'API Key', - required: true, - validate: async (auth) => { - try { - await httpClient.sendRequest<{ - data: { id: string }[]; - }>({ - url: `${baseUrl}/models`, - method: HttpMethod.GET, - authentication: { - type: AuthenticationType.BEARER_TOKEN, - token: auth.auth as string, - }, - }); - return { - valid: true, - }; - } catch (e) { - return { - valid: false, - error: 'Invalid API key', - }; - } - }, + description: markdownDescription, + displayName: 'API Key', + required: true, + validate: async (auth) => { + try { + await httpClient.sendRequest<{ + data: { id: string }[]; + }>({ + url: `${baseUrl}/models`, + method: HttpMethod.GET, + authentication: { + type: AuthenticationType.BEARER_TOKEN, + token: auth.auth as string, + }, + }); + return { + valid: true, + }; + } catch (e) { + return { + valid: false, + error: 'Invalid API key', + }; + } + }, }); export const openai = createPiece({ - displayName: 'OpenAI', - description: 'Use the many tools ChatGPT has to offer.', - minimumSupportedRelease: '0.5.0', - logoUrl: 'https://cdn.activepieces.com/pieces/openai.png', - categories: [PieceCategory.ARTIFICIAL_INTELLIGENCE], - auth: openaiAuth, - actions: [ - askOpenAI, - askAssistant, - generateImage, - visionPrompt, - textToSpeech, - transcribeAction, - translateAction, - createCustomApiCallAction({ - auth: openaiAuth, - baseUrl: () => baseUrl, - authMapping: (auth) => { - return { - Authorization: `Bearer ${auth}`, - }; - }, - }), - ], - authors: ["aboudzein","astorozhevsky","Willianwg","Nilesh","Salem-Alaa","kishanprmr","MoShizzle","khaledmashaly","abuaboud"], - triggers: [], + displayName: 'OpenAI', + description: 'Use the many tools ChatGPT has to offer.', + minimumSupportedRelease: '0.5.0', + logoUrl: 'https://cdn.activepieces.com/pieces/openai.png', + categories: [PieceCategory.ARTIFICIAL_INTELLIGENCE], + auth: openaiAuth, + actions: [ + askOpenAI, + askAssistant, + generateImage, + visionPrompt, + textToSpeech, + transcribeAction, + translateAction, + extractStructuredDataAction, + createCustomApiCallAction({ + auth: openaiAuth, + baseUrl: () => baseUrl, + authMapping: (auth) => { + return { + Authorization: `Bearer ${auth}`, + }; + }, + }), + ], + authors: [ + 'aboudzein', + 'astorozhevsky', + 'Willianwg', + 'Nilesh', + 'Salem-Alaa', + 'kishanprmr', + 'MoShizzle', + 'khaledmashaly', + 'abuaboud', + ], + triggers: [], }); diff --git a/packages/pieces/community/openai/src/lib/actions/extract-structure-data.action.ts b/packages/pieces/community/openai/src/lib/actions/extract-structure-data.action.ts new file mode 100644 index 0000000000..4539d103f9 --- /dev/null +++ b/packages/pieces/community/openai/src/lib/actions/extract-structure-data.action.ts @@ -0,0 +1,146 @@ +import { openaiAuth } from '../../'; +import { createAction, Property } from '@activepieces/pieces-framework'; +import OpenAI from 'openai'; +import { notLLMs } from '../common/common'; + +export const extractStructuredDataAction = createAction({ + auth: openaiAuth, + name: 'extract-structured-data', + displayName: 'Extract Structured Data from Text', + description: 'Returns structured data from provided unstructured text.', + props: { + model: Property.Dropdown({ + displayName: 'Model', + required: true, + refreshers: [], + defaultValue: 'gpt-3.5-turbo', + options: async ({ auth }) => { + if (!auth) { + return { + disabled: true, + placeholder: 'Enter your API key first', + options: [], + }; + } + try { + const openai = new OpenAI({ + apiKey: auth as string, + }); + const response = await openai.models.list(); + // We need to get only LLM models + const models = response.data.filter((model) => !notLLMs.includes(model.id)); + return { + disabled: false, + options: models.map((model) => { + return { + label: model.id, + value: model.id, + }; + }), + }; + } catch (error) { + return { + disabled: true, + options: [], + placeholder: "Couldn't load models, API key is invalid", + }; + } + }, + }), + text: Property.LongText({ + displayName: 'Unstructured Text', + required: true, + }), + params: Property.Array({ + displayName: 'Data Definition', + required: true, + properties: { + propName: Property.ShortText({ + displayName: 'Name', + description: + 'Provide the name of the value you want to extract from the unstructured text. The name should be unique and short. ', + required: true, + }), + propDescription: Property.LongText({ + displayName: 'Description', + description: + 'Brief description of the data, this hints for the AI on what to look for', + required: false, + }), + propDataType: Property.StaticDropdown({ + displayName: 'Data Type', + description: 'Type of parameter.', + required: true, + defaultValue: 'string', + options: { + disabled: false, + options: [ + { label: 'Text', value: 'string' }, + { label: 'Number', value: 'number' }, + { label: 'Boolean', value: 'boolean' }, + ], + }, + }), + propIsRequired: Property.Checkbox({ + displayName: 'Fail if Not present?', + required: true, + defaultValue: false, + }), + }, + }), + }, + async run(context) { + const { model, text } = context.propsValue; + const paramInputArray = context.propsValue.params as ParamInput[]; + const functionParams: Record = {}; + const requiredFunctionParams: string[] = []; + for (const param of paramInputArray) { + functionParams[param.propName] = { + type: param.propDataType, + description: param.propDescription ?? param.propName, + }; + if (param.propIsRequired) { + requiredFunctionParams.push(param.propName); + } + } + const prompt = 'Extract the following data from the provided text' + const openai = new OpenAI({ + apiKey: context.auth, + }); + + const response = await openai.chat.completions.create({ + model: model, + messages: [{ role: 'user', content: text }], + tools: [ + { + type: 'function', + function: { + name: 'extract_structured_data', + description: prompt, + parameters: { + type: 'object', + properties: functionParams, + required: requiredFunctionParams, + }, + }, + }, + ], + }); + + const toolCallsResponse = response.choices[0].message.tool_calls; + if (toolCallsResponse) { + return JSON.parse(toolCallsResponse[0].function.arguments); + } else { + throw new Error(JSON.stringify({ + message: "OpenAI couldn't extract the fields from the above text." + })); + } + }, +}); + +interface ParamInput { + propName: string; + propDescription: string; + propDataType: string; + propIsRequired: boolean; +} diff --git a/packages/pieces/community/pdf/package.json b/packages/pieces/community/pdf/package.json index 6c38c0d693..a6f0e7b593 100644 --- a/packages/pieces/community/pdf/package.json +++ b/packages/pieces/community/pdf/package.json @@ -3,7 +3,7 @@ "version": "0.0.1", "dependencies": { "@activepieces/pieces-framework": "*", - "pdf-text-reader": "4.0.1", + "pdf-text-reader": "4.1.0", "tslib": "2.6.2" }, "main": "./src/index.js", diff --git a/packages/pieces/community/poper/.eslintrc.json b/packages/pieces/community/poper/.eslintrc.json new file mode 100644 index 0000000000..4a4e695c54 --- /dev/null +++ b/packages/pieces/community/poper/.eslintrc.json @@ -0,0 +1,33 @@ +{ + "extends": [ + "../../../../.eslintrc.base.json" + ], + "ignorePatterns": [ + "!**/*" + ], + "overrides": [ + { + "files": [ + "*.ts", + "*.tsx", + "*.js", + "*.jsx" + ], + "rules": {} + }, + { + "files": [ + "*.ts", + "*.tsx" + ], + "rules": {} + }, + { + "files": [ + "*.js", + "*.jsx" + ], + "rules": {} + } + ] +} \ No newline at end of file diff --git a/packages/pieces/community/poper/README.md b/packages/pieces/community/poper/README.md new file mode 100644 index 0000000000..9deb0c9627 --- /dev/null +++ b/packages/pieces/community/poper/README.md @@ -0,0 +1,7 @@ +# pieces-poper + +This library was generated with [Nx](https://nx.dev). + +## Building + +Run `nx build pieces-poper` to build the library. diff --git a/packages/pieces/community/poper/package.json b/packages/pieces/community/poper/package.json new file mode 100644 index 0000000000..e510c65446 --- /dev/null +++ b/packages/pieces/community/poper/package.json @@ -0,0 +1,4 @@ +{ + "name": "@activepieces/piece-poper", + "version": "0.0.1" +} diff --git a/packages/pieces/community/poper/project.json b/packages/pieces/community/poper/project.json new file mode 100644 index 0000000000..89e68c6793 --- /dev/null +++ b/packages/pieces/community/poper/project.json @@ -0,0 +1,38 @@ +{ + "name": "pieces-poper", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "packages/pieces/community/poper/src", + "projectType": "library", + "targets": { + "build": { + "executor": "@nx/js:tsc", + "outputs": [ + "{options.outputPath}" + ], + "options": { + "outputPath": "dist/packages/pieces/community/poper", + "tsConfig": "packages/pieces/community/poper/tsconfig.lib.json", + "packageJson": "packages/pieces/community/poper/package.json", + "main": "packages/pieces/community/poper/src/index.ts", + "assets": [ + "packages/pieces/community/poper/*.md" + ], + "buildableProjectDepsInPackageJsonType": "dependencies", + "updateBuildableProjectDepsInPackageJson": true + } + }, + "publish": { + "command": "node tools/scripts/publish.mjs pieces-poper {args.ver} {args.tag}", + "dependsOn": [ + "build" + ] + }, + "lint": { + "executor": "@nx/eslint:lint", + "outputs": [ + "{options.outputFile}" + ] + } + }, + "tags": [] +} \ No newline at end of file diff --git a/packages/pieces/community/poper/src/index.ts b/packages/pieces/community/poper/src/index.ts new file mode 100644 index 0000000000..b70c9b5ee3 --- /dev/null +++ b/packages/pieces/community/poper/src/index.ts @@ -0,0 +1,56 @@ +import { createPiece, PieceAuth } from '@activepieces/pieces-framework'; +import { newLead } from './lib/triggers/new-lead'; +import { HttpMethod, HttpRequest, httpClient } from '@activepieces/pieces-common'; +import { PieceCategory } from '@activepieces/shared'; + +export const poperAuth = PieceAuth.SecretText({ + displayName: 'API Key', + description: + 'Use the Poper API key to authenticate the piece. You can find your API key in the Poper settings.', + required: true, + validate: async ({ auth }) => { + const request: HttpRequest = { + method: HttpMethod.POST, + url: 'https://api.poper.ai/general/v1/ping', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: { + api_key: auth, + }, + }; + + try { + const res = await httpClient.sendRequest(request); + + if (res.status === 200) { + return { + valid: true, + }; + } + + return { + valid: false, + error: 'API Key is invalid', + }; + } catch (e) { + return { + valid: false, + error: 'API Key is invalid', + }; + } + }, +}); + +export const poper = createPiece({ + displayName: 'Poper', + auth: poperAuth, + minimumSupportedRelease: '0.20.0', + categories: [PieceCategory.MARKETING], + description: + 'AI Driven Pop-up Builder that can convert visitors into customers,increase subscriber count, and skyrocket sales.', + logoUrl: 'https://cdn.activepieces.com/pieces/poper.png', + authors: ['thirstycode'], + actions: [], + triggers: [newLead], +}); diff --git a/packages/pieces/community/poper/src/lib/triggers/new-lead.ts b/packages/pieces/community/poper/src/lib/triggers/new-lead.ts new file mode 100644 index 0000000000..38c6b8f9cb --- /dev/null +++ b/packages/pieces/community/poper/src/lib/triggers/new-lead.ts @@ -0,0 +1,143 @@ + +import { createTrigger, TriggerStrategy, PiecePropValueSchema, StaticPropsValue, DropdownProperty } from '@activepieces/pieces-framework'; +import { DedupeStrategy, Polling, pollingHelper, HttpMethod, HttpRequest, httpClient } from '@activepieces/pieces-common'; +import { poperAuth } from './../../'; + +import { Property } from '@activepieces/pieces-framework'; + +const buildEmptyList = ({ placeholder }: { placeholder: string }) => { + return { + disabled: true, + options: [], + placeholder, + }; +}; + +const popupsDropdown = Property.Dropdown({ + displayName: 'Popup Name', + refreshers: [], + description: "Select the popup name to trigger the action", + required: true, + options: async ({ auth }) => { + if (!auth) { + return buildEmptyList({ + placeholder: 'Please select an authentication', + }); + } + + const request: HttpRequest = { + method: HttpMethod.POST, + url: 'https://api.poper.ai/general/v1/popup/list', + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: { + api_key: auth, + } + }; + + try{ + + const res = await httpClient.sendRequest(request); + + if(res.status === 200){ + const popups = res.body.popups; + if (popups.length === 0) { + return buildEmptyList({ + placeholder: 'No popups found! Please create a popup.', + }); + } + + const options = popups.map((p:any) => ({ + label: p.name, + value: p.id, + })); + + return { + disabled: false, + options, + }; + } + + return buildEmptyList({ + placeholder: 'Not authorized.', + }); + } catch(e){ + return buildEmptyList({ + placeholder: 'Not authorized.', + }); + } + + }, +}); + +// replace auth with piece auth variable +const polling: Polling< PiecePropValueSchema, StaticPropsValue<{ popup_id: DropdownProperty | DropdownProperty; }> > = { + strategy: DedupeStrategy.LAST_ITEM, + items: async ({ auth, propsValue }) => { + // implement the logic to fetch the items + const request: HttpRequest = { + method: HttpMethod.POST, + url: 'https://api.poper.ai/general/v1/popup/responses', + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: { + api_key: auth, + popup_id: propsValue.popup_id, + } + }; + + try{ + + const res = await httpClient.sendRequest(request); + + if(res.status === 200){ + const responses = res.body.responses; + if (responses.length === 0) { + return []; + } + + return responses.map((response:any) => ({ + id: response.id, + data: response, + })); + } + + return []; + } catch(e){ + return []; + } + }, + +} + +export const newLead = createTrigger({ +auth: poperAuth, +name: 'newLead', +displayName: 'New Lead', +description: 'Triggers when a new lead is obtained from popup', +props: { + popup_id: popupsDropdown, +}, +sampleData: {}, +type: TriggerStrategy.POLLING, +async test(context) { + const { store, auth, propsValue } = context; + return await pollingHelper.test(polling, { store, auth, propsValue }); +}, +async onEnable(context) { + const { store, auth, propsValue } = context; + await pollingHelper.onEnable(polling, { store, auth, propsValue }); +}, + +async onDisable(context) { + const { store, auth, propsValue } = context; + await pollingHelper.onDisable(polling, { store, auth, propsValue }); +}, + +async run(context) { + const { store, auth, propsValue } = context; + return await pollingHelper.poll(polling, { store, auth, propsValue }); +}, +}); \ No newline at end of file diff --git a/packages/pieces/community/poper/tsconfig.json b/packages/pieces/community/poper/tsconfig.json new file mode 100644 index 0000000000..059cd81661 --- /dev/null +++ b/packages/pieces/community/poper/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ] +} diff --git a/packages/pieces/community/poper/tsconfig.lib.json b/packages/pieces/community/poper/tsconfig.lib.json new file mode 100644 index 0000000000..28369ef762 --- /dev/null +++ b/packages/pieces/community/poper/tsconfig.lib.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "commonjs", + "outDir": "../../../../dist/out-tsc", + "declaration": true, + "types": ["node"] + }, + "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"], + "include": ["src/**/*.ts"] +} diff --git a/packages/pieces/community/todoist/package.json b/packages/pieces/community/todoist/package.json index d3e96128c4..d3dbd499dc 100644 --- a/packages/pieces/community/todoist/package.json +++ b/packages/pieces/community/todoist/package.json @@ -1,4 +1,4 @@ { "name": "@activepieces/piece-todoist", - "version": "0.3.7" + "version": "0.3.8" } \ No newline at end of file diff --git a/packages/pieces/community/todoist/src/index.ts b/packages/pieces/community/todoist/src/index.ts index 566ee16d21..51d10edd8a 100644 --- a/packages/pieces/community/todoist/src/index.ts +++ b/packages/pieces/community/todoist/src/index.ts @@ -3,31 +3,37 @@ import { OAuth2PropertyValue, PieceAuth, createPiece } from '@activepieces/piece import { PieceCategory } from '@activepieces/shared'; import { todoistCreateTaskAction } from './lib/actions/create-task-action'; import { todoistTaskCompletedTrigger } from './lib/triggers/task-completed-trigger'; +import { todoistUpdateTaskAction } from './lib/actions/update-task.action'; +import { todoistFindTaskAction } from './lib/actions/find-task.action'; +import { todoistMarkTaskCompletedAction } from './lib/actions/mark-task-completed.action'; export const todoistAuth = PieceAuth.OAuth2({ - required: true, - authUrl: 'https://todoist.com/oauth/authorize', - tokenUrl: 'https://todoist.com/oauth/access_token', - scope: ['data:read_write'], + required: true, + authUrl: 'https://todoist.com/oauth/authorize', + tokenUrl: 'https://todoist.com/oauth/access_token', + scope: ['data:read_write'], }); export const todoist = createPiece({ - displayName: 'Todoist', - description: 'To-do list and task manager', - minimumSupportedRelease: '0.5.0', - logoUrl: 'https://cdn.activepieces.com/pieces/todoist.png', - authors: ["MyWay","kishanprmr","MoShizzle","khaledmashaly","abuaboud"], - categories: [PieceCategory.PRODUCTIVITY], - auth: todoistAuth, - actions: [ - todoistCreateTaskAction, - createCustomApiCallAction({ - baseUrl: () => 'https://api.todoist.com/rest/v2', - auth: todoistAuth, - authMapping: (auth) => ({ - Authorization: `Bearer ${(auth as OAuth2PropertyValue).access_token}`, - }), - }), - ], - triggers: [todoistTaskCompletedTrigger], + displayName: 'Todoist', + description: 'To-do list and task manager', + minimumSupportedRelease: '0.5.0', + logoUrl: 'https://cdn.activepieces.com/pieces/todoist.png', + authors: ['MyWay', 'kishanprmr', 'MoShizzle', 'khaledmashaly', 'abuaboud'], + categories: [PieceCategory.PRODUCTIVITY], + auth: todoistAuth, + actions: [ + todoistCreateTaskAction, + todoistUpdateTaskAction, + todoistFindTaskAction, + todoistMarkTaskCompletedAction, + createCustomApiCallAction({ + baseUrl: () => 'https://api.todoist.com/rest/v2', + auth: todoistAuth, + authMapping: (auth) => ({ + Authorization: `Bearer ${(auth as OAuth2PropertyValue).access_token}`, + }), + }), + ], + triggers: [todoistTaskCompletedTrigger], }); diff --git a/packages/pieces/community/todoist/src/lib/actions/create-task-action.ts b/packages/pieces/community/todoist/src/lib/actions/create-task-action.ts index 566d7a63a8..2f0d7411a9 100644 --- a/packages/pieces/community/todoist/src/lib/actions/create-task-action.ts +++ b/packages/pieces/community/todoist/src/lib/actions/create-task-action.ts @@ -1,70 +1,65 @@ import { createAction, Property } from '@activepieces/pieces-framework'; import { assertNotNullOrUndefined } from '@activepieces/shared'; import { todoistRestClient } from '../common/client/rest-client'; -import { todoistProjectIdDropdown } from '../common/props'; +import { todoistProjectIdDropdown, todoistSectionIdDropdown } from '../common/props'; import { TodoistCreateTaskRequest } from '../common/models'; import { todoistAuth } from '../..'; export const todoistCreateTaskAction = createAction({ - auth: todoistAuth, - name: 'create_task', - displayName: 'Create Task', - description: 'Create task', - props: { - project_id: todoistProjectIdDropdown, - content: Property.LongText({ - displayName: 'content', - description: - "The task's content. It may contain some markdown-formatted text and hyperlinks", - required: true, - }), - description: Property.LongText({ - displayName: 'Description', - description: - 'A description for the task. This value may contain some markdown-formatted text and hyperlinks.', - required: false, - }), - labels: Property.Array({ - displayName: 'Labels', - required: false, - description: - "The task's labels (a list of names that may represent either personal or shared labels)", - }), - priority: Property.Number({ - displayName: 'Priority', - description: 'Task priority from 1 (normal) to 4 (urgent)', - required: false, - }), - due_date: Property.ShortText({ - displayName: 'Due date', - description: - "Specific date in YYYY-MM-DD format relative to user's timezone", - required: false, - }), - section_id: Property.ShortText({ - displayName: 'Section', - description: - "A section for the task. It should be a Section ID under the same project", - required: false, - }), - }, + auth: todoistAuth, + name: 'create_task', + displayName: 'Create Task', + description: 'Create task', + props: { + project_id: todoistProjectIdDropdown( + "Task project ID. If not set, task is put to user's Inbox.", + ), + content: Property.LongText({ + displayName: 'content', + description: "The task's content. It may contain some markdown-formatted text and hyperlinks", + required: true, + }), + description: Property.LongText({ + displayName: 'Description', + description: + 'A description for the task. This value may contain some markdown-formatted text and hyperlinks.', + required: false, + }), + labels: Property.Array({ + displayName: 'Labels', + required: false, + description: + "The task's labels (a list of names that may represent either personal or shared labels)", + }), + priority: Property.Number({ + displayName: 'Priority', + description: 'Task priority from 1 (normal) to 4 (urgent)', + required: false, + }), + due_date: Property.ShortText({ + displayName: 'Due date', + description: "Specific date in YYYY-MM-DD format relative to user's timezone", + required: false, + }), + section_id: todoistSectionIdDropdown, + }, - async run({ auth, propsValue }) { - const token = auth.access_token; - const { project_id, content, description, labels, priority, due_date, section_id} = - propsValue as TodoistCreateTaskRequest; + async run({ auth, propsValue }) { + const token = auth.access_token; + const { project_id, content, description, labels, priority, due_date, section_id } = + propsValue as TodoistCreateTaskRequest; - assertNotNullOrUndefined(token, 'token'); - assertNotNullOrUndefined(content, 'content'); - return await todoistRestClient.tasks.create({ - token, - project_id, - content, - description, - labels, - priority, - due_date, - section_id - }); - }, + assertNotNullOrUndefined(token, 'token'); + assertNotNullOrUndefined(content, 'content'); + return await todoistRestClient.tasks.create({ + token, + project_id, + content, + description, + labels, + priority, + due_date, + section_id, + }); + }, }); diff --git a/packages/pieces/community/todoist/src/lib/actions/find-task.action.ts b/packages/pieces/community/todoist/src/lib/actions/find-task.action.ts new file mode 100644 index 0000000000..e7cf8a84ba --- /dev/null +++ b/packages/pieces/community/todoist/src/lib/actions/find-task.action.ts @@ -0,0 +1,36 @@ +import { todoistAuth } from '../..'; +import { createAction, Property } from '@activepieces/pieces-framework'; +import { todoistProjectIdDropdown } from '../common/props'; +import { todoistRestClient } from '../common/client/rest-client'; +import { assertNotNullOrUndefined } from '@activepieces/shared'; + +export const todoistFindTaskAction = createAction({ + auth: todoistAuth, + name: 'find_task', + displayName: 'Find Task', + description: 'Finds a task by name.', + props: { + name: Property.ShortText({ + displayName: 'Name', + description: 'The name of the task to search for.', + required: true, + }), + project_id: todoistProjectIdDropdown( + 'Search for tasks within the selected project. If left blank, then all projects are searched.', + ), + }, + async run(context) { + const token = context.auth.access_token; + const { name, project_id } = context.propsValue; + + assertNotNullOrUndefined(token, 'token'); + const tasks = await todoistRestClient.tasks.list({ token, project_id }); + + const matchedTask = tasks.find((task) => task.content == name); + if (!matchedTask) { + throw new Error('Task not found'); + } else { + return matchedTask; + } + }, +}); diff --git a/packages/pieces/community/todoist/src/lib/actions/mark-task-completed.action.ts b/packages/pieces/community/todoist/src/lib/actions/mark-task-completed.action.ts new file mode 100644 index 0000000000..307ef05fa8 --- /dev/null +++ b/packages/pieces/community/todoist/src/lib/actions/mark-task-completed.action.ts @@ -0,0 +1,25 @@ +import { assertNotNullOrUndefined } from '@activepieces/shared'; +import { todoistAuth } from '../..'; +import { createAction, Property } from '@activepieces/pieces-framework'; +import { todoistRestClient } from '../common/client/rest-client'; + +export const todoistMarkTaskCompletedAction = createAction({ + auth: todoistAuth, + name: 'mark_task_completed', + displayName: 'Mark Task as Completed', + description: 'Marks a task as being completed.', + props: { + task_id: Property.ShortText({ + displayName: 'Task ID', + required: true, + }), + }, + async run(context) { + const token = context.auth.access_token; + const { task_id } = context.propsValue; + + assertNotNullOrUndefined(token, 'token'); + + return await todoistRestClient.tasks.close({ token, task_id }); + }, +}); diff --git a/packages/pieces/community/todoist/src/lib/actions/update-task.action.ts b/packages/pieces/community/todoist/src/lib/actions/update-task.action.ts new file mode 100644 index 0000000000..5dced83b92 --- /dev/null +++ b/packages/pieces/community/todoist/src/lib/actions/update-task.action.ts @@ -0,0 +1,61 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { assertNotNullOrUndefined } from '@activepieces/shared'; +import { todoistRestClient } from '../common/client/rest-client'; +import { todoistAuth } from '../..'; + +export const todoistUpdateTaskAction = createAction({ + auth: todoistAuth, + name: 'update_task', + displayName: 'Update Task', + description: 'Updates an existing task.', + props: { + task_id: Property.ShortText({ + displayName: 'Task ID', + required: true, + }), + content: Property.LongText({ + displayName: 'content', + description: "The task's content. It may contain some markdown-formatted text and hyperlinks", + required: false, + }), + description: Property.LongText({ + displayName: 'Description', + description: + 'A description for the task. This value may contain some markdown-formatted text and hyperlinks.', + required: false, + }), + labels: Property.Array({ + displayName: 'Labels', + required: false, + description: + "The task's labels (a list of names that may represent either personal or shared labels)", + }), + priority: Property.Number({ + displayName: 'Priority', + description: 'Task priority from 1 (normal) to 4 (urgent)', + required: false, + }), + due_date: Property.ShortText({ + displayName: 'Due date', + description: "Specific date in YYYY-MM-DD format relative to user's timezone", + required: false, + }), + }, + + async run({ auth, propsValue }) { + const token = auth.access_token; + const { task_id, content, description, priority, due_date } = propsValue; + const labels = propsValue.labels as string[]; + + assertNotNullOrUndefined(token, 'token'); + return await todoistRestClient.tasks.update({ + token, + task_id, + content, + description, + labels, + priority, + due_date, + }); + }, +}); diff --git a/packages/pieces/community/todoist/src/lib/common/client/rest-client.ts b/packages/pieces/community/todoist/src/lib/common/client/rest-client.ts index 8e54bb1fe0..fdd6d4b411 100644 --- a/packages/pieces/community/todoist/src/lib/common/client/rest-client.ts +++ b/packages/pieces/community/todoist/src/lib/common/client/rest-client.ts @@ -1,104 +1,166 @@ import { - HttpRequest, - HttpMethod, - AuthenticationType, - httpClient, + HttpRequest, + HttpMethod, + AuthenticationType, + httpClient, } from '@activepieces/pieces-common'; import { isNotUndefined, pickBy } from '@activepieces/shared'; import { - TodoistCreateTaskRequest, - TodoistProject, - TodoistTask, + TodoistCreateTaskRequest, + TodoistProject, + TodoistSection, + TodoistTask, + TodoistUpdateTaskRequest, } from '../models'; const API = 'https://api.todoist.com/rest/v2'; export const todoistRestClient = { - projects: { - async list({ token }: ProjectsListParams): Promise { - const request: HttpRequest = { - method: HttpMethod.GET, - url: `${API}/projects`, - authentication: { - type: AuthenticationType.BEARER_TOKEN, - token, - }, - }; - - const response = await httpClient.sendRequest(request); - return response.body; - }, - }, - - tasks: { - async create({ - token, - project_id, - content, - description, - labels, - priority, - due_date, - section_id - }: TasksCreateParams): Promise { - const request: HttpRequest = { - method: HttpMethod.POST, - url: `${API}/tasks`, - authentication: { - type: AuthenticationType.BEARER_TOKEN, - token, - }, - body: { - content, - project_id, - description, - labels, - priority, - due_date, - section_id - }, - }; - - const response = await httpClient.sendRequest(request); - return response.body; - }, - - async list({ - token, - project_id, - filter, - }: TasksListParams): Promise { - const queryParams = { - filter, - project_id, - }; - - const request: HttpRequest = { - method: HttpMethod.GET, - url: `${API}/tasks`, - authentication: { - type: AuthenticationType.BEARER_TOKEN, - token, - }, - queryParams: pickBy(queryParams, isNotUndefined), - }; - - const response = await httpClient.sendRequest(request); - return response.body; - }, - }, + projects: { + async list({ token }: ProjectsListParams): Promise { + const request: HttpRequest = { + method: HttpMethod.GET, + url: `${API}/projects`, + authentication: { + type: AuthenticationType.BEARER_TOKEN, + token, + }, + }; + + const response = await httpClient.sendRequest(request); + return response.body; + }, + }, + + sections: { + async list(params: SectionsListPrams): Promise { + const qs: Record = {}; + if (params.project_id) qs['project_id'] = params.project_id; + + const request: HttpRequest = { + method: HttpMethod.GET, + url: `${API}/sections`, + authentication: { + type: AuthenticationType.BEARER_TOKEN, + token: params.token, + }, + queryParams: qs, + }; + + const response = await httpClient.sendRequest(request); + return response.body; + }, + }, + + tasks: { + async create({ + token, + project_id, + content, + description, + labels, + priority, + due_date, + section_id, + }: TasksCreateParams): Promise { + const request: HttpRequest = { + method: HttpMethod.POST, + url: `${API}/tasks`, + authentication: { + type: AuthenticationType.BEARER_TOKEN, + token, + }, + body: { + content, + project_id, + description, + labels, + priority, + due_date, + section_id, + }, + }; + + const response = await httpClient.sendRequest(request); + return response.body; + }, + + async update(params: TasksUpdateParams): Promise { + const request: HttpRequest = { + method: HttpMethod.POST, + url: `${API}/tasks/${params.task_id}`, + authentication: { + type: AuthenticationType.BEARER_TOKEN, + token: params.token, + }, + body: { + content: params.content, + description: params.description, + labels: params.labels?.length === 0 ? undefined : params.labels, + priority: params.priority, + due_date: params.due_date, + }, + }; + + const response = await httpClient.sendRequest(request); + return response.body; + }, + + async list({ token, project_id, filter }: TasksListParams): Promise { + const queryParams = { + filter, + project_id, + }; + + const request: HttpRequest = { + method: HttpMethod.GET, + url: `${API}/tasks`, + authentication: { + type: AuthenticationType.BEARER_TOKEN, + token, + }, + queryParams: pickBy(queryParams, isNotUndefined), + }; + + const response = await httpClient.sendRequest(request); + return response.body; + }, + + async close({ token, task_id }: { token: string; task_id: string }) { + const request: HttpRequest = { + method: HttpMethod.POST, + url: `${API}/tasks/${task_id}/close`, + authentication: { + type: AuthenticationType.BEARER_TOKEN, + token, + }, + }; + + const response = await httpClient.sendRequest(request); + return response.body; + }, + }, }; type ProjectsListParams = { - token: string; + token: string; +}; + +type SectionsListPrams = { + token: string; + project_id?: string; }; type TasksCreateParams = { - token: string; + token: string; } & TodoistCreateTaskRequest; +type TasksUpdateParams = { + token: string; +} & TodoistUpdateTaskRequest; + type TasksListParams = { - token: string; - project_id?: string | undefined; - filter?: string | undefined; + token: string; + project_id?: string | undefined; + filter?: string | undefined; }; diff --git a/packages/pieces/community/todoist/src/lib/common/models.ts b/packages/pieces/community/todoist/src/lib/common/models.ts index 87c5e44cc8..c428ce1773 100644 --- a/packages/pieces/community/todoist/src/lib/common/models.ts +++ b/packages/pieces/community/todoist/src/lib/common/models.ts @@ -1,57 +1,73 @@ export type TodoistProject = { - id: string; - name: string; + id: string; + name: string; +}; + +export type TodoistSection = { + id: string; + name: string; + project_id: string; + order: number; }; export type TodoistCreateTaskRequest = { - content: string; - project_id?: string | undefined; - description?: string | undefined; - labels?: Array | undefined; - priority?: number | undefined; - due_date?: string | undefined; - section_id?: string | undefined; + content: string; + project_id?: string | undefined; + description?: string | undefined; + labels?: Array | undefined; + priority?: number | undefined; + due_date?: string | undefined; + section_id?: string | undefined; +}; + +export type TodoistUpdateTaskRequest = { + task_id: string; + content?: string; + description?: string; + labels?: Array; + priority?: number; + due_date?: string; }; type TodoistTaskDue = { - string: string; - date: string; - is_recurring: boolean; - datetime?: string | undefined; - timezone?: string | undefined; + string: string; + date: string; + is_recurring: boolean; + datetime?: string | undefined; + timezone?: string | undefined; }; export type TodoistTask = { - id: string; - projectId: string | null; - sectionId: string | null; - content: string; - description?: string | undefined; - is_completed: boolean; - labels: string[]; - parent_id: string | null; - order: number; - priority: number; - due: TodoistTaskDue | null; - url: string; - comment_count: number; - created_at: string; - creator_id: string; - assignee_id: string | null; - assigner_id: string | null; + id: string; + projectId: string | null; + sectionId: string | null; + content: string; + description?: string | undefined; + is_completed: boolean; + labels: string[]; + parent_id: string | null; + order: number; + priority: number; + due: TodoistTaskDue | null; + url: string; + comment_count: number; + created_at: string; + creator_id: string; + assignee_id: string | null; + assigner_id: string | null; }; export type TodoistCompletedTask = { - id: string; - task_id: string; - user_id: string; - project_id: string; - section_id: string; - content: string; - completed_at: string; - note_count: number; + id: string; + task_id: string; + user_id: string; + project_id: string; + section_id: string; + content: string; + completed_at: string; + note_count: number; }; export type TodoistCompletedListResponse = { - items: TodoistCompletedTask[]; + items: TodoistCompletedTask[]; }; diff --git a/packages/pieces/community/todoist/src/lib/common/props.ts b/packages/pieces/community/todoist/src/lib/common/props.ts index 94973caf6a..0fc04a559f 100644 --- a/packages/pieces/community/todoist/src/lib/common/props.ts +++ b/packages/pieces/community/todoist/src/lib/common/props.ts @@ -2,42 +2,76 @@ import { OAuth2PropertyValue, Property } from '@activepieces/pieces-framework'; import { todoistRestClient } from './client/rest-client'; const buildEmptyList = ({ placeholder }: { placeholder: string }) => { - return { - disabled: true, - options: [], - placeholder, - }; + return { + disabled: true, + options: [], + placeholder, + }; }; -export const todoistProjectIdDropdown = Property.Dropdown({ - displayName: 'Project', - refreshers: [], - description: "Task project ID. If not set, task is put to user's Inbox.", - required: false, - options: async ({ auth }) => { - if (!auth) { - return buildEmptyList({ - placeholder: 'Please select an authentication', - }); - } - - const token = (auth as OAuth2PropertyValue).access_token; - const projects = await todoistRestClient.projects.list({ token }); - - if (projects.length === 0) { - return buildEmptyList({ - placeholder: 'No projects found! Please create a project.', - }); - } - - const options = projects.map((p) => ({ - label: p.name, - value: p.id, - })); - - return { - disabled: false, - options, - }; - }, +export const todoistProjectIdDropdown = (description: string) => + Property.Dropdown({ + displayName: 'Project', + refreshers: [], + description, + required: false, + options: async ({ auth }) => { + if (!auth) { + return buildEmptyList({ + placeholder: 'Please select an authentication', + }); + } + + const token = (auth as OAuth2PropertyValue).access_token; + const projects = await todoistRestClient.projects.list({ token }); + + if (projects.length === 0) { + return buildEmptyList({ + placeholder: 'No projects found! Please create a project.', + }); + } + + const options = projects.map((p) => ({ + label: p.name, + value: p.id, + })); + + return { + disabled: false, + options, + }; + }, + }); + +export const todoistSectionIdDropdown = Property.Dropdown({ + displayName: 'Section', + refreshers: ['project_id'], + required: false, + options: async ({ auth, project_id }) => { + if (!auth) { + return buildEmptyList({ + placeholder: 'Please select an authentication', + }); + } + + const token = (auth as OAuth2PropertyValue).access_token; + const projectId = project_id as string | undefined; + const sections = await todoistRestClient.sections.list({ token, project_id: projectId }); + + if (sections.length === 0) { + return buildEmptyList({ + placeholder: 'No sections found! Please create a section.', + }); + } + + const options = sections.map((p) => ({ + label: p.name, + value: p.id, + })); + + return { + disabled: false, + options, + }; + }, }); diff --git a/packages/pieces/community/todoist/src/lib/triggers/task-completed-trigger.ts b/packages/pieces/community/todoist/src/lib/triggers/task-completed-trigger.ts index fddb9883ec..e6b058e623 100644 --- a/packages/pieces/community/todoist/src/lib/triggers/task-completed-trigger.ts +++ b/packages/pieces/community/todoist/src/lib/triggers/task-completed-trigger.ts @@ -7,7 +7,7 @@ import { todoistProjectIdDropdown } from '../common/props'; import { todoistAuth } from '../..'; type TriggerData = { - lastChecked: string; + lastChecked: string; }; const TRIGGER_DATA_STORE_KEY = 'todoist_task_completed_trigger_data'; @@ -17,60 +17,62 @@ const fiveMinutesAgo = () => dayjs().subtract(5, 'minutes').format(ISO_FORMAT); const now = () => dayjs().format(ISO_FORMAT); export const todoistTaskCompletedTrigger = createTrigger({ - auth: todoistAuth, - name: 'task_completed', - displayName: 'Task Completed', - description: 'Triggers when a new task is completed', - type: TriggerStrategy.POLLING, + auth: todoistAuth, + name: 'task_completed', + displayName: 'Task Completed', + description: 'Triggers when a new task is completed', + type: TriggerStrategy.POLLING, - sampleData: { - content: 'Buy Milk', - meta_data: null, - user_id: '2671355', - task_id: '2995104339', - note_count: 0, - project_id: '2203306141', - section_id: '7025', - completed_at: '2015-02-17T15:40:41.000000Z', - id: '1899066186', - }, + sampleData: { + content: 'Buy Milk', + meta_data: null, + user_id: '2671355', + task_id: '2995104339', + note_count: 0, + project_id: '2203306141', + section_id: '7025', + completed_at: '2015-02-17T15:40:41.000000Z', + id: '1899066186', + }, - props: { - project_id: todoistProjectIdDropdown, - }, + props: { + project_id: todoistProjectIdDropdown( + 'Leave it blank if you want to get completed tasks from all your projects.', + ), + }, - async onEnable({ store }): Promise { - await store.put(TRIGGER_DATA_STORE_KEY, { - lastChecked: now(), - }); - }, + async onEnable({ store }): Promise { + await store.put(TRIGGER_DATA_STORE_KEY, { + lastChecked: now(), + }); + }, - async onDisable({ store }): Promise { - await store.put(TRIGGER_DATA_STORE_KEY, null); - }, + async onDisable({ store }): Promise { + await store.put(TRIGGER_DATA_STORE_KEY, null); + }, - async run({ auth, propsValue, store }): Promise { - const token = auth.access_token; - const { project_id } = propsValue; + async run({ auth, propsValue, store }): Promise { + const token = auth.access_token; + const { project_id } = propsValue; - assertNotNullOrUndefined(token, 'token'); + assertNotNullOrUndefined(token, 'token'); - const triggerData = await store.get(TRIGGER_DATA_STORE_KEY); - const since = triggerData?.lastChecked ?? fiveMinutesAgo(); - const until = now(); + const triggerData = await store.get(TRIGGER_DATA_STORE_KEY); + const since = triggerData?.lastChecked ?? fiveMinutesAgo(); + const until = now(); - const response = await todoistSyncClient.completed.list({ - token, - since, - until, - project_id, - }); + const response = await todoistSyncClient.completed.list({ + token, + since, + until, + project_id, + }); - await store.put(TRIGGER_DATA_STORE_KEY, { - // It returns data newer than the since parameter, and not equal. - lastChecked: until, - }); + await store.put(TRIGGER_DATA_STORE_KEY, { + // It returns data newer than the since parameter, and not equal. + lastChecked: until, + }); - return response.items; - }, + return response.items; + }, }); diff --git a/packages/pieces/community/trello/package.json b/packages/pieces/community/trello/package.json index 3d2166e726..6450e725bd 100644 --- a/packages/pieces/community/trello/package.json +++ b/packages/pieces/community/trello/package.json @@ -1,4 +1,4 @@ { "name": "@activepieces/piece-trello", - "version": "0.3.5" + "version": "0.3.6" } \ No newline at end of file diff --git a/packages/pieces/community/trello/src/lib/actions/get-card.ts b/packages/pieces/community/trello/src/lib/actions/get-card.ts index 62a173b6d9..10980e9612 100644 --- a/packages/pieces/community/trello/src/lib/actions/get-card.ts +++ b/packages/pieces/community/trello/src/lib/actions/get-card.ts @@ -34,8 +34,6 @@ export const getCard = createAction({ headers: { Accept: 'application/json', }, - body: {}, - queryParams: {}, }; return (await httpClient.sendRequest(request)).body; }, diff --git a/packages/pieces/community/trello/src/lib/common/index.ts b/packages/pieces/community/trello/src/lib/common/index.ts index af1e83b37a..3316d0c7be 100644 --- a/packages/pieces/community/trello/src/lib/common/index.ts +++ b/packages/pieces/community/trello/src/lib/common/index.ts @@ -184,8 +184,6 @@ async function getAuthorisedUser(apikey: string, token: string) { headers: { Accept: 'application/json', }, - body: {}, - queryParams: {}, }; const response = await httpClient.sendRequest(request); @@ -211,8 +209,6 @@ async function listBoards(apikey: string, token: string, user_id: string) { headers: { Accept: 'application/json', }, - body: {}, - queryParams: {}, }; const response = await httpClient.sendRequest<{ id: string; name: string }[]>( request @@ -240,8 +236,6 @@ async function listBoardLists(apikey: string, token: string, board_id: string) { headers: { Accept: 'application/json', }, - body: {}, - queryParams: {}, }; const response = await httpClient.sendRequest<{ id: string; name: string }[]>( request diff --git a/packages/pieces/community/webhook/package.json b/packages/pieces/community/webhook/package.json index 2cb8dfae35..63f73a93d1 100644 --- a/packages/pieces/community/webhook/package.json +++ b/packages/pieces/community/webhook/package.json @@ -1,4 +1,4 @@ { "name": "@activepieces/piece-webhook", - "version": "0.1.2" -} \ No newline at end of file + "version": "0.1.3" +} diff --git a/packages/pieces/community/webhook/src/index.ts b/packages/pieces/community/webhook/src/index.ts index cfc6e926d7..3a62c1952a 100644 --- a/packages/pieces/community/webhook/src/index.ts +++ b/packages/pieces/community/webhook/src/index.ts @@ -7,7 +7,7 @@ export const webhook = createPiece({ description: 'Receive HTTP requests and trigger flows using unique URLs.', auth: PieceAuth.None(), categories: [PieceCategory.CORE], - minimumSupportedRelease: '0.22.0', + minimumSupportedRelease: '0.26.0', logoUrl: 'https://cdn.activepieces.com/pieces/webhook.svg', authors: ['abuaboud', 'pfernandez98'], actions: [], diff --git a/packages/pieces/community/webhook/src/lib/triggers/catch-hook.ts b/packages/pieces/community/webhook/src/lib/triggers/catch-hook.ts index 4c413d1a38..a4a4128d0c 100644 --- a/packages/pieces/community/webhook/src/lib/triggers/catch-hook.ts +++ b/packages/pieces/community/webhook/src/lib/triggers/catch-hook.ts @@ -7,17 +7,21 @@ import { import { assertNotNullOrUndefined } from '@activepieces/shared'; const message = ` -URL: +**Production URL:** \`\`\`text {{webhookUrl}} \`\`\` -If you are expecting a reply from this webhook, append **/sync** to the URL. - -In that case, you will also have to add an HTTP step with **return response** at the end of your flow. +**Testing URL:** +\`\`\`text +{{webhookUrl}}/test +\`\`\` +***Use this URL for testing the webhook and saving sample data. It won't start the flow***. -If the flow takes more than **30 seconds**, it will give a **408 Request Timeout** response. +**Notes:** +- If you are expecting a reply from this webhook, append **/sync** to the URL in that case, you will also have to add an HTTP step with **return response** at the end of your flow. +- If the flow takes more than **30 seconds**, it will give a **408 Request Timeout** response. `; enum AuthType { diff --git a/packages/pieces/community/whatsapp/.eslintrc.json b/packages/pieces/community/whatsapp/.eslintrc.json new file mode 100644 index 0000000000..4a4e695c54 --- /dev/null +++ b/packages/pieces/community/whatsapp/.eslintrc.json @@ -0,0 +1,33 @@ +{ + "extends": [ + "../../../../.eslintrc.base.json" + ], + "ignorePatterns": [ + "!**/*" + ], + "overrides": [ + { + "files": [ + "*.ts", + "*.tsx", + "*.js", + "*.jsx" + ], + "rules": {} + }, + { + "files": [ + "*.ts", + "*.tsx" + ], + "rules": {} + }, + { + "files": [ + "*.js", + "*.jsx" + ], + "rules": {} + } + ] +} \ No newline at end of file diff --git a/packages/pieces/community/whatsapp/README.md b/packages/pieces/community/whatsapp/README.md new file mode 100644 index 0000000000..85a4a45abf --- /dev/null +++ b/packages/pieces/community/whatsapp/README.md @@ -0,0 +1,7 @@ +# pieces-whatsapp + +This library was generated with [Nx](https://nx.dev). + +## Building + +Run `nx build pieces-whatsapp` to build the library. diff --git a/packages/pieces/community/whatsapp/package.json b/packages/pieces/community/whatsapp/package.json new file mode 100644 index 0000000000..7ddc589990 --- /dev/null +++ b/packages/pieces/community/whatsapp/package.json @@ -0,0 +1,4 @@ +{ + "name": "@activepieces/piece-whatsapp", + "version": "0.0.1" +} diff --git a/packages/pieces/community/whatsapp/project.json b/packages/pieces/community/whatsapp/project.json new file mode 100644 index 0000000000..adad8436e1 --- /dev/null +++ b/packages/pieces/community/whatsapp/project.json @@ -0,0 +1,38 @@ +{ + "name": "pieces-whatsapp", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "packages/pieces/community/whatsapp/src", + "projectType": "library", + "targets": { + "build": { + "executor": "@nx/js:tsc", + "outputs": [ + "{options.outputPath}" + ], + "options": { + "outputPath": "dist/packages/pieces/community/whatsapp", + "tsConfig": "packages/pieces/community/whatsapp/tsconfig.lib.json", + "packageJson": "packages/pieces/community/whatsapp/package.json", + "main": "packages/pieces/community/whatsapp/src/index.ts", + "assets": [ + "packages/pieces/community/whatsapp/*.md" + ], + "buildableProjectDepsInPackageJsonType": "dependencies", + "updateBuildableProjectDepsInPackageJson": true + } + }, + "publish": { + "command": "node tools/scripts/publish.mjs pieces-whatsapp {args.ver} {args.tag}", + "dependsOn": [ + "build" + ] + }, + "lint": { + "executor": "@nx/eslint:lint", + "outputs": [ + "{options.outputFile}" + ] + } + }, + "tags": [] +} \ No newline at end of file diff --git a/packages/pieces/community/whatsapp/src/index.ts b/packages/pieces/community/whatsapp/src/index.ts new file mode 100644 index 0000000000..a856dea7e3 --- /dev/null +++ b/packages/pieces/community/whatsapp/src/index.ts @@ -0,0 +1,47 @@ + +import { createPiece, PieceAuth, Property } from "@activepieces/pieces-framework"; +import { sendMessage } from "./lib/actions/send-message"; +import { sendMedia } from "./lib/actions/send-media"; + +const markdown = ` +To Obtain a Phone Number ID and a Permanent System User Access Token, follow these steps: + +1. Go to https://developers.facebook.com/ +2. Make a new app, Select Other for usecase. +3. Choose Business as the type of app. +4. Add new Product -> WhatsApp. +5. Navigate to WhatsApp Settings > API Setup. +6. Select a phone number and copy its Phone number ID. +7. Login to your [Meta Business Manager](https://business.facebook.com/). +8. Click on Settings. +9. Create a new System User with access over the app and copy the access token. +`; + +export const whatsappAuth = PieceAuth.CustomAuth({ + required: true, + description: markdown, + props: { + access_token: PieceAuth.SecretText({ + displayName: 'System User Access Token', + description: 'The system user access token of your WhatsApp business account', + required: true, + }), + phoneNumberId: Property.ShortText({ + displayName: 'Phone Number ID', + description: 'The phone number ID of your WhatsApp business account', + required: true, + }), + }, +}); + + +export const whatsapp = createPiece({ + displayName: "WhatsApp Business", + description: 'Manage your WhatsApp business account', + auth: whatsappAuth, + minimumSupportedRelease: '0.20.0', + logoUrl: "https://cdn.activepieces.com/pieces/whatsapp.png", + authors: ["LevwTech"], + actions: [sendMessage, sendMedia], + triggers: [], +}); diff --git a/packages/pieces/community/whatsapp/src/lib/actions/send-media.ts b/packages/pieces/community/whatsapp/src/lib/actions/send-media.ts new file mode 100644 index 0000000000..a200ccfacf --- /dev/null +++ b/packages/pieces/community/whatsapp/src/lib/actions/send-media.ts @@ -0,0 +1,70 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { httpClient, HttpMethod } from '@activepieces/pieces-common'; +import { whatsappAuth } from '../..'; +import { supportedMediaTypes, capitalizeFirstLetter, mediaTypeSupportsCaption } from '../common/utils'; + +export const sendMedia = createAction({ + auth: whatsappAuth, + name: 'sendMedia', + displayName: 'Send Media', + description: 'Send a media message through WhatsApp', + props: { + to: Property.ShortText({ + displayName: 'To', + description: 'The recipient of the message', + required: true, + }), + type: Property.Dropdown({ + displayName: 'Type', + description: 'The type of media to send', + required: true, + options: async () => { + return { + options: supportedMediaTypes.map((type) => ({ + label: capitalizeFirstLetter(type), + value: type, + })), + }; + }, + refreshers: [] + }), + media: Property.ShortText({ + displayName: 'Media URL', + description: 'The URL of the media to send', + required: true, + }), + caption: Property.LongText({ + displayName: 'Caption', + description: 'A caption for the media', + required: false, + }), + filename: Property.LongText({ + displayName: 'Filename', + description: 'Filename of the document to send', + required: false, + }), + }, + async run(context) { + const { to, caption, media, type, filename } = context.propsValue; + const { access_token, phoneNumberId } = context.auth; + const body = { + messaging_product: "whatsapp", + recipient_type: "individual", + to, + type, + [type]: { + link: media, + } + } + if (caption && mediaTypeSupportsCaption(type)) (body[type] as any).caption = caption; + if (filename && type === 'document') (body[type] as any).filename = filename; + return await httpClient.sendRequest({ + method: HttpMethod.POST, + url: `https://graph.facebook.com/v17.0/${phoneNumberId}/messages`, + headers: { + Authorization: 'Bearer ' + access_token, + }, + body, + }); + }, +}); \ No newline at end of file diff --git a/packages/pieces/community/whatsapp/src/lib/actions/send-message.ts b/packages/pieces/community/whatsapp/src/lib/actions/send-message.ts new file mode 100644 index 0000000000..2aba8ac46a --- /dev/null +++ b/packages/pieces/community/whatsapp/src/lib/actions/send-message.ts @@ -0,0 +1,43 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { httpClient, HttpMethod } from '@activepieces/pieces-common'; +import { whatsappAuth } from '../..'; + + +export const sendMessage = createAction({ + auth: whatsappAuth, + name: 'sendMessage', + displayName: 'Send Message', + description: 'Send a text message through WhatsApp', + props: { + to: Property.ShortText({ + displayName: 'To', + description: 'The recipient of the message', + required: true, + }), + text: Property.LongText({ + displayName: 'Message', + description: 'The message to send', + required: true, + }), + }, + async run(context) { + const { to, text } = context.propsValue; + const { access_token, phoneNumberId } = context.auth; + return await httpClient.sendRequest({ + method: HttpMethod.POST, + url: `https://graph.facebook.com/v17.0/${phoneNumberId}/messages`, + headers: { + Authorization: 'Bearer ' + access_token, + }, + body: { + messaging_product: "whatsapp", + recipient_type: "individual", + to, + type: "text", + text: { + body: text + } + } + }); + }, +}); diff --git a/packages/pieces/community/whatsapp/src/lib/common/utils.ts b/packages/pieces/community/whatsapp/src/lib/common/utils.ts new file mode 100644 index 0000000000..1e327b8d8a --- /dev/null +++ b/packages/pieces/community/whatsapp/src/lib/common/utils.ts @@ -0,0 +1,3 @@ +export const supportedMediaTypes = ['image', 'audio', 'document', 'sticker', 'video']; +export const capitalizeFirstLetter = (word: string) => word.charAt(0).toUpperCase() + word.slice(1); +export const mediaTypeSupportsCaption = (type: string) => ['image', 'video', 'document'].includes(type); \ No newline at end of file diff --git a/packages/pieces/community/whatsapp/tsconfig.json b/packages/pieces/community/whatsapp/tsconfig.json new file mode 100644 index 0000000000..059cd81661 --- /dev/null +++ b/packages/pieces/community/whatsapp/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ] +} diff --git a/packages/pieces/community/whatsapp/tsconfig.lib.json b/packages/pieces/community/whatsapp/tsconfig.lib.json new file mode 100644 index 0000000000..0fe2205530 --- /dev/null +++ b/packages/pieces/community/whatsapp/tsconfig.lib.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "commonjs", + "outDir": "../../../../dist/out-tsc", + "declaration": true, + "types": ["node"] + }, + "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"], + "include": ["src/**/*.ts", "src/lib/actions/common/utils.ts"] +} diff --git a/packages/pieces/community/wordpress/package.json b/packages/pieces/community/wordpress/package.json index 340d897c2f..d219e9f0e3 100644 --- a/packages/pieces/community/wordpress/package.json +++ b/packages/pieces/community/wordpress/package.json @@ -1,4 +1,4 @@ { "name": "@activepieces/piece-wordpress", - "version": "0.3.13" + "version": "0.3.14" } \ No newline at end of file diff --git a/packages/pieces/community/wordpress/src/lib/actions/create-post.action.ts b/packages/pieces/community/wordpress/src/lib/actions/create-post.action.ts index 6fd5849c1f..d14ca4f8c9 100644 --- a/packages/pieces/community/wordpress/src/lib/actions/create-post.action.ts +++ b/packages/pieces/community/wordpress/src/lib/actions/create-post.action.ts @@ -39,6 +39,12 @@ export const createWordPressPost = createAction({ }), featured_media_file: wordpressCommon.featured_media_file, tags: wordpressCommon.tags, + acfFields: Property.Object({ + displayName: 'Custom ACF fields', + description: + 'Provide field name with value.You can find out field name from ACF plugin menu.', + required: false, + }), categories: wordpressCommon.categories, featured_media: wordpressCommon.featured_media, status: wordpressCommon.status, @@ -93,6 +99,13 @@ export const createWordPressPost = createAction({ requestBody['featured_media'] = context.propsValue.featured_media; } + if ( + context.propsValue.acfFields && + Object.keys(context.propsValue.acfFields).length > 0 + ) { + requestBody['acf'] = context.propsValue.acfFields; + } + if (context.propsValue.featured_media_file) { const formData = new FormData(); const { filename, base64 } = context.propsValue.featured_media_file; diff --git a/packages/pieces/community/wordpress/src/lib/actions/update-post.action.ts b/packages/pieces/community/wordpress/src/lib/actions/update-post.action.ts index 2cb42a7832..eeb89a55a0 100644 --- a/packages/pieces/community/wordpress/src/lib/actions/update-post.action.ts +++ b/packages/pieces/community/wordpress/src/lib/actions/update-post.action.ts @@ -36,6 +36,12 @@ export const updateWordPressPost = createAction({ }), featured_media_file: wordpressCommon.featured_media_file, tags: wordpressCommon.tags, + acfFields: Property.Object({ + displayName: 'Custom ACF fields', + description: + 'Provide field name with value.You can find out field name from ACF plugin menu.', + required: false, + }), categories: wordpressCommon.categories, featured_media: wordpressCommon.featured_media, status: wordpressCommon.status, @@ -99,6 +105,13 @@ export const updateWordPressPost = createAction({ requestBody['featured_media'] = context.propsValue.featured_media; } + if ( + context.propsValue.acfFields && + Object.keys(context.propsValue.acfFields).length > 0 + ) { + requestBody['acf'] = context.propsValue.acfFields; + } + if (context.propsValue.featured_media_file) { const formData = new FormData(); const { filename, base64 } = context.propsValue.featured_media_file; diff --git a/packages/server/api/src/app/app-event-routing/app-event-routing.module.ts b/packages/server/api/src/app/app-event-routing/app-event-routing.module.ts index fc968cd194..3b50004bac 100644 --- a/packages/server/api/src/app/app-event-routing/app-event-routing.module.ts +++ b/packages/server/api/src/app/app-event-routing/app-event-routing.module.ts @@ -9,7 +9,8 @@ import { slack } from '@activepieces/piece-slack' import { square } from '@activepieces/piece-square' import { Piece } from '@activepieces/pieces-framework' import { logger, rejectedPromiseHandler } from '@activepieces/server-shared' -import { ActivepiecesError, ALL_PRINCIPAL_TYPES, +import { + ActivepiecesError, ALL_PRINCIPAL_TYPES, ErrorCode, EventPayload, isNil, diff --git a/packages/server/api/src/app/core/security/authz/project-authz-handler.ts b/packages/server/api/src/app/core/security/authz/project-authz-handler.ts index d4cb817995..a351d13b0f 100644 --- a/packages/server/api/src/app/core/security/authz/project-authz-handler.ts +++ b/packages/server/api/src/app/core/security/authz/project-authz-handler.ts @@ -11,7 +11,7 @@ export class ProjectAuthzHandler extends BaseSecurityHandler { '/v1/users/projects/:projectId/token', '/v1/webhooks', '/v1/webhooks/:flowId', - '/v1/webhooks/:flowId/simulate', + '/v1/webhooks/:flowId/test', '/v1/webhooks/:flowId/sync', ] diff --git a/packages/server/api/src/app/flows/flow-run/flow-run-service.ts b/packages/server/api/src/app/flows/flow-run/flow-run-service.ts index 0282e52894..a60db820c0 100644 --- a/packages/server/api/src/app/flows/flow-run/flow-run-service.ts +++ b/packages/server/api/src/app/flows/flow-run/flow-run-service.ts @@ -78,6 +78,31 @@ async function updateFlowRunToLatestFlowVersionId( }) } +function returnHandlerId(pauseMetadata: PauseMetadata | undefined, requestId: string | undefined): string { + const handlerId = engineResponseWatcher.getHandlerId() + if (isNil(pauseMetadata)) { + return handlerId + } + + if (pauseMetadata.type === PauseType.WEBHOOK && requestId === pauseMetadata.requestId && pauseMetadata.handlerId) { + return pauseMetadata.handlerId + } + else { + return handlerId + } +} + +function modifyPauseMetadata(pauseMetadata: PauseMetadata): PauseMetadata { + if (pauseMetadata.type === PauseType.WEBHOOK) { + return { + ...pauseMetadata, + handlerId: engineResponseWatcher.getHandlerId(), + } + } + + return pauseMetadata +} + export const flowRunService = { async list({ projectId, @@ -173,6 +198,8 @@ export const flowRunService = { flowRunId: flowRunToResume.id, projectId: flowRunToResume.projectId, flowVersionId: flowRunToResume.flowVersionId, + synchronousHandlerId: returnHandlerId(pauseMetadata, requestId), + hookType: HookType.AFTER_LOG, executionType, environment: RunEnvironment.PRODUCTION, }) @@ -292,7 +319,7 @@ export const flowRunService = { status: FlowRunStatus.PAUSED, logsFileId: logFileId, // eslint-disable-next-line @typescript-eslint/no-explicit-any - pauseMetadata: pauseMetadata as any, + pauseMetadata: modifyPauseMetadata(pauseMetadata) as any, }) const flowRun = await flowRunRepo.findOneByOrFail({ id: flowRunId }) diff --git a/packages/server/api/src/app/flows/flow.module.ts b/packages/server/api/src/app/flows/flow.module.ts index d913879d9b..d7bda432ec 100644 --- a/packages/server/api/src/app/flows/flow.module.ts +++ b/packages/server/api/src/app/flows/flow.module.ts @@ -9,7 +9,7 @@ import { folderController } from './folder/folder.controller' import { stepRunService } from './step-run/step-run-service' import { testTriggerController } from './test-trigger/test-trigger-controller' import { logger } from '@activepieces/server-shared' -import { CreateStepRunRequestBody, StepRunResponse, TestFlowRunRequestBody, WebsocketClientEvent, WebsocketServerEvent } from '@activepieces/shared' +import { CreateStepRunRequestBody, isFlowStateTerminal, StepRunResponse, TestFlowRunRequestBody, WebsocketClientEvent, WebsocketServerEvent } from '@activepieces/shared' export const flowModule: FastifyPluginAsyncTypebox = async (app) => { await app.register(flowVersionController, { prefix: '/v1/flows' }) @@ -23,9 +23,21 @@ export const flowModule: FastifyPluginAsyncTypebox = async (app) => { projectId: principal.projectId, flowVersionId: data.flowVersionId, }) + socket.emit(WebsocketClientEvent.TEST_FLOW_RUN_STARTED, flowRun) - await engineResponseWatcher.listen(flowRun.id, false) - socket.emit(WebsocketClientEvent.TEST_FLOW_RUN_FINISHED, flowRun) + const eventEmitter = engineResponseWatcher.listen(flowRun.id) + eventEmitter.on(async (data) => { + const flowRun = await flowRunService.getOneOrThrow({ + id: data.requestId, + projectId: principal.projectId, + }) + + if (isFlowStateTerminal(flowRun.status)) { + engineResponseWatcher.removeListener(flowRun.id) + } + socket.emit(WebsocketClientEvent.TEST_FLOW_RUN_PROGRESS, flowRun) + }) + } }) websocketService.addListener(WebsocketServerEvent.TEST_STEP_RUN, (socket) => { diff --git a/packages/server/api/src/app/webhooks/webhook-controller.ts b/packages/server/api/src/app/webhooks/webhook-controller.ts index 228d377fa0..7abe7f1041 100644 --- a/packages/server/api/src/app/webhooks/webhook-controller.ts +++ b/packages/server/api/src/app/webhooks/webhook-controller.ts @@ -65,7 +65,7 @@ export const webhookController: FastifyPluginAsyncTypebox = async (app) => { .send(response.body) }) - app.all('/:flowId/simulate', WEBHOOK_PARAMS, async (request, reply) => { + app.all('/:flowId/test', WEBHOOK_PARAMS, async (request, reply) => { const response = await handleWebhook({ request, flowId: request.params.flowId, @@ -104,7 +104,7 @@ async function handleWebhook({ request, flowId, async, simulate }: { request: Fa headers: {}, } } - return engineResponseWatcher.listen(requestId, true) + return engineResponseWatcher.oneTimeListener(requestId, true) } async function convertRequest(request: FastifyRequest): Promise { diff --git a/packages/server/api/src/app/webhooks/webhook-service.ts b/packages/server/api/src/app/webhooks/webhook-service.ts index 8c28f79133..e9e52342c5 100644 --- a/packages/server/api/src/app/webhooks/webhook-service.ts +++ b/packages/server/api/src/app/webhooks/webhook-service.ts @@ -1,4 +1,3 @@ -import { flowService } from '../flows/flow/flow.service' import { flowRunService, HookType } from '../flows/flow-run/flow-run-service' import { flowVersionService } from '../flows/flow-version/flow-version.service' import { triggerHooks } from '../flows/trigger' @@ -66,32 +65,16 @@ export const webhookService = { const { projectId } = flow - if (isNil(flow.publishedVersionId)) { + if (flow.status !== FlowStatus.ENABLED || isNil(flow.publishedVersionId)) { logger.info( - `[WebhookService#callback] flowInstance not found, flowId=${flow.id}`, + `[WebhookService#callback] flowInstance not found or not enabled ignoring the webhook, flowId=${flow.id}`, ) - const flowVersion = ( - await flowService.getOnePopulatedOrThrow({ - projectId, + throw new ActivepiecesError({ + code: ErrorCode.FLOW_NOT_FOUND, + params: { id: flow.id, - }) - ).version - const payloads: unknown[] = await triggerHooks.executeTrigger({ - projectId, - flowVersion, - payload, - simulate: false, + }, }) - payloads.forEach((resultPayload) => { - saveSampleDataForWebhookTesting(flow, resultPayload) - }) - return [] - } - if (flow.status !== FlowStatus.ENABLED) { - logger.info( - `[WebhookService#callback] flowInstance not found or not enabled ignoring the webhook, flowId=${flow.id}`, - ) - return [] } const flowVersion = await flowVersionService.getOneOrThrow( @@ -104,20 +87,20 @@ export const webhookService = { simulate: false, }) - payloads.forEach((payload) => { - triggerEventService - .saveEvent({ - flowId: flow.id, - payload, - projectId, - }) - .catch((e) => - logger.error( - e, - '[WebhookService#callback] triggerEventService.saveEvent', - ), - ) - }) + const savePayloads = payloads.map((payload) => + triggerEventService.saveEvent({ + flowId: flow.id, + payload, + projectId, + }).catch((e) => + logger.error( + e, + '[WebhookService#callback] triggerEventService.saveEvent', + ), + ), + ) + + await Promise.all(savePayloads) const filterPayloads = await dedupeService.filterUniquePayloads( flowVersion.id, @@ -180,7 +163,7 @@ export const webhookService = { flowId, simulate, }: GetWebhookUrlParams): Promise { - const suffix: WebhookUrlSuffix = simulate ? '/simulate' : '' + const suffix: WebhookUrlSuffix = simulate ? '/test' : '' const webhookPrefix = await this.getWebhookPrefix() return `${webhookPrefix}/${flowId}${suffix}` }, @@ -211,22 +194,7 @@ const getLatestFlowVersionOrThrow = async ( return flowVersion } -function saveSampleDataForWebhookTesting(flow: Flow, payload: unknown): void { - triggerEventService - .saveEvent({ - flowId: flow.id, - payload, - projectId: flow.projectId, - }) - .catch((e) => - logger.error( - e, - '[WebhookService#saveSampleDataForWebhookTesting] triggerEventService.saveEvent', - ), - ) -} - -type WebhookUrlSuffix = '' | '/simulate' +type WebhookUrlSuffix = '' | '/test' type GetWebhookUrlParams = { flowId: FlowId diff --git a/packages/server/api/src/app/webhooks/webhook-simulation/webhook-simulation-service.ts b/packages/server/api/src/app/webhooks/webhook-simulation/webhook-simulation-service.ts index 4e4dc92e83..61dfd76872 100644 --- a/packages/server/api/src/app/webhooks/webhook-simulation/webhook-simulation-service.ts +++ b/packages/server/api/src/app/webhooks/webhook-simulation/webhook-simulation-service.ts @@ -49,7 +49,7 @@ export const webhookSimulationService = { }) try { - const webhookSimulationExists = await webhookSimulationRepo.exist({ + const webhookSimulationExists = await webhookSimulationRepo.exists({ where: { flowId }, }) diff --git a/packages/server/api/src/app/workers/flow-worker/consumer/webook-consumer.ts b/packages/server/api/src/app/workers/flow-worker/consumer/webook-consumer.ts index 96eddd9757..5852cb8c2b 100644 --- a/packages/server/api/src/app/workers/flow-worker/consumer/webook-consumer.ts +++ b/packages/server/api/src/app/workers/flow-worker/consumer/webook-consumer.ts @@ -3,7 +3,7 @@ import { flowService } from '../../../flows/flow/flow.service' import { webhookService } from '../../../webhooks/webhook-service' import { EngineHttpResponse, engineResponseWatcher } from '../engine-response-watcher' import { WebhookJobData } from '../job-data' -import { isNil } from '@activepieces/shared' +import { FlowStatus, isNil } from '@activepieces/shared' export const webhookConsumer = { async consumeWebhook(data: WebhookJobData): Promise { @@ -17,6 +17,15 @@ export const webhookConsumer = { }) return } + const isPublishedAndEnabled = (flow.status !== FlowStatus.ENABLED || isNil(flow.publishedVersionId)) && !simulate + if (isPublishedAndEnabled) { + await stopAndReply(data, { + status: StatusCodes.NOT_FOUND, + body: {}, + headers: {}, + }) + return + } const handshakeResponse = await webhookService.handshake({ flow, payload, @@ -51,7 +60,7 @@ export const webhookConsumer = { return } const firstRun = runs[0] - const response = await engineResponseWatcher.listen(firstRun.id, true) + const response = await engineResponseWatcher.oneTimeListener(firstRun.id, true) await stopAndReply(data, response) }, diff --git a/packages/server/api/src/app/workers/flow-worker/engine-response-watcher.ts b/packages/server/api/src/app/workers/flow-worker/engine-response-watcher.ts index aae19a1b6e..2975cc1afb 100644 --- a/packages/server/api/src/app/workers/flow-worker/engine-response-watcher.ts +++ b/packages/server/api/src/app/workers/flow-worker/engine-response-watcher.ts @@ -1,10 +1,10 @@ import { logger } from '@sentry/utils' import { StatusCodes } from 'http-status-codes' import { pubSub } from '../../helper/pubsub' -import { system, SystemProp } from '@activepieces/server-shared' +import { system, SystemProp, TypedEventEmitter } from '@activepieces/server-shared' import { apId } from '@activepieces/shared' -const listeners = new Map void>() +const listeners = new Map void>() export type EngineHttpResponse = { status: number @@ -12,7 +12,7 @@ export type EngineHttpResponse = { headers: Record } -type EngineResponseWithId = { +export type EngineResponseWithId = { requestId: string httpResponse: EngineHttpResponse } @@ -24,6 +24,9 @@ export const engineResponseWatcher = { getHandlerId(): string { return HANDLER_ID }, + removeListener(requestId: string): void { + listeners.delete(requestId) + }, async init(): Promise { logger.info('[engineWatcher#init] Initializing engine run watcher') @@ -33,8 +36,7 @@ export const engineResponseWatcher = { const parsedMessasge: EngineResponseWithId = JSON.parse(message) const listener = listeners.get(parsedMessasge.requestId) if (listener) { - listener(parsedMessasge.httpResponse) - listeners.delete(parsedMessasge.requestId) + listener(parsedMessasge) } logger.info( `[engineWatcher#init] message=${parsedMessasge.requestId}`, @@ -42,28 +44,37 @@ export const engineResponseWatcher = { }, ) }, - async listen(requestId: string, timeoutRequest: boolean): Promise { + listen(requestId: string): TypedEventEmitter { + const eventEmitter = new TypedEventEmitter() + listeners.set(requestId, (data) => { + eventEmitter.emit(data) + }) + return eventEmitter + }, + async oneTimeListener(requestId: string, timeoutRequest: boolean): Promise { logger.info(`[engineWatcher#listen] requestId=${requestId}`) return new Promise((resolve) => { - const defaultResponse: EngineHttpResponse = { - status: StatusCodes.NO_CONTENT, - body: {}, - headers: {}, - } - const responseHandler = (flowResponse: EngineHttpResponse) => { - clearTimeout(timeout) - resolve(flowResponse) - } let timeout: NodeJS.Timeout - if (!timeoutRequest) { - listeners.set(requestId, resolve) - } - else { + if (timeoutRequest) { + const defaultResponse: EngineHttpResponse = { + status: StatusCodes.NO_CONTENT, + body: {}, + headers: {}, + } timeout = setTimeout(() => { + this.removeListener(requestId) resolve(defaultResponse) }, WEBHOOK_TIMEOUT_MS) - listeners.set(requestId, responseHandler) + + } + const responseHandler = (flowResponse: EngineResponseWithId) => { + if (timeout) { + clearTimeout(timeout) + } + this.removeListener(requestId) + resolve(flowResponse.httpResponse) } + listeners.set(requestId, responseHandler) }) }, async publish( diff --git a/packages/server/shared/package.json b/packages/server/shared/package.json index ef59338eff..8dbc4602ec 100644 --- a/packages/server/shared/package.json +++ b/packages/server/shared/package.json @@ -6,7 +6,8 @@ "@sentry/node": "7.64.0", "pino": "8.18.0", "pino-loki": "2.1.3", - "@activepieces/shared": "*" + "@activepieces/shared": "*", + "events": "3.3.0" }, "type": "commonjs", "main": "./src/index.js", diff --git a/packages/server/shared/src/index.ts b/packages/server/shared/src/index.ts index 9b0e318118..2821662697 100644 --- a/packages/server/shared/src/index.ts +++ b/packages/server/shared/src/index.ts @@ -1,3 +1,5 @@ +export * from './lib/exception-handler' +export * from './lib/typed-event-emitter' export * from './lib/semaphore' export * from './lib/file-compressor' export * from './lib/file-system' @@ -5,5 +7,4 @@ export * from './lib/package-manager' export * from './lib/system/system' export * from './lib/system/system-prop' export * from './lib/promise-handler' -export * from './lib/logger' -export * from './lib/exception-handler' \ No newline at end of file +export * from './lib/logger' \ No newline at end of file diff --git a/packages/server/shared/src/lib/typed-event-emitter.ts b/packages/server/shared/src/lib/typed-event-emitter.ts new file mode 100644 index 0000000000..46b6c74a45 --- /dev/null +++ b/packages/server/shared/src/lib/typed-event-emitter.ts @@ -0,0 +1,17 @@ +import EventEmitter from 'events' + +export class TypedEventEmitter { + private emitter = new EventEmitter() + + emit(eventArg: TEventData) { + this.emitter.emit('event', eventArg) + } + + on(handler: (eventArg: TEventData) => void) { + this.emitter.on('event', handler) + } + + off(handler: (eventArg: TEventData) => void) { + this.emitter.off('event', handler) + } +} diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 93a9663be2..879918c2ba 100755 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -60,6 +60,7 @@ export { DelayPauseMetadata, PauseMetadata, WebhookPauseMetadata } from './lib/f export * from './lib/federated-authn' export { STORE_KEY_MAX_LENGTH } from './lib/store-entry/store-entry' export { RetryFlowRequestBody } from './lib/flow-run/test-flow-run-request' +export * from './lib/flow-run/flow-status' export * from './lib/flows/dto/flow-template-request' // Look at https://github.com/sinclairzx81/typebox/issues/350 TypeSystem.ExactOptionalPropertyTypes = false diff --git a/packages/shared/src/lib/flow-run/execution/flow-execution.ts b/packages/shared/src/lib/flow-run/execution/flow-execution.ts index a1adc6e3a5..cbca52894e 100644 --- a/packages/shared/src/lib/flow-run/execution/flow-execution.ts +++ b/packages/shared/src/lib/flow-run/execution/flow-execution.ts @@ -29,6 +29,7 @@ export const WebhookPauseMetadata = Type.Object({ type: Type.Literal(PauseType.WEBHOOK), requestId: Type.String(), response: Type.Unknown(), + handlerId: Type.Optional(Type.String({})), }) export type WebhookPauseMetadata = Static diff --git a/packages/shared/src/lib/flow-run/flow-status.ts b/packages/shared/src/lib/flow-run/flow-status.ts new file mode 100644 index 0000000000..24411d3909 --- /dev/null +++ b/packages/shared/src/lib/flow-run/flow-status.ts @@ -0,0 +1,5 @@ +import { FlowRunStatus } from './execution/flow-execution' + +export const isFlowStateTerminal = (status: FlowRunStatus): boolean => { + return status === FlowRunStatus.SUCCEEDED || status === FlowRunStatus.FAILED || status === FlowRunStatus.INTERNAL_ERROR || status === FlowRunStatus.QUOTA_EXCEEDED +} \ No newline at end of file diff --git a/packages/shared/src/lib/websocket/index.ts b/packages/shared/src/lib/websocket/index.ts index cc85d14a77..34156832c2 100644 --- a/packages/shared/src/lib/websocket/index.ts +++ b/packages/shared/src/lib/websocket/index.ts @@ -2,7 +2,7 @@ export enum WebsocketClientEvent { TEST_FLOW_RUN_STARTED = 'TEST_FLOW_RUN_STARTED', - TEST_FLOW_RUN_FINISHED = 'TEST_FLOW_RUN_FINISHED', + TEST_FLOW_RUN_PROGRESS = 'TEST_FLOW_RUN_PROGRESS', GENERATE_CODE_FINISHED = 'GENERATE_CODE_FINIISHED', TEST_STEP_FINISHED = 'TEST_STEP_FINISHED', } diff --git a/packages/ui/common/src/lib/components/upgrade-note/upgrade-note.component.ts b/packages/ui/common/src/lib/components/upgrade-note/upgrade-note.component.ts index 33e2e50193..9e1b7fa8a4 100644 --- a/packages/ui/common/src/lib/components/upgrade-note/upgrade-note.component.ts +++ b/packages/ui/common/src/lib/components/upgrade-note/upgrade-note.component.ts @@ -12,20 +12,15 @@ import { fadeIn400ms } from '../../animation/fade-in.animations'; animations: [fadeIn400ms], }) export class UpgradeNoteComponent { - @Input() docsLink = ''; @Input({ required: true }) featureNoteTitle = ''; @Input({ required: true }) featureNote = ''; @Input() videoUrl = ''; - @Input() featureKey: FeatureKey; + @Input({ required: true }) featureKey: FeatureKey; constructor(private contactSalesService: ContactSalesService) {} @Input() insideTab = false; - openDocs() { - window.open(this.docsLink, '_blank', 'noopener noreferrer'); - } - openContactSales(): void { this.contactSalesService.open([this.featureKey]); } diff --git a/packages/ui/feature-builder-canvas/src/lib/components/widgets/test-flow-widget/test-flow-widget.component.ts b/packages/ui/feature-builder-canvas/src/lib/components/widgets/test-flow-widget/test-flow-widget.component.ts index 8d161b2119..56d34955ba 100644 --- a/packages/ui/feature-builder-canvas/src/lib/components/widgets/test-flow-widget/test-flow-widget.component.ts +++ b/packages/ui/feature-builder-canvas/src/lib/components/widgets/test-flow-widget/test-flow-widget.component.ts @@ -112,7 +112,7 @@ export class TestFlowWidgetComponent implements OnInit { ); this.testResult$ = this.websockService.socket - .fromEvent(WebsocketClientEvent.TEST_FLOW_RUN_FINISHED) + .fromEvent(WebsocketClientEvent.TEST_FLOW_RUN_PROGRESS) .pipe( switchMap((flowRun) => { return this.instanceRunService.get(flowRun.id); diff --git a/packages/ui/feature-builder-form-controls/src/lib/pipes/replace-markdown-consts.pipe.ts b/packages/ui/feature-builder-form-controls/src/lib/pipes/replace-markdown-consts.pipe.ts index a6d8911d1c..020a8de557 100644 --- a/packages/ui/feature-builder-form-controls/src/lib/pipes/replace-markdown-consts.pipe.ts +++ b/packages/ui/feature-builder-form-controls/src/lib/pipes/replace-markdown-consts.pipe.ts @@ -13,7 +13,7 @@ export class ReplaceMarkdownConstsPipe implements PipeTransform { formPieceTriggerPrefix: string ): string { return value - .replace('{{webhookUrl}}', `${webhookPrefix}/${flowId}`) - .replace('{{formUrl}}', `${formPieceTriggerPrefix}/${flowId}`); + .replaceAll('{{webhookUrl}}', `${webhookPrefix}/${flowId}`) + .replaceAll('{{formUrl}}', `${formPieceTriggerPrefix}/${flowId}`); } } diff --git a/packages/ui/feature-builder-left-sidebar/src/lib/components/run-details/selected-step-result/selected-step-result.component.html b/packages/ui/feature-builder-left-sidebar/src/lib/components/run-details/selected-step-result/selected-step-result.component.html index 1b93646346..590b9f013a 100644 --- a/packages/ui/feature-builder-left-sidebar/src/lib/components/run-details/selected-step-result/selected-step-result.component.html +++ b/packages/ui/feature-builder-left-sidebar/src/lib/components/run-details/selected-step-result/selected-step-result.component.html @@ -20,7 +20,7 @@
Duration: - {{ (selectedStepResult.duration || 1) | number: '1.0-1' }} ms + {{ ((selectedStepResult.duration || 1) / 1000) | number: '1.1-3' }} s
+ featureKey="GIT_SYNC"> } @else {
[] = steps.map((step) => this.pieceService.getStepDetails(step)); this.urlsToLoad$ = forkJoin(stepMetadata$).pipe( map((items) => items.map((item) => item.logoUrl!).slice(0, this.maxNumberOfIconsToLoad))); @@ -46,11 +50,29 @@ export class PiecesIconsFromFlowComponent implements OnInit { const stepsAppsNames = items.map((item) => item.name); if (stepsAppsNames.length === 1) { return stepsAppsNames[0]!; - } else if (stepsAppsNames.length < 7) { + } else if (stepsAppsNames.length <= 7) { return stepsAppsNames.slice(0, stepsAppsNames.length - 1).join(', ') + ` and ${stepsAppsNames[stepsAppsNames.length - 1]}`; } else { return stepsAppsNames.join(', ') + ' and others'; } } + + private filterOutDuplicates(steps: (Action | Trigger)[]) + { + const seen = new Set(); + return steps.filter((step) => { + let isDuplicate = false; + if(step.type === ActionType.PIECE || step.type === TriggerType.PIECE) + { + isDuplicate = seen.has(step.settings.pieceName); + seen.add(step.settings.pieceName); + } + else{ + isDuplicate = seen.has(step.type); + seen.add(step.type); + } + return !isDuplicate; + }); + } } diff --git a/tsconfig.base.json b/tsconfig.base.json index acf980862d..6838572878 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -390,6 +390,9 @@ "@activepieces/piece-pipedrive": [ "packages/pieces/community/pipedrive/src/index.ts" ], + "@activepieces/piece-poper": [ + "packages/pieces/community/poper/src/index.ts" + ], "@activepieces/piece-postgres": [ "packages/pieces/community/postgres/src/index.ts" ], @@ -553,6 +556,9 @@ "@activepieces/piece-whatsable": [ "packages/pieces/community/whatsable/src/index.ts" ], + "@activepieces/piece-whatsapp": [ + "packages/pieces/community/whatsapp/src/index.ts" + ], "@activepieces/piece-woocommerce": [ "packages/pieces/community/woocommerce/src/index.ts" ],