diff --git a/packages/pieces/community/digital-ocean/.eslintrc.json b/packages/pieces/community/digital-ocean/.eslintrc.json new file mode 100644 index 00000000000..4a4e695c547 --- /dev/null +++ b/packages/pieces/community/digital-ocean/.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/digital-ocean/README.md b/packages/pieces/community/digital-ocean/README.md new file mode 100644 index 00000000000..04281ea6d3b --- /dev/null +++ b/packages/pieces/community/digital-ocean/README.md @@ -0,0 +1,7 @@ +# pieces-digital-ocean + +This library was generated with [Nx](https://nx.dev). + +## Building + +Run `nx build pieces-digital-ocean` to build the library. diff --git a/packages/pieces/community/digital-ocean/package.json b/packages/pieces/community/digital-ocean/package.json new file mode 100644 index 00000000000..61ed52d4343 --- /dev/null +++ b/packages/pieces/community/digital-ocean/package.json @@ -0,0 +1,10 @@ +{ + "name": "@activepieces/piece-digital-ocean", + "version": "0.0.1", + "type": "commonjs", + "main": "./src/index.js", + "types": "./src/index.d.ts", + "dependencies": { + "tslib": "^2.3.0" + } +} diff --git a/packages/pieces/community/digital-ocean/project.json b/packages/pieces/community/digital-ocean/project.json new file mode 100644 index 00000000000..9064f7cea0a --- /dev/null +++ b/packages/pieces/community/digital-ocean/project.json @@ -0,0 +1,66 @@ +{ + "name": "pieces-digital-ocean", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "packages/pieces/community/digital-ocean/src", + "projectType": "library", + "release": { + "version": { + "manifestRootsToUpdate": [ + "dist/{projectRoot}" + ], + "currentVersionResolver": "git-tag", + "fallbackCurrentVersionResolver": "disk" + } + }, + "tags": [], + "targets": { + "build": { + "executor": "@nx/js:tsc", + "outputs": [ + "{options.outputPath}" + ], + "options": { + "outputPath": "dist/packages/pieces/community/digital-ocean", + "tsConfig": "packages/pieces/community/digital-ocean/tsconfig.lib.json", + "packageJson": "packages/pieces/community/digital-ocean/package.json", + "main": "packages/pieces/community/digital-ocean/src/index.ts", + "assets": [ + "packages/pieces/community/digital-ocean/*.md", + { + "input": "packages/pieces/community/digital-ocean/src/i18n", + "output": "./src/i18n", + "glob": "**/!(i18n.json)" + } + ], + "buildableProjectDepsInPackageJsonType": "dependencies", + "updateBuildableProjectDepsInPackageJson": true, + "clean": false + }, + "dependsOn": [ + "prebuild", + "^build" + ] + }, + "nx-release-publish": { + "options": { + "packageRoot": "dist/{projectRoot}" + } + }, + "prebuild": { + "dependsOn": [ + "^build" + ], + "executor": "nx:run-commands", + "options": { + "cwd": "packages/pieces/community/digital-ocean", + "command": "bun install --no-save --silent" + } + }, + "lint": { + "executor": "@nx/eslint:lint", + "outputs": [ + "{options.outputFile}" + ] + } + } +} \ No newline at end of file diff --git a/packages/pieces/community/digital-ocean/src/index.ts b/packages/pieces/community/digital-ocean/src/index.ts new file mode 100644 index 00000000000..81192641607 --- /dev/null +++ b/packages/pieces/community/digital-ocean/src/index.ts @@ -0,0 +1,49 @@ +import { createCustomApiCallAction } from '@activepieces/pieces-common'; +import { createPiece } from '@activepieces/pieces-framework'; +import { PieceCategory } from '@activepieces/shared'; +import { digitalOceanAuth, DigitalOceanAuthValue } from './lib/common/auth'; +import { + listDomains, + createDomain, + getDomain, + deleteDomain, + listDroplets, + getDroplet, + createDroplet, + deleteDroplet, + listDatabaseClusters, + listDatabaseEvents, +} from './lib/actions'; + +export const digitalOcean = createPiece({ + displayName: 'DigitalOcean', + auth: digitalOceanAuth, + minimumSupportedRelease: '0.36.1', + logoUrl: 'https://cdn.activepieces.com/pieces/digital-ocean.png', + description: 'Cloud infrastructure provider for developers.', + categories: [PieceCategory.DEVELOPER_TOOLS], + authors: ['onyedikachi-david'], + actions: [ + listDomains, + createDomain, + getDomain, + deleteDomain, + listDroplets, + getDroplet, + createDroplet, + deleteDroplet, + listDatabaseClusters, + listDatabaseEvents, + createCustomApiCallAction({ + baseUrl: () => 'https://api.digitalocean.com/v2', + auth: digitalOceanAuth, + authMapping: async (auth) => { + const token = typeof auth === 'string' ? auth : (auth as { access_token: string }).access_token; + return { + Authorization: `Bearer ${token}`, + }; + }, + }), + ], + triggers: [], +}); diff --git a/packages/pieces/community/digital-ocean/src/lib/actions/create-domain.ts b/packages/pieces/community/digital-ocean/src/lib/actions/create-domain.ts new file mode 100644 index 00000000000..f484b4bd875 --- /dev/null +++ b/packages/pieces/community/digital-ocean/src/lib/actions/create-domain.ts @@ -0,0 +1,46 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { digitalOceanAuth, DigitalOceanAuthValue } from '../common/auth'; +import { digitalOceanApiCall, getAuthFromValue } from '../common/client'; +import { HttpMethod } from '@activepieces/pieces-common'; + +export const createDomain = createAction({ + auth: digitalOceanAuth, + name: 'create_domain', + displayName: 'Create Domain', + description: 'Add a new domain to your DigitalOcean account.', + props: { + name: Property.ShortText({ + displayName: 'Domain Name', + description: 'The domain name (e.g., example.com).', + required: true, + }), + ip_address: Property.ShortText({ + displayName: 'IP Address', + description: 'Optional IP address to create an A record pointing to the apex domain.', + required: false, + }), + }, + async run(context) { + const { name, ip_address } = context.propsValue; + + const body: { name: string; ip_address?: string } = { name }; + if (ip_address) { + body.ip_address = ip_address; + } + + const response = await digitalOceanApiCall<{ + domain: { + name: string; + ttl: number; + zone_file: string | null; + }; + }>({ + method: HttpMethod.POST, + path: '/domains', + auth: getAuthFromValue(context.auth as DigitalOceanAuthValue), + body, + }); + + return response; + }, +}); diff --git a/packages/pieces/community/digital-ocean/src/lib/actions/create-droplet.ts b/packages/pieces/community/digital-ocean/src/lib/actions/create-droplet.ts new file mode 100644 index 00000000000..d33223169ed --- /dev/null +++ b/packages/pieces/community/digital-ocean/src/lib/actions/create-droplet.ts @@ -0,0 +1,300 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { digitalOceanAuth, DigitalOceanAuthValue } from '../common/auth'; +import { digitalOceanApiCall, getAuthFromValue } from '../common/client'; +import { HttpMethod } from '@activepieces/pieces-common'; + +export const createDroplet = createAction({ + auth: digitalOceanAuth, + name: 'create_droplet', + displayName: 'Create Droplet', + description: 'Create a new Droplet or multiple Droplets.', + props: { + creation_mode: Property.StaticDropdown({ + displayName: 'Creation Mode', + description: 'Create a single Droplet or multiple Droplets at once.', + required: true, + defaultValue: 'single', + options: { + options: [ + { label: 'Single Droplet', value: 'single' }, + { label: 'Multiple Droplets (up to 10)', value: 'multiple' }, + ], + }, + }), + name: Property.ShortText({ + displayName: 'Droplet Name', + description: 'Name for the Droplet (e.g., example.com).', + required: true, + }), + names: Property.Array({ + displayName: 'Droplet Names', + description: 'Names for multiple Droplets (up to 10).', + required: true, + }), + region: Property.Dropdown({ + displayName: 'Region', + description: 'Region to deploy the Droplet.', + required: false, + refreshers: [], + auth: digitalOceanAuth, + options: async ({ auth }) => { + if (!auth) { + return { + disabled: true, + placeholder: 'Connect your account first', + options: [], + }; + } + + const response = await digitalOceanApiCall<{ + regions: Array<{ slug: string; name: string; available: boolean }>; + }>({ + method: HttpMethod.GET, + path: '/regions', + auth: getAuthFromValue(auth as DigitalOceanAuthValue), + }); + + return { + disabled: false, + options: response.regions + .filter((r) => r.available) + .map((region) => ({ + label: region.name, + value: region.slug, + })), + }; + }, + }), + size: Property.Dropdown({ + displayName: 'Size', + description: 'Droplet size (CPU, RAM, disk).', + required: true, + refreshers: [], + auth: digitalOceanAuth, + options: async ({ auth }) => { + if (!auth) { + return { + disabled: true, + placeholder: 'Connect your account first', + options: [], + }; + } + + const response = await digitalOceanApiCall<{ + sizes: Array<{ + slug: string; + description: string; + memory: number; + vcpus: number; + disk: number; + price_monthly: number; + available: boolean; + }>; + }>({ + method: HttpMethod.GET, + path: '/sizes', + auth: getAuthFromValue(auth as DigitalOceanAuthValue), + }); + + return { + disabled: false, + options: response.sizes + .filter((s) => s.available) + .map((size) => ({ + label: `${size.description} ($${size.price_monthly}/mo)`, + value: size.slug, + })), + }; + }, + }), + image: Property.Dropdown({ + displayName: 'Image', + description: 'Base image for the Droplet.', + required: true, + refreshers: [], + auth: digitalOceanAuth, + options: async ({ auth }) => { + if (!auth) { + return { + disabled: true, + placeholder: 'Connect your account first', + options: [], + }; + } + + const response = await digitalOceanApiCall<{ + images: Array<{ + id: number; + slug: string | null; + name: string; + distribution: string; + public: boolean; + }>; + }>({ + method: HttpMethod.GET, + path: '/images', + auth: getAuthFromValue(auth as DigitalOceanAuthValue), + query: { type: 'distribution', per_page: 200 }, + }); + + return { + disabled: false, + options: response.images.map((image) => ({ + label: `${image.distribution} - ${image.name}`, + value: image.slug ?? image.id, + })), + }; + }, + }), + ssh_keys: Property.MultiSelectDropdown({ + displayName: 'SSH Keys', + description: 'SSH keys to embed in the Droplet.', + required: false, + refreshers: [], + auth: digitalOceanAuth, + options: async ({ auth }) => { + if (!auth) { + return { + disabled: true, + placeholder: 'Connect your account first', + options: [], + }; + } + + const response = await digitalOceanApiCall<{ + ssh_keys: Array<{ id: number; name: string; fingerprint: string }>; + }>({ + method: HttpMethod.GET, + path: '/account/keys', + auth: getAuthFromValue(auth as DigitalOceanAuthValue), + }); + + return { + disabled: false, + options: response.ssh_keys.map((key) => ({ + label: key.name, + value: key.id, + })), + }; + }, + }), + vpc_uuid: Property.Dropdown({ + displayName: 'VPC', + description: 'VPC network for the Droplet.', + required: false, + refreshers: [], + auth: digitalOceanAuth, + options: async ({ auth }) => { + if (!auth) { + return { + disabled: true, + placeholder: 'Connect your account first', + options: [], + }; + } + + const response = await digitalOceanApiCall<{ + vpcs: Array<{ id: string; name: string; region: string }>; + }>({ + method: HttpMethod.GET, + path: '/vpcs', + auth: getAuthFromValue(auth as DigitalOceanAuthValue), + }); + + return { + disabled: false, + options: response.vpcs.map((vpc) => ({ + label: `${vpc.name} (${vpc.region})`, + value: vpc.id, + })), + }; + }, + }), + backups: Property.Checkbox({ + displayName: 'Enable Backups', + description: 'Enable automated backups.', + required: false, + defaultValue: false, + }), + ipv6: Property.Checkbox({ + displayName: 'Enable IPv6', + description: 'Enable IPv6 networking.', + required: false, + defaultValue: false, + }), + monitoring: Property.Checkbox({ + displayName: 'Enable Monitoring', + description: 'Install the DigitalOcean monitoring agent.', + required: false, + defaultValue: false, + }), + tags: Property.Array({ + displayName: 'Tags', + description: 'Tags to apply to the Droplet.', + required: false, + }), + user_data: Property.LongText({ + displayName: 'User Data', + description: 'Cloud-init script or user data (max 64 KiB).', + required: false, + }), + with_droplet_agent: Property.Checkbox({ + displayName: 'Install Droplet Agent', + description: 'Install agent for web console access.', + required: false, + defaultValue: true, + }), + }, + async run(context) { + const { + creation_mode, + name, + names, + region, + size, + image, + ssh_keys, + vpc_uuid, + backups, + ipv6, + monitoring, + tags, + user_data, + with_droplet_agent, + } = context.propsValue; + + const body: Record = { + size, + image, + }; + + if (creation_mode === 'single') { + body['name'] = name; + } else { + body['names'] = names; + } + + if (region) body['region'] = region; + if (ssh_keys && ssh_keys.length > 0) body['ssh_keys'] = ssh_keys; + if (vpc_uuid) body['vpc_uuid'] = vpc_uuid; + if (backups !== undefined) body['backups'] = backups; + if (ipv6 !== undefined) body['ipv6'] = ipv6; + if (monitoring !== undefined) body['monitoring'] = monitoring; + if (tags && tags.length > 0) body['tags'] = tags; + if (user_data) body['user_data'] = user_data; + if (with_droplet_agent !== undefined) body['with_droplet_agent'] = with_droplet_agent; + + const response = await digitalOceanApiCall<{ + droplet?: object; + droplets?: object[]; + links: object; + }>({ + method: HttpMethod.POST, + path: '/droplets', + auth: getAuthFromValue(context.auth as DigitalOceanAuthValue), + body, + }); + + return response; + }, +}); diff --git a/packages/pieces/community/digital-ocean/src/lib/actions/delete-domain.ts b/packages/pieces/community/digital-ocean/src/lib/actions/delete-domain.ts new file mode 100644 index 00000000000..ba86c167537 --- /dev/null +++ b/packages/pieces/community/digital-ocean/src/lib/actions/delete-domain.ts @@ -0,0 +1,32 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { digitalOceanAuth, DigitalOceanAuthValue } from '../common/auth'; +import { digitalOceanApiCall, getAuthFromValue } from '../common/client'; +import { HttpMethod } from '@activepieces/pieces-common'; + +export const deleteDomain = createAction({ + auth: digitalOceanAuth, + name: 'delete_domain', + displayName: 'Delete Domain', + description: 'Remove a domain from your DigitalOcean account.', + props: { + domain_name: Property.ShortText({ + displayName: 'Domain Name', + description: 'The domain name to delete (e.g., example.com).', + required: true, + }), + }, + async run(context) { + const { domain_name } = context.propsValue; + + await digitalOceanApiCall({ + method: HttpMethod.DELETE, + path: `/domains/${encodeURIComponent(domain_name)}`, + auth: getAuthFromValue(context.auth as DigitalOceanAuthValue), + }); + + return { + success: true, + message: `Domain '${domain_name}' has been deleted.`, + }; + }, +}); diff --git a/packages/pieces/community/digital-ocean/src/lib/actions/delete-droplet.ts b/packages/pieces/community/digital-ocean/src/lib/actions/delete-droplet.ts new file mode 100644 index 00000000000..c2fefd6a805 --- /dev/null +++ b/packages/pieces/community/digital-ocean/src/lib/actions/delete-droplet.ts @@ -0,0 +1,60 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { digitalOceanAuth, DigitalOceanAuthValue } from '../common/auth'; +import { digitalOceanApiCall, getAuthFromValue } from '../common/client'; +import { HttpMethod } from '@activepieces/pieces-common'; + +export const deleteDroplet = createAction({ + auth: digitalOceanAuth, + name: 'delete_droplet', + displayName: 'Delete Droplet', + description: 'Delete an existing Droplet.', + props: { + droplet_id: Property.Dropdown({ + displayName: 'Droplet', + description: 'Select the Droplet to delete.', + required: true, + refreshers: [], + auth: digitalOceanAuth, + options: async ({ auth }) => { + if (!auth) { + return { + disabled: true, + placeholder: 'Connect your account first', + options: [], + }; + } + + const response = await digitalOceanApiCall<{ + droplets: Array<{ id: number; name: string }>; + }>({ + method: HttpMethod.GET, + path: '/droplets', + auth: getAuthFromValue(auth as DigitalOceanAuthValue), + query: { per_page: 200 }, + }); + + return { + disabled: false, + options: response.droplets.map((droplet) => ({ + label: droplet.name, + value: droplet.id, + })), + }; + }, + }), + }, + async run(context) { + const { droplet_id } = context.propsValue; + + await digitalOceanApiCall({ + method: HttpMethod.DELETE, + path: `/droplets/${droplet_id}`, + auth: getAuthFromValue(context.auth as DigitalOceanAuthValue), + }); + + return { + success: true, + message: `Droplet with ID '${droplet_id}' has been deleted.`, + }; + }, +}); diff --git a/packages/pieces/community/digital-ocean/src/lib/actions/get-domain.ts b/packages/pieces/community/digital-ocean/src/lib/actions/get-domain.ts new file mode 100644 index 00000000000..79421dcfe1f --- /dev/null +++ b/packages/pieces/community/digital-ocean/src/lib/actions/get-domain.ts @@ -0,0 +1,35 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { digitalOceanAuth, DigitalOceanAuthValue } from '../common/auth'; +import { digitalOceanApiCall, getAuthFromValue } from '../common/client'; +import { HttpMethod } from '@activepieces/pieces-common'; + +export const getDomain = createAction({ + auth: digitalOceanAuth, + name: 'get_domain', + displayName: 'Get Domain', + description: 'Retrieve details about a specific domain.', + props: { + domain_name: Property.ShortText({ + displayName: 'Domain Name', + description: 'The domain name to retrieve (e.g., example.com).', + required: true, + }), + }, + async run(context) { + const { domain_name } = context.propsValue; + + const response = await digitalOceanApiCall<{ + domain: { + name: string; + ttl: number; + zone_file: string; + }; + }>({ + method: HttpMethod.GET, + path: `/domains/${encodeURIComponent(domain_name)}`, + auth: getAuthFromValue(context.auth as DigitalOceanAuthValue), + }); + + return response; + }, +}); diff --git a/packages/pieces/community/digital-ocean/src/lib/actions/get-droplet.ts b/packages/pieces/community/digital-ocean/src/lib/actions/get-droplet.ts new file mode 100644 index 00000000000..e8903f43c7f --- /dev/null +++ b/packages/pieces/community/digital-ocean/src/lib/actions/get-droplet.ts @@ -0,0 +1,79 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { digitalOceanAuth, DigitalOceanAuthValue } from '../common/auth'; +import { digitalOceanApiCall, getAuthFromValue } from '../common/client'; +import { HttpMethod } from '@activepieces/pieces-common'; + +export const getDroplet = createAction({ + auth: digitalOceanAuth, + name: 'get_droplet', + displayName: 'Get Droplet', + description: 'Retrieve details about a specific Droplet.', + props: { + droplet_id: Property.Dropdown({ + displayName: 'Droplet', + description: 'Select the Droplet to retrieve.', + required: true, + refreshers: [], + auth: digitalOceanAuth, + options: async ({ auth }) => { + if (!auth) { + return { + disabled: true, + placeholder: 'Connect your account first', + options: [], + }; + } + + const response = await digitalOceanApiCall<{ + droplets: Array<{ id: number; name: string }>; + }>({ + method: HttpMethod.GET, + path: '/droplets', + auth: getAuthFromValue(auth as DigitalOceanAuthValue), + query: { per_page: 200 }, + }); + + return { + disabled: false, + options: response.droplets.map((droplet) => ({ + label: droplet.name, + value: droplet.id, + })), + }; + }, + }), + }, + async run(context) { + const { droplet_id } = context.propsValue; + + const response = await digitalOceanApiCall<{ + droplet: { + id: number; + name: string; + memory: number; + vcpus: number; + disk: number; + locked: boolean; + status: string; + created_at: string; + features: string[]; + backup_ids: number[]; + snapshot_ids: number[]; + image: object; + volume_ids: string[]; + size: object; + size_slug: string; + networks: object; + region: object; + tags: string[]; + vpc_uuid: string; + }; + }>({ + method: HttpMethod.GET, + path: `/droplets/${droplet_id}`, + auth: getAuthFromValue(context.auth as DigitalOceanAuthValue), + }); + + return response; + }, +}); diff --git a/packages/pieces/community/digital-ocean/src/lib/actions/index.ts b/packages/pieces/community/digital-ocean/src/lib/actions/index.ts new file mode 100644 index 00000000000..69bc2f2c49b --- /dev/null +++ b/packages/pieces/community/digital-ocean/src/lib/actions/index.ts @@ -0,0 +1,10 @@ +export { listDomains } from './list-domains'; +export { createDomain } from './create-domain'; +export { getDomain } from './get-domain'; +export { deleteDomain } from './delete-domain'; +export { listDroplets } from './list-droplets'; +export { getDroplet } from './get-droplet'; +export { createDroplet } from './create-droplet'; +export { deleteDroplet } from './delete-droplet'; +export { listDatabaseClusters } from './list-database-clusters'; +export { listDatabaseEvents } from './list-database-events'; diff --git a/packages/pieces/community/digital-ocean/src/lib/actions/list-database-clusters.ts b/packages/pieces/community/digital-ocean/src/lib/actions/list-database-clusters.ts new file mode 100644 index 00000000000..0d6b5e396ff --- /dev/null +++ b/packages/pieces/community/digital-ocean/src/lib/actions/list-database-clusters.ts @@ -0,0 +1,55 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { digitalOceanAuth, DigitalOceanAuthValue } from '../common/auth'; +import { digitalOceanApiCall, getAuthFromValue } from '../common/client'; +import { HttpMethod } from '@activepieces/pieces-common'; + +export const listDatabaseClusters = createAction({ + auth: digitalOceanAuth, + name: 'list_database_clusters', + displayName: 'List Database Clusters', + description: 'Retrieve a list of all database clusters in your account.', + props: { + tag_name: Property.ShortText({ + displayName: 'Tag Name', + description: 'Filter database clusters by a specific tag.', + required: false, + }), + }, + async run(context) { + const { tag_name } = context.propsValue; + + const query: Record = {}; + if (tag_name) { + query['tag_name'] = tag_name; + } + + const response = await digitalOceanApiCall<{ + databases: Array<{ + id: string; + name: string; + engine: string; + version: string; + num_nodes: number; + size: string; + region: string; + status: string; + created_at: string; + private_network_uuid: string; + tags: string[] | null; + db_names: string[] | null; + connection: object; + private_connection: object; + users: object[] | null; + maintenance_window: object | null; + storage_size_mib: number; + }>; + }>({ + method: HttpMethod.GET, + path: '/databases', + auth: getAuthFromValue(context.auth as DigitalOceanAuthValue), + query: Object.keys(query).length > 0 ? query : undefined, + }); + + return response; + }, +}); diff --git a/packages/pieces/community/digital-ocean/src/lib/actions/list-database-events.ts b/packages/pieces/community/digital-ocean/src/lib/actions/list-database-events.ts new file mode 100644 index 00000000000..d0fd7b7c0c1 --- /dev/null +++ b/packages/pieces/community/digital-ocean/src/lib/actions/list-database-events.ts @@ -0,0 +1,63 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { digitalOceanAuth, DigitalOceanAuthValue } from '../common/auth'; +import { digitalOceanApiCall, getAuthFromValue } from '../common/client'; +import { HttpMethod } from '@activepieces/pieces-common'; + +export const listDatabaseEvents = createAction({ + auth: digitalOceanAuth, + name: 'list_database_events', + displayName: 'List Database Events', + description: 'Retrieve all event logs for a database cluster.', + props: { + database_cluster_uuid: Property.Dropdown({ + displayName: 'Database Cluster', + description: 'Select the database cluster.', + required: true, + refreshers: [], + auth: digitalOceanAuth, + options: async ({ auth }) => { + if (!auth) { + return { + disabled: true, + placeholder: 'Connect your account first', + options: [], + }; + } + + const response = await digitalOceanApiCall<{ + databases: Array<{ id: string; name: string; engine: string }>; + }>({ + method: HttpMethod.GET, + path: '/databases', + auth: getAuthFromValue(auth as DigitalOceanAuthValue), + }); + + return { + disabled: false, + options: response.databases.map((db) => ({ + label: `${db.name} (${db.engine})`, + value: db.id, + })), + }; + }, + }), + }, + async run(context) { + const { database_cluster_uuid } = context.propsValue; + + const response = await digitalOceanApiCall<{ + events: Array<{ + id: string; + cluster_name: string; + event_type: string; + create_time: string; + }>; + }>({ + method: HttpMethod.GET, + path: `/databases/${database_cluster_uuid}/events`, + auth: getAuthFromValue(context.auth as DigitalOceanAuthValue), + }); + + return response; + }, +}); diff --git a/packages/pieces/community/digital-ocean/src/lib/actions/list-domains.ts b/packages/pieces/community/digital-ocean/src/lib/actions/list-domains.ts new file mode 100644 index 00000000000..dbab0c836f9 --- /dev/null +++ b/packages/pieces/community/digital-ocean/src/lib/actions/list-domains.ts @@ -0,0 +1,48 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { digitalOceanAuth, DigitalOceanAuthValue } from '../common/auth'; +import { digitalOceanApiCall, getAuthFromValue } from '../common/client'; +import { HttpMethod } from '@activepieces/pieces-common'; + +export const listDomains = createAction({ + auth: digitalOceanAuth, + name: 'list_domains', + displayName: 'List All Domains', + description: 'Retrieve a list of all domains in your account.', + props: { + per_page: Property.Number({ + displayName: 'Results Per Page', + description: 'Number of domains to return per page (1-200).', + required: false, + defaultValue: 20, + }), + page: Property.Number({ + displayName: 'Page', + description: 'Which page of results to return.', + required: false, + defaultValue: 1, + }), + }, + async run(context) { + const { per_page, page } = context.propsValue; + + const response = await digitalOceanApiCall<{ + domains: Array<{ + name: string; + ttl: number; + zone_file: string | null; + }>; + links: object; + meta: { total: number }; + }>({ + method: HttpMethod.GET, + path: '/domains', + auth: getAuthFromValue(context.auth as DigitalOceanAuthValue), + query: { + per_page: per_page ?? 20, + page: page ?? 1, + }, + }); + + return response; + }, +}); diff --git a/packages/pieces/community/digital-ocean/src/lib/actions/list-droplets.ts b/packages/pieces/community/digital-ocean/src/lib/actions/list-droplets.ts new file mode 100644 index 00000000000..da7f53036c3 --- /dev/null +++ b/packages/pieces/community/digital-ocean/src/lib/actions/list-droplets.ts @@ -0,0 +1,97 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { digitalOceanAuth, DigitalOceanAuthValue } from '../common/auth'; +import { digitalOceanApiCall, getAuthFromValue } from '../common/client'; +import { HttpMethod } from '@activepieces/pieces-common'; + +export const listDroplets = createAction({ + auth: digitalOceanAuth, + name: 'list_droplets', + displayName: 'List All Droplets', + description: 'Retrieve a list of all Droplets in your account.', + props: { + per_page: Property.Number({ + displayName: 'Results Per Page', + description: 'Number of Droplets to return per page (1-200).', + required: false, + defaultValue: 20, + }), + page: Property.Number({ + displayName: 'Page', + description: 'Which page of results to return.', + required: false, + defaultValue: 1, + }), + tag_name: Property.ShortText({ + displayName: 'Tag Name', + description: 'Filter Droplets by a specific tag. Cannot be combined with Name or Type.', + required: false, + }), + name: Property.ShortText({ + displayName: 'Name', + description: 'Filter by exact Droplet name (case-insensitive). Cannot be combined with Tag Name.', + required: false, + }), + type: Property.StaticDropdown({ + displayName: 'Type', + description: 'Filter by Droplet type. Cannot be combined with Tag Name.', + required: false, + options: { + options: [ + { label: 'Standard Droplets', value: 'droplets' }, + { label: 'GPU Droplets', value: 'gpus' }, + ], + }, + }), + }, + async run(context) { + const { per_page, page, tag_name, name, type } = context.propsValue; + + const query: Record = { + per_page: per_page ?? 20, + page: page ?? 1, + }; + + if (tag_name) { + query['tag_name'] = tag_name; + } + if (name) { + query['name'] = name; + } + if (type) { + query['type'] = type; + } + + const response = await digitalOceanApiCall<{ + droplets: Array<{ + id: number; + name: string; + memory: number; + vcpus: number; + disk: number; + locked: boolean; + status: string; + created_at: string; + features: string[]; + backup_ids: number[]; + snapshot_ids: number[]; + image: object; + volume_ids: string[]; + size: object; + size_slug: string; + networks: object; + region: object; + tags: string[]; + vpc_uuid: string; + }>; + links: object; + meta: { total: number }; + }>({ + method: HttpMethod.GET, + path: '/droplets', + auth: getAuthFromValue(context.auth as DigitalOceanAuthValue), + query, + }); + + return response; + }, +}); diff --git a/packages/pieces/community/digital-ocean/src/lib/common/auth.ts b/packages/pieces/community/digital-ocean/src/lib/common/auth.ts new file mode 100644 index 00000000000..1ea30ecc79d --- /dev/null +++ b/packages/pieces/community/digital-ocean/src/lib/common/auth.ts @@ -0,0 +1,63 @@ +import { + AppConnectionValueForAuthProperty, + PieceAuth, +} from '@activepieces/pieces-framework'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { digitalOceanApiCall } from './client'; +import { AppConnectionType } from '@activepieces/shared'; + +export const digitalOceanAuth = [ + PieceAuth.OAuth2({ + description: 'Connect your DigitalOcean account using OAuth2.', + authUrl: 'https://cloud.digitalocean.com/v1/oauth/authorize', + tokenUrl: 'https://cloud.digitalocean.com/v1/oauth/token', + required: true, + scope: [], + validate: async ({ auth }) => { + try { + await digitalOceanApiCall({ + method: HttpMethod.GET, + path: '/account', + auth: { + type: AppConnectionType.OAUTH2, + access_token: auth.access_token, + }, + }); + return { valid: true }; + } catch (e) { + return { + valid: false, + error: (e as Error).message, + }; + } + }, + }), + PieceAuth.SecretText({ + displayName: 'Personal Access Token', + required: true, + description: + 'Generate a Personal Access Token from DigitalOcean Control Panel under API > Personal access tokens.', + validate: async ({ auth }) => { + try { + await digitalOceanApiCall({ + method: HttpMethod.GET, + path: '/account', + auth: { + type: AppConnectionType.SECRET_TEXT, + secret_text: auth, + }, + }); + return { valid: true }; + } catch (e) { + return { + valid: false, + error: (e as Error).message, + }; + } + }, + }), +]; + +export type DigitalOceanAuthValue = AppConnectionValueForAuthProperty< + typeof digitalOceanAuth +>; diff --git a/packages/pieces/community/digital-ocean/src/lib/common/client.ts b/packages/pieces/community/digital-ocean/src/lib/common/client.ts new file mode 100644 index 00000000000..752f69da81b --- /dev/null +++ b/packages/pieces/community/digital-ocean/src/lib/common/client.ts @@ -0,0 +1,94 @@ +import { + AuthenticationType, + httpClient, + HttpMethod, + HttpRequest, + QueryParams, +} from '@activepieces/pieces-common'; +import { AppConnectionType } from '@activepieces/shared'; + +const BASE_URL = 'https://api.digitalocean.com/v2'; + +export type DigitalOceanAuth = + | { + type: + | AppConnectionType.OAUTH2 + | AppConnectionType.CLOUD_OAUTH2 + | AppConnectionType.PLATFORM_OAUTH2; + access_token: string; + } + | { + type: AppConnectionType.SECRET_TEXT; + secret_text: string; + }; + +export type DigitalOceanApiCallParams = { + method: HttpMethod; + path: `/${string}`; + auth: DigitalOceanAuth; + query?: Record; + body?: unknown; +}; + +function getAccessToken(auth: DigitalOceanAuth): string { + switch (auth.type) { + case AppConnectionType.OAUTH2: + case AppConnectionType.CLOUD_OAUTH2: + case AppConnectionType.PLATFORM_OAUTH2: + return auth.access_token; + case AppConnectionType.SECRET_TEXT: + return auth.secret_text; + } +} + +export function getAuthFromValue(auth: unknown): DigitalOceanAuth { + if (typeof auth === 'string') { + return { + type: AppConnectionType.SECRET_TEXT, + secret_text: auth, + }; + } + const authObj = auth as { access_token?: string; type?: string }; + if (authObj.access_token) { + return { + type: AppConnectionType.OAUTH2, + access_token: authObj.access_token, + }; + } + throw new Error('Invalid authentication value'); +} + +export async function digitalOceanApiCall({ + method, + path, + auth, + query, + body, +}: DigitalOceanApiCallParams): Promise { + const queryParams: QueryParams = {}; + + if (query) { + for (const [key, value] of Object.entries(query)) { + if (value !== undefined) { + queryParams[key] = String(value); + } + } + } + + const request: HttpRequest = { + method, + url: `${BASE_URL}${path}`, + authentication: { + type: AuthenticationType.BEARER_TOKEN, + token: getAccessToken(auth), + }, + headers: { + 'Content-Type': 'application/json', + }, + queryParams: Object.keys(queryParams).length > 0 ? queryParams : undefined, + body, + }; + + const response = await httpClient.sendRequest(request); + return response.body; +} diff --git a/packages/pieces/community/digital-ocean/src/lib/common/index.ts b/packages/pieces/community/digital-ocean/src/lib/common/index.ts new file mode 100644 index 00000000000..e8b49f4c64d --- /dev/null +++ b/packages/pieces/community/digital-ocean/src/lib/common/index.ts @@ -0,0 +1,2 @@ +export * from './auth'; +export * from './client'; diff --git a/packages/pieces/community/digital-ocean/tsconfig.json b/packages/pieces/community/digital-ocean/tsconfig.json new file mode 100644 index 00000000000..eff240ac143 --- /dev/null +++ b/packages/pieces/community/digital-ocean/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "forceConsistentCasingInFileNames": true, + "strict": true, + "importHelpers": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noPropertyAccessFromIndexSignature": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ] +} diff --git a/packages/pieces/community/digital-ocean/tsconfig.lib.json b/packages/pieces/community/digital-ocean/tsconfig.lib.json new file mode 100644 index 00000000000..5995d959bf0 --- /dev/null +++ b/packages/pieces/community/digital-ocean/tsconfig.lib.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../dist/out-tsc", + "declaration": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"] +} diff --git a/packages/pieces/community/gender-api/.eslintrc.json b/packages/pieces/community/gender-api/.eslintrc.json new file mode 100644 index 00000000000..4a4e695c547 --- /dev/null +++ b/packages/pieces/community/gender-api/.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/gender-api/README.md b/packages/pieces/community/gender-api/README.md new file mode 100644 index 00000000000..6b268045032 --- /dev/null +++ b/packages/pieces/community/gender-api/README.md @@ -0,0 +1,7 @@ +# pieces-gender-api + +This library was generated with [Nx](https://nx.dev). + +## Building + +Run `nx build pieces-gender-api` to build the library. diff --git a/packages/pieces/community/gender-api/package.json b/packages/pieces/community/gender-api/package.json new file mode 100644 index 00000000000..9c0f6a23fb7 --- /dev/null +++ b/packages/pieces/community/gender-api/package.json @@ -0,0 +1,10 @@ +{ + "name": "@activepieces/piece-gender-api", + "version": "0.0.1", + "type": "commonjs", + "main": "./src/index.js", + "types": "./src/index.d.ts", + "dependencies": { + "tslib": "^2.3.0" + } +} diff --git a/packages/pieces/community/gender-api/project.json b/packages/pieces/community/gender-api/project.json new file mode 100644 index 00000000000..44352036c9b --- /dev/null +++ b/packages/pieces/community/gender-api/project.json @@ -0,0 +1,66 @@ +{ + "name": "pieces-gender-api", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "packages/pieces/community/gender-api/src", + "projectType": "library", + "release": { + "version": { + "manifestRootsToUpdate": [ + "dist/{projectRoot}" + ], + "currentVersionResolver": "git-tag", + "fallbackCurrentVersionResolver": "disk" + } + }, + "tags": [], + "targets": { + "build": { + "executor": "@nx/js:tsc", + "outputs": [ + "{options.outputPath}" + ], + "options": { + "outputPath": "dist/packages/pieces/community/gender-api", + "tsConfig": "packages/pieces/community/gender-api/tsconfig.lib.json", + "packageJson": "packages/pieces/community/gender-api/package.json", + "main": "packages/pieces/community/gender-api/src/index.ts", + "assets": [ + "packages/pieces/community/gender-api/*.md", + { + "input": "packages/pieces/community/gender-api/src/i18n", + "output": "./src/i18n", + "glob": "**/!(i18n.json)" + } + ], + "buildableProjectDepsInPackageJsonType": "dependencies", + "updateBuildableProjectDepsInPackageJson": true, + "clean": false + }, + "dependsOn": [ + "prebuild", + "^build" + ] + }, + "nx-release-publish": { + "options": { + "packageRoot": "dist/{projectRoot}" + } + }, + "prebuild": { + "dependsOn": [ + "^build" + ], + "executor": "nx:run-commands", + "options": { + "cwd": "packages/pieces/community/gender-api", + "command": "bun install --no-save --silent" + } + }, + "lint": { + "executor": "@nx/eslint:lint", + "outputs": [ + "{options.outputFile}" + ] + } + } +} \ No newline at end of file diff --git a/packages/pieces/community/gender-api/src/index.ts b/packages/pieces/community/gender-api/src/index.ts new file mode 100644 index 00000000000..2bb1ce6a66e --- /dev/null +++ b/packages/pieces/community/gender-api/src/index.ts @@ -0,0 +1,30 @@ +import { createPiece, PieceAuth } from '@activepieces/pieces-framework'; +import { genderApiAuth } from './lib/common/auth'; +import { getStatistics } from './lib/actions/get-statistics'; +import { getGenderByFullName } from './lib/actions/get-gender-by-full-name'; +import { getGenderByFirstName } from './lib/actions/get-gender-by-first-name'; +import { createCustomApiCallAction } from '@activepieces/pieces-common'; + +export const genderApi = createPiece({ + displayName: 'Gender API', + auth: genderApiAuth, + minimumSupportedRelease: '0.36.1', + logoUrl: 'https://cdn.activepieces.com/pieces/gender-api.png', + authors: ['sanket-a11y'], + description: 'Predict the gender of a person based on their name using Gender-api service.', + actions: [ + getGenderByFirstName, + getGenderByFullName, + getStatistics, + createCustomApiCallAction({ + auth: genderApiAuth, + baseUrl: () => 'https://gender-api.com/v2', + authMapping: async (auth) => { + return { + Authorization: `Bearer ${auth.secret_text}`, + }; + }, + }), + ], + triggers: [], +}); diff --git a/packages/pieces/community/gender-api/src/lib/actions/get-gender-by-first-name.ts b/packages/pieces/community/gender-api/src/lib/actions/get-gender-by-first-name.ts new file mode 100644 index 00000000000..770c2435793 --- /dev/null +++ b/packages/pieces/community/gender-api/src/lib/actions/get-gender-by-first-name.ts @@ -0,0 +1,52 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { genderApiAuth } from '../common/auth'; +import { httpClient, HttpMethod } from '@activepieces/pieces-common'; + +export const getGenderByFirstName = createAction({ + auth: genderApiAuth, + name: 'getGenderByFirstName', + displayName: 'Get Gender by First Name', + description: 'Predict the gender of a person based on their first name', + props: { + first_name: Property.ShortText({ + displayName: 'First Name', + description: 'The first name to query', + required: true, + }), + country_code: Property.ShortText({ + displayName: 'Country Code', + description: 'ISO 3166-1 alpha-2 country code to improve accuracy (e.g., "US", "DE")', + required: false, + }), + locale: Property.ShortText({ + displayName: 'Locale', + description: 'Browser locale for localization (e.g., "en-US", "de-DE")', + required: false, + }), + }, + async run(context) { + const payload: any = { + first_name: context.propsValue.first_name, + }; + + if (context.propsValue.country_code) { + payload.country_code = context.propsValue.country_code; + } + + if (context.propsValue.locale) { + payload.locale = context.propsValue.locale; + } + + const response = await httpClient.sendRequest({ + method: HttpMethod.POST, + url: 'https://gender-api.com/v2/gender', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${context.auth.secret_text}`, + }, + body: payload, + }); + + return response.body; + }, +}); diff --git a/packages/pieces/community/gender-api/src/lib/actions/get-gender-by-full-name.ts b/packages/pieces/community/gender-api/src/lib/actions/get-gender-by-full-name.ts new file mode 100644 index 00000000000..e1b16efce47 --- /dev/null +++ b/packages/pieces/community/gender-api/src/lib/actions/get-gender-by-full-name.ts @@ -0,0 +1,52 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { genderApiAuth } from '../common/auth'; +import { httpClient, HttpMethod } from '@activepieces/pieces-common'; + +export const getGenderByFullName = createAction({ + auth: genderApiAuth, + name: 'getGenderByFullName', + displayName: 'Get Gender by Full Name', + description: 'Predict the gender of a person based on their full name', + props: { + full_name: Property.ShortText({ + displayName: 'Full Name', + description: 'The full name (first and last name) to query', + required: true, + }), + country_code: Property.ShortText({ + displayName: 'Country Code', + description: 'ISO 3166-1 alpha-2 country code to improve accuracy (e.g., "US", "DE")', + required: false, + }), + locale: Property.ShortText({ + displayName: 'Locale', + description: 'Browser locale for localization (e.g., "en-US", "de-DE")', + required: false, + }), + }, + async run(context) { + const payload: any = { + full_name: context.propsValue.full_name, + }; + + if (context.propsValue.country_code) { + payload.country_code = context.propsValue.country_code; + } + + if (context.propsValue.locale) { + payload.locale = context.propsValue.locale; + } + + const response = await httpClient.sendRequest({ + method: HttpMethod.POST, + url: 'https://gender-api.com/v2/gender', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${context.auth.secret_text}`, + }, + body: payload, + }); + + return response.body; + }, +}); diff --git a/packages/pieces/community/gender-api/src/lib/actions/get-statistics.ts b/packages/pieces/community/gender-api/src/lib/actions/get-statistics.ts new file mode 100644 index 00000000000..b3e8209d9cf --- /dev/null +++ b/packages/pieces/community/gender-api/src/lib/actions/get-statistics.ts @@ -0,0 +1,24 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { genderApiAuth } from '../common/auth'; +import { httpClient, HttpMethod } from '@activepieces/pieces-common'; + +export const getStatistics = createAction({ + auth: genderApiAuth, + name: 'getStatistics', + displayName: 'Get Statistics', + description: + 'Get account statistics including remaining credits and usage information', + props: {}, + async run(context) { + const response = await httpClient.sendRequest({ + method: HttpMethod.GET, + url: 'https://gender-api.com/v2/statistic', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${context.auth.secret_text}`, + }, + }); + + return response.body; + }, +}); diff --git a/packages/pieces/community/gender-api/src/lib/common/auth.ts b/packages/pieces/community/gender-api/src/lib/common/auth.ts new file mode 100644 index 00000000000..9a2b0f4aae1 --- /dev/null +++ b/packages/pieces/community/gender-api/src/lib/common/auth.ts @@ -0,0 +1,7 @@ +import { PieceAuth } from '@activepieces/pieces-framework'; + +export const genderApiAuth = PieceAuth.SecretText({ + displayName: 'API Key', + description: 'The API key for accessing the Gender-api service', + required: true, +}); diff --git a/packages/pieces/community/gender-api/tsconfig.json b/packages/pieces/community/gender-api/tsconfig.json new file mode 100644 index 00000000000..eff240ac143 --- /dev/null +++ b/packages/pieces/community/gender-api/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "forceConsistentCasingInFileNames": true, + "strict": true, + "importHelpers": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noPropertyAccessFromIndexSignature": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ] +} diff --git a/packages/pieces/community/gender-api/tsconfig.lib.json b/packages/pieces/community/gender-api/tsconfig.lib.json new file mode 100644 index 00000000000..5995d959bf0 --- /dev/null +++ b/packages/pieces/community/gender-api/tsconfig.lib.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../dist/out-tsc", + "declaration": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"] +} diff --git a/packages/pieces/community/leap-ai/.eslintrc.json b/packages/pieces/community/leap-ai/.eslintrc.json new file mode 100644 index 00000000000..4a4e695c547 --- /dev/null +++ b/packages/pieces/community/leap-ai/.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/leap-ai/README.md b/packages/pieces/community/leap-ai/README.md new file mode 100644 index 00000000000..806005549ec --- /dev/null +++ b/packages/pieces/community/leap-ai/README.md @@ -0,0 +1,7 @@ +# pieces-leap-ai + +This library was generated with [Nx](https://nx.dev). + +## Building + +Run `nx build pieces-leap-ai` to build the library. diff --git a/packages/pieces/community/leap-ai/package.json b/packages/pieces/community/leap-ai/package.json new file mode 100644 index 00000000000..ae25e11b628 --- /dev/null +++ b/packages/pieces/community/leap-ai/package.json @@ -0,0 +1,10 @@ +{ + "name": "@activepieces/piece-leap-ai", + "version": "0.0.1", + "type": "commonjs", + "main": "./src/index.js", + "types": "./src/index.d.ts", + "dependencies": { + "tslib": "^2.3.0" + } +} diff --git a/packages/pieces/community/leap-ai/project.json b/packages/pieces/community/leap-ai/project.json new file mode 100644 index 00000000000..66b21982ace --- /dev/null +++ b/packages/pieces/community/leap-ai/project.json @@ -0,0 +1,66 @@ +{ + "name": "pieces-leap-ai", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "packages/pieces/community/leap-ai/src", + "projectType": "library", + "release": { + "version": { + "manifestRootsToUpdate": [ + "dist/{projectRoot}" + ], + "currentVersionResolver": "git-tag", + "fallbackCurrentVersionResolver": "disk" + } + }, + "tags": [], + "targets": { + "build": { + "executor": "@nx/js:tsc", + "outputs": [ + "{options.outputPath}" + ], + "options": { + "outputPath": "dist/packages/pieces/community/leap-ai", + "tsConfig": "packages/pieces/community/leap-ai/tsconfig.lib.json", + "packageJson": "packages/pieces/community/leap-ai/package.json", + "main": "packages/pieces/community/leap-ai/src/index.ts", + "assets": [ + "packages/pieces/community/leap-ai/*.md", + { + "input": "packages/pieces/community/leap-ai/src/i18n", + "output": "./src/i18n", + "glob": "**/!(i18n.json)" + } + ], + "buildableProjectDepsInPackageJsonType": "dependencies", + "updateBuildableProjectDepsInPackageJson": true, + "clean": false + }, + "dependsOn": [ + "prebuild", + "^build" + ] + }, + "nx-release-publish": { + "options": { + "packageRoot": "dist/{projectRoot}" + } + }, + "prebuild": { + "dependsOn": [ + "^build" + ], + "executor": "nx:run-commands", + "options": { + "cwd": "packages/pieces/community/leap-ai", + "command": "bun install --no-save --silent" + } + }, + "lint": { + "executor": "@nx/eslint:lint", + "outputs": [ + "{options.outputFile}" + ] + } + } +} \ No newline at end of file diff --git a/packages/pieces/community/leap-ai/src/index.ts b/packages/pieces/community/leap-ai/src/index.ts new file mode 100644 index 00000000000..73b1b2dc5ea --- /dev/null +++ b/packages/pieces/community/leap-ai/src/index.ts @@ -0,0 +1,18 @@ +import { createPiece, PieceAuth } from '@activepieces/pieces-framework'; +import { leapAiAuth } from './lib/common/auth'; +import { getAWorkflowRun } from './lib/actions/get-a-workflow-run'; +import { runAWorkflow } from './lib/actions/run-a-workflow'; +import { PieceCategory } from '@activepieces/shared'; + +export const leapAi = createPiece({ + displayName: 'Leap AI', + auth: leapAiAuth, + minimumSupportedRelease: '0.36.1', + logoUrl: 'https://cdn.activepieces.com/pieces/leap-ai.png', + categories: [PieceCategory.ARTIFICIAL_INTELLIGENCE], + description: + 'Automate any workflow with AI. Build custom AI automations to scale your marketing, sales, and operations.', + authors: ['sanket-a11y'], + actions: [getAWorkflowRun, runAWorkflow], + triggers: [], +}); diff --git a/packages/pieces/community/leap-ai/src/lib/actions/get-a-workflow-run.ts b/packages/pieces/community/leap-ai/src/lib/actions/get-a-workflow-run.ts new file mode 100644 index 00000000000..e8951eaba08 --- /dev/null +++ b/packages/pieces/community/leap-ai/src/lib/actions/get-a-workflow-run.ts @@ -0,0 +1,29 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { leapAiAuth } from '../common/auth'; +import { makeRequest } from '../common/client'; +import { HttpMethod } from '@activepieces/pieces-common'; + +export const getAWorkflowRun = createAction({ + auth: leapAiAuth, + name: 'getAWorkflowRun', + displayName: 'Get a Workflow Run', + description: 'Retrieve the status and results of a workflow run', + props: { + workflow_run_id: Property.ShortText({ + displayName: 'Workflow Run ID', + description: 'The ID of the workflow run to retrieve', + required: true, + }), + }, + async run(context) { + const { workflow_run_id } = context.propsValue; + + const response = await makeRequest( + context.auth.secret_text, + HttpMethod.GET, + `/runs/${workflow_run_id}` + ); + + return response; + }, +}); diff --git a/packages/pieces/community/leap-ai/src/lib/actions/run-a-workflow.ts b/packages/pieces/community/leap-ai/src/lib/actions/run-a-workflow.ts new file mode 100644 index 00000000000..c6c215785df --- /dev/null +++ b/packages/pieces/community/leap-ai/src/lib/actions/run-a-workflow.ts @@ -0,0 +1,54 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { leapAiAuth } from '../common/auth'; +import { makeRequest } from '../common/client'; +import { HttpMethod } from '@activepieces/pieces-common'; + +export const runAWorkflow = createAction({ + auth: leapAiAuth, + name: 'runAWorkflow', + displayName: 'Run a Workflow', + description: 'Execute a Leap AI workflow and return the run ID', + props: { + workflow_id: Property.ShortText({ + displayName: 'Workflow ID', + description: 'The ID of the workflow to run', + required: true, + }), + webhook_url: Property.ShortText({ + displayName: 'Webhook URL', + description: + 'The URL to which workflow results should be sent on completion (optional)', + required: false, + }), + input: Property.Object({ + displayName: 'Input Variables', + description: + 'Variables that the workflow can use globally and their values (optional)', + required: false, + }), + }, + async run(context) { + const { workflow_id, webhook_url, input } = context.propsValue; + + const body: any = { + workflow_id, + }; + + if (webhook_url) { + body.webhook_url = webhook_url; + } + + if (input) { + body.input = input; + } + + const response = await makeRequest( + context.auth.secret_text, + HttpMethod.POST, + '/runs', + body + ); + + return response; + }, +}); diff --git a/packages/pieces/community/leap-ai/src/lib/common/auth.ts b/packages/pieces/community/leap-ai/src/lib/common/auth.ts new file mode 100644 index 00000000000..1034f01048d --- /dev/null +++ b/packages/pieces/community/leap-ai/src/lib/common/auth.ts @@ -0,0 +1,14 @@ +import { PieceAuth } from "@activepieces/pieces-framework"; + +export const leapAiAuth = PieceAuth.SecretText({ + displayName: "Leap AI API Key", + description: `Provide your Leap AI API key. + +**How to get your API key:** +1. Go to [app.tryleap.ai](https://app.tryleap.ai) +2. Click on **Settings** in the left sidebar (bottom) +3. Navigate to the **API** section +4. Click **Create API Key** to generate a new key +5. Copy and paste the key here`, + required: true, +}); \ No newline at end of file diff --git a/packages/pieces/community/leap-ai/src/lib/common/client.ts b/packages/pieces/community/leap-ai/src/lib/common/client.ts new file mode 100644 index 00000000000..ee628392408 --- /dev/null +++ b/packages/pieces/community/leap-ai/src/lib/common/client.ts @@ -0,0 +1,24 @@ +import { HttpMethod, httpClient } from '@activepieces/pieces-common'; + +export const BASE_URL = 'https://api.workflows.tryleap.ai/v2'; + +export async function makeRequest( + auth: string, + method: HttpMethod, + path: string, + body?: unknown +) { + const url = `${BASE_URL}${path}`; + + const response = await httpClient.sendRequest({ + method, + url, + headers: { + 'X-Api-Key': auth, + 'Content-Type': 'application/json', + }, + body, + }); + + return response.body; +} diff --git a/packages/pieces/community/leap-ai/tsconfig.json b/packages/pieces/community/leap-ai/tsconfig.json new file mode 100644 index 00000000000..eff240ac143 --- /dev/null +++ b/packages/pieces/community/leap-ai/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "forceConsistentCasingInFileNames": true, + "strict": true, + "importHelpers": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noPropertyAccessFromIndexSignature": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ] +} diff --git a/packages/pieces/community/leap-ai/tsconfig.lib.json b/packages/pieces/community/leap-ai/tsconfig.lib.json new file mode 100644 index 00000000000..5995d959bf0 --- /dev/null +++ b/packages/pieces/community/leap-ai/tsconfig.lib.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../dist/out-tsc", + "declaration": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"] +} diff --git a/packages/pieces/community/lokalise/.eslintrc.json b/packages/pieces/community/lokalise/.eslintrc.json new file mode 100644 index 00000000000..4a4e695c547 --- /dev/null +++ b/packages/pieces/community/lokalise/.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/lokalise/README.md b/packages/pieces/community/lokalise/README.md new file mode 100644 index 00000000000..5aff13eb2ab --- /dev/null +++ b/packages/pieces/community/lokalise/README.md @@ -0,0 +1,7 @@ +# pieces-lokalise + +This library was generated with [Nx](https://nx.dev). + +## Building + +Run `nx build pieces-lokalise` to build the library. diff --git a/packages/pieces/community/lokalise/package.json b/packages/pieces/community/lokalise/package.json new file mode 100644 index 00000000000..bc049028360 --- /dev/null +++ b/packages/pieces/community/lokalise/package.json @@ -0,0 +1,10 @@ +{ + "name": "@activepieces/piece-lokalise", + "version": "0.0.1", + "type": "commonjs", + "main": "./src/index.js", + "types": "./src/index.d.ts", + "dependencies": { + "tslib": "^2.3.0" + } +} diff --git a/packages/pieces/community/lokalise/project.json b/packages/pieces/community/lokalise/project.json new file mode 100644 index 00000000000..a5946dabc3f --- /dev/null +++ b/packages/pieces/community/lokalise/project.json @@ -0,0 +1,66 @@ +{ + "name": "pieces-lokalise", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "packages/pieces/community/lokalise/src", + "projectType": "library", + "release": { + "version": { + "manifestRootsToUpdate": [ + "dist/{projectRoot}" + ], + "currentVersionResolver": "git-tag", + "fallbackCurrentVersionResolver": "disk" + } + }, + "tags": [], + "targets": { + "build": { + "executor": "@nx/js:tsc", + "outputs": [ + "{options.outputPath}" + ], + "options": { + "outputPath": "dist/packages/pieces/community/lokalise", + "tsConfig": "packages/pieces/community/lokalise/tsconfig.lib.json", + "packageJson": "packages/pieces/community/lokalise/package.json", + "main": "packages/pieces/community/lokalise/src/index.ts", + "assets": [ + "packages/pieces/community/lokalise/*.md", + { + "input": "packages/pieces/community/lokalise/src/i18n", + "output": "./src/i18n", + "glob": "**/!(i18n.json)" + } + ], + "buildableProjectDepsInPackageJsonType": "dependencies", + "updateBuildableProjectDepsInPackageJson": true, + "clean": false + }, + "dependsOn": [ + "prebuild", + "^build" + ] + }, + "nx-release-publish": { + "options": { + "packageRoot": "dist/{projectRoot}" + } + }, + "prebuild": { + "dependsOn": [ + "^build" + ], + "executor": "nx:run-commands", + "options": { + "cwd": "packages/pieces/community/lokalise", + "command": "bun install --no-save --silent" + } + }, + "lint": { + "executor": "@nx/eslint:lint", + "outputs": [ + "{options.outputFile}" + ] + } + } +} \ No newline at end of file diff --git a/packages/pieces/community/lokalise/src/index.ts b/packages/pieces/community/lokalise/src/index.ts new file mode 100644 index 00000000000..59f67273b6d --- /dev/null +++ b/packages/pieces/community/lokalise/src/index.ts @@ -0,0 +1,52 @@ +import { createPiece, PieceAuth } from '@activepieces/pieces-framework'; +import { lokaliseAuth } from './lib/common/auth'; +import { PieceCategory } from '@activepieces/shared'; +import { createComment } from './lib/actions/create-comment'; +import { createProject } from './lib/actions/create-project'; +import { createKey } from './lib/actions/create-key'; +import { createTask } from './lib/actions/create-task'; +import { retrieveTranslation } from './lib/actions/retrieve-translation'; +import { updateKey } from './lib/actions/update-key'; +import { updateTranslation } from './lib/actions/update-translation'; +import { deleteKey } from './lib/actions/delete-key'; +import { retrieveAComment } from './lib/actions/retrieve-a-comment'; +import { retrieveAProject } from './lib/actions/retrieve-a-project'; +import { retrieveAKey } from './lib/actions/retrieve-a-key'; + +import { keyAdded } from './lib/triggers/key-added'; +import { keyUpdated } from './lib/triggers/key-updated'; +import { translationUpdated } from './lib/triggers/translation-updated'; +import { createCustomApiCallAction } from '@activepieces/pieces-common'; + +export const lokalise = createPiece({ + displayName: 'Lokalise', + auth: lokaliseAuth, + minimumSupportedRelease: '0.36.1', + logoUrl: 'https://cdn.activepieces.com/pieces/lokalise.png', + authors: ['sanket-a11y'], + categories: [PieceCategory.DEVELOPER_TOOLS], + description: 'Lokalise is a collaborative translation platform.', + actions: [ + createComment, + createKey, + createProject, + createTask, + deleteKey, + retrieveAComment, + retrieveAKey, + retrieveAProject, + retrieveTranslation, + updateKey, + updateTranslation, + createCustomApiCallAction({ + auth: lokaliseAuth, + baseUrl: () => 'https://api.lokalise.com/api2', + authMapping: async (auth) => { + return { + 'X-Api-Token': auth.secret_text, + }; + }, + }), + ], + triggers: [keyAdded, keyUpdated, translationUpdated], +}); diff --git a/packages/pieces/community/lokalise/src/lib/actions/create-comment.ts b/packages/pieces/community/lokalise/src/lib/actions/create-comment.ts new file mode 100644 index 00000000000..d0bcc2e3819 --- /dev/null +++ b/packages/pieces/community/lokalise/src/lib/actions/create-comment.ts @@ -0,0 +1,42 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { makeRequest } from '../common/client'; +import { lokaliseAuth } from '../common/auth'; +import { keyIdProp, projectDropdown } from '../common/props'; + +export const createComment = createAction({ + auth: lokaliseAuth, + name: 'createComment', + displayName: 'Create Comment', + description: 'Add comments to a key in your Lokalise project', + props: { + projectId: projectDropdown, + keyId: keyIdProp, + comment: Property.LongText({ + displayName: 'Comment', + description: 'The comment text to add to the key', + required: true, + }), + }, + async run(context) { + const { projectId, keyId, comment } = context.propsValue; + + const body = { + comments: [ + { + comment, + }, + ], + }; + + const path = `/projects/${projectId}/keys/${keyId}/comments`; + const response = await makeRequest( + context.auth.secret_text, + HttpMethod.POST, + path, + body + ); + + return response; + }, +}); diff --git a/packages/pieces/community/lokalise/src/lib/actions/create-key.ts b/packages/pieces/community/lokalise/src/lib/actions/create-key.ts new file mode 100644 index 00000000000..d6e22c092c0 --- /dev/null +++ b/packages/pieces/community/lokalise/src/lib/actions/create-key.ts @@ -0,0 +1,77 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { makeRequest } from '../common/client'; +import { lokaliseAuth } from '../common/auth'; +import { projectDropdown } from '../common/props'; + +export const createKey = createAction({ + auth: lokaliseAuth, + name: 'createKey', + displayName: 'Create Key', + description: 'Create one or more keys in your Lokalise project', + props: { + projectId: projectDropdown, + keyName: Property.ShortText({ + displayName: 'Key Name', + description: 'The name/identifier for the key (e.g., "index.welcome")', + required: true, + }), + description: Property.LongText({ + displayName: 'Description', + description: 'Optional description for the key', + required: false, + }), + platforms: Property.StaticMultiSelectDropdown({ + displayName: 'Platforms', + description: 'Select the platforms this key applies to', + required: true, + options: { + options: [ + { label: 'iOS', value: 'ios' }, + { label: 'Android', value: 'android' }, + { label: 'Web', value: 'web' }, + { label: 'Flutter', value: 'flutter' }, + { label: 'React Native', value: 'react_native' }, + { label: 'Other', value: 'other' }, + ], + }, + }), + tags: Property.Array({ + displayName: 'Tags', + description: 'Comma-separated tags for the key (e.g., "urgent,ui")', + required: false, + }), + useAutomations: Property.Checkbox({ + displayName: 'Use Automations', + description: 'Whether to run automations on the new key translations', + required: false, + defaultValue: false, + }), + }, + async run(context) { + const { projectId, keyName, description, platforms, tags, useAutomations } = + context.propsValue; + + const body = { + keys: [ + { + key_name: keyName, + ...(description && { description }), + ...(platforms && platforms.length > 0 && { platforms }), + ...(tags && tags.length > 0 && { tags }), + }, + ], + use_automations: useAutomations, + }; + + const path = `/projects/${projectId}/keys`; + const response = await makeRequest( + context.auth.secret_text, + HttpMethod.POST, + path, + body + ); + + return response; + }, +}); diff --git a/packages/pieces/community/lokalise/src/lib/actions/create-project.ts b/packages/pieces/community/lokalise/src/lib/actions/create-project.ts new file mode 100644 index 00000000000..7a7156ecbe9 --- /dev/null +++ b/packages/pieces/community/lokalise/src/lib/actions/create-project.ts @@ -0,0 +1,96 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { makeRequest } from '../common/client'; +import { lokaliseAuth } from '../common/auth'; + +export const createProject = createAction({ + auth: lokaliseAuth, + name: 'createProject', + displayName: 'Create Project', + description: 'Create a new project in your Lokalise team', + props: { + projectName: Property.ShortText({ + displayName: 'Project Name', + description: 'Name of the project', + required: true, + }), + description: Property.LongText({ + displayName: 'Description', + description: 'Description of the project', + required: false, + }), + projectType: Property.StaticDropdown({ + displayName: 'Project Type', + description: 'Type of the project', + required: false, + options: { + options: [ + { + label: 'Web and Mobile (Software Projects)', + value: 'localization_files', + }, + { label: 'Documents (Ad hoc documents)', value: 'paged_documents' }, + { + label: 'Marketing Projects (with integrations)', + value: 'content_integration', + }, + { + label: 'Marketing Projects (Automatically translated)', + value: 'marketing', + }, + { + label: + 'Marketing Projects (Automatically translated with integrations)', + value: 'marketing_integrations', + }, + ], + }, + }), + baseLangIso: Property.ShortText({ + displayName: 'Base Language ISO', + description: + 'Language/locale code of the project base language (e.g., "en", "en-us")', + required: false, + }), + teamId: Property.ShortText({ + displayName: 'Team ID', + description: + 'ID of the team to create a project in (numerical ID or UUID). If omitted, the project will be created in your current team', + required: false, + }), + isSegmentationEnabled: Property.Checkbox({ + displayName: 'Enable Segmentation', + description: 'Enable Segmentation feature for project', + required: false, + defaultValue: false, + }), + }, + async run(context) { + const { + projectName, + description, + projectType, + baseLangIso, + teamId, + isSegmentationEnabled, + } = context.propsValue; + + const body: any = { + name: projectName, + ...(description && { description }), + ...(projectType && { project_type: projectType }), + ...(baseLangIso && { base_lang_iso: baseLangIso }), + ...(teamId && { team_id: teamId }), + ...(isSegmentationEnabled && { is_segmentation_enabled: true }), + }; + + const response = await makeRequest( + context.auth.secret_text, + HttpMethod.POST, + '/projects', + body + ); + + return response; + }, +}); diff --git a/packages/pieces/community/lokalise/src/lib/actions/create-task.ts b/packages/pieces/community/lokalise/src/lib/actions/create-task.ts new file mode 100644 index 00000000000..ff4472adaab --- /dev/null +++ b/packages/pieces/community/lokalise/src/lib/actions/create-task.ts @@ -0,0 +1,171 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { makeRequest } from '../common/client'; +import { lokaliseAuth } from '../common/auth'; +import { projectDropdown } from '../common/props'; + +export const createTask = createAction({ + auth: lokaliseAuth, + name: 'createTask', + displayName: 'Create Task', + description: 'Create a new task in your Lokalise project', + props: { + projectId: projectDropdown, + taskTitle: Property.ShortText({ + displayName: 'Task Title', + description: 'Title of the task', + required: true, + }), + description: Property.LongText({ + displayName: 'Description', + description: + 'Brief task description. Used as instructions for AI in automatic_translation and lqa_by_ai task types', + required: false, + }), + taskType: Property.StaticDropdown({ + displayName: 'Task Type', + description: 'Type of the task', + required: false, + options: { + options: [ + { label: 'Translation', value: 'translation' }, + { label: 'Automatic Translation', value: 'automatic_translation' }, + { label: 'LQA by AI', value: 'lqa_by_ai' }, + { label: 'Review', value: 'review' }, + ], + }, + }), + keys: Property.Array({ + displayName: 'Key', + description: + 'Comma-separated list of key IDs to include in the task (required unless parent_task_id is specified)', + required: false, + }), + languages: Property.Array({ + displayName: 'Languages', + description: + 'Comma-separated language ISO codes for the task (e.g., "fr,de,es")', + required: false, + properties: { + languageIso: Property.ShortText({ + displayName: 'Language ISO', + description: 'Language ISO code (e.g., "fr", "de")', + required: true, + }), + }, + }), + sourceLanguageIso: Property.ShortText({ + displayName: 'Source Language ISO', + description: 'Source language code for the task', + required: false, + }), + dueDate: Property.ShortText({ + displayName: 'Due Date', + description: + 'Due date in format: Y-m-d H:i:s (e.g., "2024-12-31 23:59:59")', + required: false, + }), + autoCloseLanguages: Property.Checkbox({ + displayName: 'Auto Close Languages', + description: + 'Whether languages should be closed automatically upon completion. Default is true', + required: false, + defaultValue: true, + }), + autoCloseTask: Property.Checkbox({ + displayName: 'Auto Close Task', + description: + 'Whether the task should be automatically closed upon all language completion. Default is true', + required: false, + defaultValue: true, + }), + closingTags: Property.Array({ + displayName: 'Closing Tags', + description: + 'Comma-separated tags to be added to keys when task is closed', + required: false, + }), + doLockTranslations: Property.Checkbox({ + displayName: 'Lock Translations', + description: + 'If set to true, will lock translations for non-assigned project members', + required: false, + defaultValue: false, + }), + markVerified: Property.Checkbox({ + displayName: 'Mark Verified', + description: + 'Mark translations as verified. Only for automatic_translation tasks. Default is true', + required: false, + defaultValue: true, + }), + saveAiTranslationToTm: Property.Checkbox({ + displayName: 'Save AI Translation to TM', + description: + 'Save AI translations to Translation Memory. Only for automatic_translation tasks', + required: false, + defaultValue: false, + }), + applyAiTm100Matches: Property.Checkbox({ + displayName: 'Apply AI TM 100% Matches', + description: + 'Apply 100% translation memory matches. Only for automatic_translation tasks', + required: false, + defaultValue: false, + }), + }, + async run(context) { + const { + projectId, + taskTitle, + description, + taskType, + keys, + languages, + sourceLanguageIso, + dueDate, + autoCloseLanguages, + autoCloseTask, + closingTags, + doLockTranslations, + markVerified, + saveAiTranslationToTm, + applyAiTm100Matches, + } = context.propsValue; + + const body: any = { + title: taskTitle, + ...(description && { description }), + ...(taskType && { task_type: taskType }), + ...(keys && { keys }), + ...(languages && { languages }), + ...(sourceLanguageIso && { source_language_iso: sourceLanguageIso }), + ...(dueDate && { due_date: dueDate }), + ...(autoCloseLanguages !== undefined && { + auto_close_languages: autoCloseLanguages, + }), + ...(autoCloseTask !== undefined && { + auto_close_task: autoCloseTask, + }), + ...(closingTags && { closing_tags: closingTags }), + ...(doLockTranslations && { do_lock_translations: true }), + ...(markVerified !== undefined && { mark_verified: markVerified }), + ...(saveAiTranslationToTm && { + save_ai_translation_to_tm: true, + }), + ...(applyAiTm100Matches && { + apply_ai_tm100_matches: true, + }), + }; + + const path = `/projects/${projectId}/tasks`; + const response = await makeRequest( + context.auth.secret_text, + HttpMethod.POST, + path, + body + ); + + return response; + }, +}); diff --git a/packages/pieces/community/lokalise/src/lib/actions/delete-key.ts b/packages/pieces/community/lokalise/src/lib/actions/delete-key.ts new file mode 100644 index 00000000000..594b46368b4 --- /dev/null +++ b/packages/pieces/community/lokalise/src/lib/actions/delete-key.ts @@ -0,0 +1,29 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { makeRequest } from '../common/client'; +import { lokaliseAuth } from '../common/auth'; +import { projectDropdown, keyIdProp } from '../common/props'; + +export const deleteKey = createAction({ + auth: lokaliseAuth, + name: 'deleteKey', + displayName: 'Delete Key', + description: + 'Delete a key from your Lokalise project (software and marketing projects only)', + props: { + projectId: projectDropdown, + keyId: keyIdProp, + }, + async run(context) { + const { projectId, keyId } = context.propsValue; + + const path = `/projects/${projectId}/keys/${keyId}`; + const response = await makeRequest( + context.auth.secret_text, + HttpMethod.DELETE, + path + ); + + return response; + }, +}); diff --git a/packages/pieces/community/lokalise/src/lib/actions/retrieve-a-comment.ts b/packages/pieces/community/lokalise/src/lib/actions/retrieve-a-comment.ts new file mode 100644 index 00000000000..bf6b358b01d --- /dev/null +++ b/packages/pieces/community/lokalise/src/lib/actions/retrieve-a-comment.ts @@ -0,0 +1,33 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { makeRequest } from '../common/client'; +import { lokaliseAuth } from '../common/auth'; +import { projectDropdown, keyIdProp } from '../common/props'; + +export const retrieveAComment = createAction({ + auth: lokaliseAuth, + name: 'retrieveAComment', + displayName: 'Retrieve a comment', + description: 'Retrieve a specific comment on a key in your Lokalise project', + props: { + projectId: projectDropdown, + keyId: keyIdProp, + commentId: Property.ShortText({ + displayName: 'Comment ID', + description: 'Unique identifier of the comment', + required: true, + }), + }, + async run(context) { + const { projectId, keyId, commentId } = context.propsValue; + + const path = `/projects/${projectId}/keys/${keyId}/comments/${commentId}`; + const response = await makeRequest( + context.auth.secret_text, + HttpMethod.GET, + path + ); + + return response; + }, +}); diff --git a/packages/pieces/community/lokalise/src/lib/actions/retrieve-a-key.ts b/packages/pieces/community/lokalise/src/lib/actions/retrieve-a-key.ts new file mode 100644 index 00000000000..91afca4b11a --- /dev/null +++ b/packages/pieces/community/lokalise/src/lib/actions/retrieve-a-key.ts @@ -0,0 +1,42 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { makeRequest } from '../common/client'; +import { lokaliseAuth } from '../common/auth'; +import { projectDropdown, keyIdProp } from '../common/props'; + +export const retrieveAKey = createAction({ + auth: lokaliseAuth, + name: 'retrieveAKey', + displayName: 'Retrieve a key', + description: 'Retrieve detailed information about a specific key in your Lokalise project', + props: { + projectId: projectDropdown, + keyId: keyIdProp, + disableReferences: Property.Checkbox({ + displayName: 'Disable References', + description: 'Disable key references in the response', + required: false, + defaultValue: false, + }), + }, + async run(context) { + const { projectId, keyId, disableReferences } = context.propsValue; + + const queryParams = new URLSearchParams(); + if (disableReferences) { + queryParams.append('disable_references', '1'); + } + + const path = `/projects/${projectId}/keys/${keyId}${ + queryParams.toString() ? `?${queryParams.toString()}` : '' + }`; + + const response = await makeRequest( + context.auth.secret_text, + HttpMethod.GET, + path + ); + + return response; + }, +}); diff --git a/packages/pieces/community/lokalise/src/lib/actions/retrieve-a-project.ts b/packages/pieces/community/lokalise/src/lib/actions/retrieve-a-project.ts new file mode 100644 index 00000000000..7564f56a237 --- /dev/null +++ b/packages/pieces/community/lokalise/src/lib/actions/retrieve-a-project.ts @@ -0,0 +1,27 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { makeRequest } from '../common/client'; +import { lokaliseAuth } from '../common/auth'; +import { projectDropdown } from '../common/props'; + +export const retrieveAProject = createAction({ + auth: lokaliseAuth, + name: 'retrieveAProject', + displayName: 'Retrieve a project', + description: 'Retrieve detailed information about a Lokalise project', + props: { + projectId: projectDropdown, + }, + async run(context) { + const { projectId } = context.propsValue; + + const path = `/projects/${projectId}`; + const response = await makeRequest( + context.auth.secret_text, + HttpMethod.GET, + path + ); + + return response; + }, +}); diff --git a/packages/pieces/community/lokalise/src/lib/actions/retrieve-translation.ts b/packages/pieces/community/lokalise/src/lib/actions/retrieve-translation.ts new file mode 100644 index 00000000000..5bb0f417903 --- /dev/null +++ b/packages/pieces/community/lokalise/src/lib/actions/retrieve-translation.ts @@ -0,0 +1,46 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { makeRequest } from '../common/client'; +import { lokaliseAuth } from '../common/auth'; +import { projectDropdown } from '../common/props'; + +export const retrieveTranslation = createAction({ + auth: lokaliseAuth, + name: 'retrieveTranslation', + displayName: 'Retrieve Translation', + description: 'Retrieve a specific translation from your Lokalise project', + props: { + projectId: projectDropdown, + translationId: Property.ShortText({ + displayName: 'Translation ID', + description: 'Unique translation identifier', + required: true, + }), + disableReferences: Property.Checkbox({ + displayName: 'Disable References', + description: 'Disable key references in the translation', + required: false, + defaultValue: false, + }), + }, + async run(context) { + const { projectId, translationId, disableReferences } = context.propsValue; + + const queryParams = new URLSearchParams(); + if (disableReferences) { + queryParams.append('disable_references', '1'); + } + + const path = `/projects/${projectId}/translations/${translationId}${ + queryParams.toString() ? `?${queryParams.toString()}` : '' + }`; + + const response = await makeRequest( + context.auth.secret_text, + HttpMethod.GET, + path + ); + + return response; + }, +}); diff --git a/packages/pieces/community/lokalise/src/lib/actions/update-key.ts b/packages/pieces/community/lokalise/src/lib/actions/update-key.ts new file mode 100644 index 00000000000..59351b43b06 --- /dev/null +++ b/packages/pieces/community/lokalise/src/lib/actions/update-key.ts @@ -0,0 +1,125 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { makeRequest } from '../common/client'; +import { lokaliseAuth } from '../common/auth'; +import { projectDropdown, keyIdProp } from '../common/props'; + +export const updateKey = createAction({ + auth: lokaliseAuth, + name: 'updateKey', + displayName: 'Update Key', + description: 'Update properties of a key in your Lokalise project', + props: { + projectId: projectDropdown, + keyId: keyIdProp, + keyName: Property.ShortText({ + displayName: 'Key Name', + description: + 'Key identifier (or JSON with ios, android, web, other for per-platform names)', + required: false, + }), + description: Property.LongText({ + displayName: 'Description', + description: 'Description of the key', + required: false, + }), + platforms: Property.StaticMultiSelectDropdown({ + displayName: 'Platforms', + description: 'List of platforms enabled for this key', + required: false, + options: { + options: [ + { label: 'iOS', value: 'ios' }, + { label: 'Android', value: 'android' }, + { label: 'Web', value: 'web' }, + { label: 'Other', value: 'other' }, + ], + }, + }), + tags: Property.Array({ + displayName: 'Tags', + description: 'Comma-separated tags for the key', + required: false, + }), + mergeTags: Property.Checkbox({ + displayName: 'Merge Tags', + description: 'Enable to merge specified tags with current tags', + required: false, + defaultValue: false, + }), + isPlural: Property.Checkbox({ + displayName: 'Is Plural', + description: 'Whether this key is plural', + required: false, + defaultValue: false, + }), + pluralName: Property.ShortText({ + displayName: 'Plural Name', + description: 'Optional custom plural name', + required: false, + }), + isHidden: Property.Checkbox({ + displayName: 'Is Hidden', + description: 'Whether this key is hidden from non-admins', + required: false, + defaultValue: false, + }), + isArchived: Property.Checkbox({ + displayName: 'Is Archived', + description: 'Whether this key is archived', + required: false, + defaultValue: false, + }), + context: Property.ShortText({ + displayName: 'Context', + description: 'Optional context of the key (used with some file formats)', + required: false, + }), + charLimit: Property.ShortText({ + displayName: 'Character Limit', + description: 'Maximum allowed number of characters in translations', + required: false, + }), + }, + async run(context) { + const { + projectId, + keyId, + keyName, + description, + platforms, + tags, + mergeTags, + isPlural, + pluralName, + isHidden, + isArchived, + context: keyContext, + charLimit, + } = context.propsValue; + + const body: any = { + ...(keyName && { key_name: keyName }), + ...(description && { description }), + ...(platforms && platforms.length > 0 && { platforms }), + ...(tags && tags.length > 0 && { tags }), + ...(mergeTags && { merge_tags: true }), + ...(isPlural !== undefined && { is_plural: isPlural }), + ...(pluralName && { plural_name: pluralName }), + ...(isHidden !== undefined && { is_hidden: isHidden }), + ...(isArchived !== undefined && { is_archived: isArchived }), + ...(keyContext && { context: keyContext }), + ...(charLimit && { char_limit: parseInt(charLimit, 10) }), + }; + + const path = `/projects/${projectId}/keys/${keyId}`; + const response = await makeRequest( + context.auth.secret_text, + HttpMethod.PUT, + path, + body + ); + + return response; + }, +}); diff --git a/packages/pieces/community/lokalise/src/lib/actions/update-translation.ts b/packages/pieces/community/lokalise/src/lib/actions/update-translation.ts new file mode 100644 index 00000000000..44797bbab67 --- /dev/null +++ b/packages/pieces/community/lokalise/src/lib/actions/update-translation.ts @@ -0,0 +1,74 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { makeRequest } from '../common/client'; +import { lokaliseAuth } from '../common/auth'; +import { projectDropdown } from '../common/props'; + +export const updateTranslation = createAction({ + auth: lokaliseAuth, + name: 'updateTranslation', + displayName: 'Update Translation', + description: 'Update a translation in your Lokalise project', + props: { + projectId: projectDropdown, + translationId: Property.ShortText({ + displayName: 'Translation ID', + description: 'Unique translation identifier', + required: true, + }), + translation: Property.LongText({ + displayName: 'Translation', + description: + 'The actual translation content. Use JSON object format for plural keys', + required: true, + }), + isUnverified: Property.Checkbox({ + displayName: 'Mark as Unverified', + description: 'Whether the Unverified flag is enabled', + required: false, + defaultValue: false, + }), + isReviewed: Property.Checkbox({ + displayName: 'Mark as Reviewed', + description: 'Whether the Reviewed flag is enabled', + required: false, + defaultValue: false, + }), + custom_translation_status_ids: Property.Array({ + displayName: 'Custom Translation Status IDs', + description: + 'Comma-separated custom translation status IDs to assign (existing statuses will be replaced)', + required: false, + }), + }, + async run(context) { + const { + projectId, + translationId, + translation, + isUnverified, + isReviewed, + custom_translation_status_ids, + } = context.propsValue; + + const body: any = { + translation, + ...(isUnverified !== undefined && { is_unverified: isUnverified }), + ...(isReviewed !== undefined && { is_reviewed: isReviewed }), + ...(custom_translation_status_ids && + custom_translation_status_ids.length > 0 && { + custom_translation_status_ids: custom_translation_status_ids, + }), + }; + + const path = `/projects/${projectId}/translations/${translationId}`; + const response = await makeRequest( + context.auth.secret_text, + HttpMethod.PUT, + path, + body + ); + + return response; + }, +}); diff --git a/packages/pieces/community/lokalise/src/lib/common/auth.ts b/packages/pieces/community/lokalise/src/lib/common/auth.ts new file mode 100644 index 00000000000..7152d7cb8bd --- /dev/null +++ b/packages/pieces/community/lokalise/src/lib/common/auth.ts @@ -0,0 +1,8 @@ +import { PieceAuth } from '@activepieces/pieces-framework'; + +export const lokaliseAuth = PieceAuth.SecretText({ + displayName: 'API Token', + description: + 'Lokalise API Token. You can generate one from your Lokalise account.', + required: true, +}); diff --git a/packages/pieces/community/lokalise/src/lib/common/client.ts b/packages/pieces/community/lokalise/src/lib/common/client.ts new file mode 100644 index 00000000000..da1f1720b8c --- /dev/null +++ b/packages/pieces/community/lokalise/src/lib/common/client.ts @@ -0,0 +1,25 @@ +import { HttpMethod, httpClient } from '@activepieces/pieces-common'; + +export const BASE_URL = `https://api.lokalise.com/api2`; + +export async function makeRequest( + apikey: string, + method: HttpMethod, + path: string, + body?: unknown +) { + try { + const response = await httpClient.sendRequest({ + method, + url: `${BASE_URL}${path}`, + headers: { + 'X-Api-Token': apikey, + 'Content-Type': 'application/json', + }, + body, + }); + return response.body; + } catch (error: any) { + throw new Error(`Unexpected error: ${error.message || String(error)}`); + } +} diff --git a/packages/pieces/community/lokalise/src/lib/common/props.ts b/packages/pieces/community/lokalise/src/lib/common/props.ts new file mode 100644 index 00000000000..e05befc69d1 --- /dev/null +++ b/packages/pieces/community/lokalise/src/lib/common/props.ts @@ -0,0 +1,91 @@ +import { Property } from '@activepieces/pieces-framework'; +import { lokaliseAuth } from './auth'; +import { makeRequest } from './client'; +import { HttpMethod } from '@activepieces/pieces-common'; + +export const projectDropdown = Property.Dropdown({ + auth: lokaliseAuth, + displayName: 'Project', + description: 'Select the Lokalise project', + required: true, + refreshers: [], + options: async ({ auth }) => { + if (!auth) { + return { + disabled: true, + placeholder: 'Please authenticate first', + options: [], + }; + } + try { + const response = await makeRequest( + auth.secret_text, + HttpMethod.GET, + '/projects' + ); + const projects = response.projects; + + return { + disabled: false, + options: projects.map((project: any) => ({ + label: project.name, + value: project.project_id, + })), + }; + } catch (error) { + return { + disabled: true, + options: [], + }; + } + }, +}); + +export const keyIdProp = Property.Dropdown({ + auth: lokaliseAuth, + displayName: 'Key', + description: 'Select the key', + required: true, + refreshers: ['projectId'], + options: async ({ auth, projectId }) => { + if (!auth) { + return { + disabled: true, + placeholder: 'Please authenticate first', + options: [], + }; + } + if (!projectId) { + return { + disabled: true, + placeholder: 'Please select a project first', + options: [], + }; + } + try { + const response = await makeRequest( + auth.secret_text, + HttpMethod.GET, + `/projects/${projectId}/keys` + ); + const keys = response.keys; + + return { + disabled: false, + options: keys.map((key: any) => ({ + label: + key.key_name.other || + key.key_name.ios || + key.key_name.android || + key.key_name.web, + value: key.key_id, + })), + }; + } catch (error) { + return { + disabled: true, + options: [], + }; + } + }, +}); diff --git a/packages/pieces/community/lokalise/src/lib/triggers/key-added.ts b/packages/pieces/community/lokalise/src/lib/triggers/key-added.ts new file mode 100644 index 00000000000..b83eb918f64 --- /dev/null +++ b/packages/pieces/community/lokalise/src/lib/triggers/key-added.ts @@ -0,0 +1,89 @@ +import { + createTrigger, + TriggerStrategy, + AppConnectionValueForAuthProperty, +} from '@activepieces/pieces-framework'; +import { + DedupeStrategy, + HttpMethod, + Polling, + pollingHelper, +} from '@activepieces/pieces-common'; +import { lokaliseAuth } from '../common/auth'; +import { projectDropdown } from '../common/props'; +import { makeRequest } from '../common/client'; + +const polling: Polling< + AppConnectionValueForAuthProperty, + { projectId: string } +> = { + strategy: DedupeStrategy.TIMEBASED, + items: async ({ auth, propsValue, lastFetchEpochMS }) => { + const keys = (await makeRequest( + auth.secret_text, + HttpMethod.GET, + `/projects/${propsValue.projectId}/keys` + )) as any; + console.log(keys) + return keys.keys + .filter( + (key: { created_at_timestamp: number }) => + key.created_at_timestamp * 1000 > lastFetchEpochMS + ) + .map((key: any) => ({ + epochMilliSeconds: key.created_at_timestamp * 1000, + data: key, + })); + }, +}; + +export const keyAdded = createTrigger({ + auth: lokaliseAuth, + name: 'keyAdded', + displayName: 'Key Added', + description: 'Trigger when a new key is added to your Lokalise project', + props: { + projectId: projectDropdown, + }, + sampleData: { + event: 'project.key.added', + key: { + id: 783570856, + name: 'index.welcome', + base_value: null, + filenames: { + ios: null, + android: null, + web: null, + other: null, + }, + tags: [], + }, + project: { + id: 'aasasasasas', + name: 'test', + }, + user: { + full_name: 'fadse', + email: 'sasdf@gmail.com', + }, + created_at: '2026-01-09 07:38:20', + created_at_timestamp: 1767940700, + }, + type: TriggerStrategy.POLLING, + async test(context) { + return await pollingHelper.test(polling, context); + }, + + async onEnable(context) { + await pollingHelper.onEnable(polling, context); + }, + + async onDisable(context) { + await pollingHelper.onDisable(polling, context); + }, + + async run(context) { + return await pollingHelper.poll(polling, context); + }, +}); diff --git a/packages/pieces/community/lokalise/src/lib/triggers/key-updated.ts b/packages/pieces/community/lokalise/src/lib/triggers/key-updated.ts new file mode 100644 index 00000000000..e4763c20f81 --- /dev/null +++ b/packages/pieces/community/lokalise/src/lib/triggers/key-updated.ts @@ -0,0 +1,81 @@ +import { + AppConnectionValueForAuthProperty, + createTrigger, + TriggerStrategy, +} from '@activepieces/pieces-framework'; +import { lokaliseAuth } from '../common/auth'; +import { makeRequest } from '../common/client'; +import { + DedupeStrategy, + HttpMethod, + Polling, + pollingHelper, +} from '@activepieces/pieces-common'; +import { projectDropdown } from '../common/props'; + +const polling: Polling< + AppConnectionValueForAuthProperty, + { projectId: string } +> = { + strategy: DedupeStrategy.TIMEBASED, + items: async ({ auth, propsValue, lastFetchEpochMS }) => { + const keys = (await makeRequest( + auth.secret_text, + HttpMethod.GET, + `/projects/${propsValue.projectId}/keys` + )) as any; + + return keys.keys + .filter( + (key: { modified_at_timestamp: number }) => + key.modified_at_timestamp * 1000 > lastFetchEpochMS + ) + .map((key: any) => ({ + epochMilliSeconds: key.modified_at_timestamp * 1000, + data: key, + })); + }, +}; + +export const keyUpdated = createTrigger({ + auth: lokaliseAuth, + name: 'keyUpdated', + displayName: 'Key Updated', + description: 'Trigger when a key is updated in your Lokalise project', + props: { projectId: projectDropdown }, + sampleData: { + event: 'project.key.modified', + key: { + id: 782130622, + name: 'test update key', + previous_name: 'welcome_header', + filenames: { ios: null, android: null, web: null, other: null }, + tags: [], + hidden: false, + screenshots: [], + }, + project: { id: '30473913695e05bacfe965.32690341', name: 'test' }, + user: { + full_name: 'jon ', + email: 'jon@example.com', + }, + created_at: '2026-01-09 07:43:32', + created_at_timestamp: 1767941012, + }, + type: TriggerStrategy.POLLING, + async test(context) { + return await pollingHelper.test(polling, context); + }, + + async onEnable(context) { + await pollingHelper.onEnable(polling, context); + }, + + async onDisable(context) { + await pollingHelper.onDisable(polling, context); + }, + + async run(context) { + return await pollingHelper.poll(polling, context); + }, +}); diff --git a/packages/pieces/community/lokalise/src/lib/triggers/translation-updated.ts b/packages/pieces/community/lokalise/src/lib/triggers/translation-updated.ts new file mode 100644 index 00000000000..41cc96966bf --- /dev/null +++ b/packages/pieces/community/lokalise/src/lib/triggers/translation-updated.ts @@ -0,0 +1,65 @@ +import { + AppConnectionValueForAuthProperty, + createTrigger, + TriggerStrategy, +} from '@activepieces/pieces-framework'; +import { + DedupeStrategy, + HttpMethod, + Polling, + pollingHelper, +} from '@activepieces/pieces-common'; +import { lokaliseAuth } from '../common/auth'; +import { projectDropdown } from '../common/props'; +import { makeRequest } from '../common/client'; + +const polling: Polling< + AppConnectionValueForAuthProperty, + { projectId: string } +> = { + strategy: DedupeStrategy.TIMEBASED, + items: async ({ auth, propsValue, lastFetchEpochMS }) => { + const translations = (await makeRequest( + auth.secret_text, + HttpMethod.GET, + `/projects/${propsValue.projectId}/translations` + )) as any; + + return translations.translations + .filter( + (translation: { modified_at_timestamp: number }) => + translation.modified_at_timestamp * 1000 > lastFetchEpochMS + ) + .map((translation: any) => ({ + epochMilliSeconds: translation.modified_at_timestamp * 1000, + data: translation, + })); + }, +}; + +export const translationUpdated = createTrigger({ + auth: lokaliseAuth, + name: 'translationUpdated', + displayName: 'Translation Updated', + description: 'Trigger when a translation is updated in your Lokalise project', + props: { + projectId: projectDropdown, + }, + sampleData: {}, + type: TriggerStrategy.POLLING, + async test(context) { + return await pollingHelper.test(polling, context); + }, + + async onEnable(context) { + await pollingHelper.onEnable(polling, context); + }, + + async onDisable(context) { + await pollingHelper.onDisable(polling, context); + }, + + async run(context) { + return await pollingHelper.poll(polling, context); + }, +}); diff --git a/packages/pieces/community/lokalise/tsconfig.json b/packages/pieces/community/lokalise/tsconfig.json new file mode 100644 index 00000000000..eff240ac143 --- /dev/null +++ b/packages/pieces/community/lokalise/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "forceConsistentCasingInFileNames": true, + "strict": true, + "importHelpers": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noPropertyAccessFromIndexSignature": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ] +} diff --git a/packages/pieces/community/lokalise/tsconfig.lib.json b/packages/pieces/community/lokalise/tsconfig.lib.json new file mode 100644 index 00000000000..5995d959bf0 --- /dev/null +++ b/packages/pieces/community/lokalise/tsconfig.lib.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../dist/out-tsc", + "declaration": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"] +} diff --git a/packages/pieces/community/metabase/package.json b/packages/pieces/community/metabase/package.json index 2b73b2f9672..d875e106e7d 100644 --- a/packages/pieces/community/metabase/package.json +++ b/packages/pieces/community/metabase/package.json @@ -2,6 +2,6 @@ "name": "@activepieces/piece-metabase", "version": "0.2.0", "dependencies": { - "playwright": "1.55.1" + "playwright": "1.56.0" } } diff --git a/packages/pieces/community/time-ops/.eslintrc.json b/packages/pieces/community/time-ops/.eslintrc.json new file mode 100644 index 00000000000..4a4e695c547 --- /dev/null +++ b/packages/pieces/community/time-ops/.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/time-ops/README.md b/packages/pieces/community/time-ops/README.md new file mode 100644 index 00000000000..51de39c04b0 --- /dev/null +++ b/packages/pieces/community/time-ops/README.md @@ -0,0 +1,7 @@ +# pieces-time-ops + +This library was generated with [Nx](https://nx.dev). + +## Building + +Run `nx build pieces-time-ops` to build the library. diff --git a/packages/pieces/community/time-ops/package.json b/packages/pieces/community/time-ops/package.json new file mode 100644 index 00000000000..b6c8f06aa62 --- /dev/null +++ b/packages/pieces/community/time-ops/package.json @@ -0,0 +1,10 @@ +{ + "name": "@activepieces/piece-time-ops", + "version": "0.0.1", + "type": "commonjs", + "main": "./src/index.js", + "types": "./src/index.d.ts", + "dependencies": { + "tslib": "^2.3.0" + } +} diff --git a/packages/pieces/community/time-ops/project.json b/packages/pieces/community/time-ops/project.json new file mode 100644 index 00000000000..e94e988b914 --- /dev/null +++ b/packages/pieces/community/time-ops/project.json @@ -0,0 +1,66 @@ +{ + "name": "pieces-time-ops", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "packages/pieces/community/time-ops/src", + "projectType": "library", + "release": { + "version": { + "manifestRootsToUpdate": [ + "dist/{projectRoot}" + ], + "currentVersionResolver": "git-tag", + "fallbackCurrentVersionResolver": "disk" + } + }, + "tags": [], + "targets": { + "build": { + "executor": "@nx/js:tsc", + "outputs": [ + "{options.outputPath}" + ], + "options": { + "outputPath": "dist/packages/pieces/community/time-ops", + "tsConfig": "packages/pieces/community/time-ops/tsconfig.lib.json", + "packageJson": "packages/pieces/community/time-ops/package.json", + "main": "packages/pieces/community/time-ops/src/index.ts", + "assets": [ + "packages/pieces/community/time-ops/*.md", + { + "input": "packages/pieces/community/time-ops/src/i18n", + "output": "./src/i18n", + "glob": "**/!(i18n.json)" + } + ], + "buildableProjectDepsInPackageJsonType": "dependencies", + "updateBuildableProjectDepsInPackageJson": true, + "clean": false + }, + "dependsOn": [ + "prebuild", + "^build" + ] + }, + "nx-release-publish": { + "options": { + "packageRoot": "dist/{projectRoot}" + } + }, + "prebuild": { + "dependsOn": [ + "^build" + ], + "executor": "nx:run-commands", + "options": { + "cwd": "packages/pieces/community/time-ops", + "command": "bun install --no-save --silent" + } + }, + "lint": { + "executor": "@nx/eslint:lint", + "outputs": [ + "{options.outputFile}" + ] + } + } +} \ No newline at end of file diff --git a/packages/pieces/community/time-ops/src/index.ts b/packages/pieces/community/time-ops/src/index.ts new file mode 100644 index 00000000000..059b9443af3 --- /dev/null +++ b/packages/pieces/community/time-ops/src/index.ts @@ -0,0 +1,69 @@ +import { createPiece, PieceAuth } from '@activepieces/pieces-framework'; +import { PieceCategory } from '@activepieces/shared'; +import { + createCustomApiCallAction, + httpClient, + HttpMethod, +} from '@activepieces/pieces-common'; + +import { BASE_URL } from './lib/common'; +import { createCustomer } from './lib/actions/create-customer'; +import { createProject } from './lib/actions/create-project'; +import { startTimer } from './lib/actions/start-timer'; +import { createRegistration } from './lib/actions/create-registration'; +import { stopTimer } from './lib/actions/stop-timer'; +import { newCustomer } from './lib/triggers/new-customer'; +import { newProject } from './lib/triggers/new-project'; +import { newUser } from './lib/triggers/new-user'; +import { newRegistration } from './lib/triggers/new-registration'; + +export const timeOpsAuth = PieceAuth.SecretText({ + displayName: 'API Key', + description: 'Enter your TimeOps API key. You can find it in your TimeOps account settings.', + required: true, + validate: async ({ auth }) => { + try { + await httpClient.sendRequest({ + method: HttpMethod.GET, + url: `${BASE_URL}/customers`, + headers: { + 'x-api-key': auth, + }, + }); + + return { + valid: true, + }; + } catch { + return { + valid: false, + error: 'Invalid API key.', + }; + } + }, +}); + +export const timeOps = createPiece({ + displayName: 'TimeOps', + description: 'Time tracking and project management for teams and freelancers.', + auth: timeOpsAuth, + minimumSupportedRelease: '0.36.1', + logoUrl: 'https://cdn.activepieces.com/pieces/time-ops.png', + authors: ['onyedikachi-david'], + categories: [PieceCategory.PRODUCTIVITY], + actions: [ + createCustomer, + createProject, + startTimer, + stopTimer, + createRegistration, + createCustomApiCallAction({ + auth: timeOpsAuth, + baseUrl: () => BASE_URL, + authMapping: async (auth) => ({ + 'x-api-key': auth.secret_text, + }), + }), + ], + triggers: [newCustomer, newProject, newUser, newRegistration], +}); \ No newline at end of file diff --git a/packages/pieces/community/time-ops/src/lib/actions/create-customer.ts b/packages/pieces/community/time-ops/src/lib/actions/create-customer.ts new file mode 100644 index 00000000000..824b1acd438 --- /dev/null +++ b/packages/pieces/community/time-ops/src/lib/actions/create-customer.ts @@ -0,0 +1,42 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { timeOpsAuth } from '../..'; +import { timeOpsClient } from '../common'; + +export const createCustomer = createAction({ + auth: timeOpsAuth, + name: 'create_customer', + displayName: 'Create Customer', + description: 'Creates a customer.', + props: { + name: Property.ShortText({ + displayName: 'Name', + description: 'The name of the customer.', + required: false, + }), + vatNumber: Property.ShortText({ + displayName: 'VAT Number', + description: 'The VAT number of the customer.', + required: false, + }), + defaultRate: Property.Number({ + displayName: 'Default Rate', + description: 'The default hourly rate for this customer.', + required: false, + }), + }, + async run(context) { + const { name, vatNumber, defaultRate } = context.propsValue; + + return await timeOpsClient.makeRequest( + context.auth.secret_text, + HttpMethod.POST, + '/customers', + { + name: name ?? null, + vatNumber: vatNumber ?? null, + defaultRate: defaultRate ?? null, + } + ); + }, +}); diff --git a/packages/pieces/community/time-ops/src/lib/actions/create-project.ts b/packages/pieces/community/time-ops/src/lib/actions/create-project.ts new file mode 100644 index 00000000000..be1991b258e --- /dev/null +++ b/packages/pieces/community/time-ops/src/lib/actions/create-project.ts @@ -0,0 +1,84 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { httpClient, HttpMethod } from '@activepieces/pieces-common'; +import { timeOpsAuth } from '../..'; +import { BASE_URL, timeOpsClient } from '../common'; + +export const createProject = createAction({ + auth: timeOpsAuth, + name: 'create_project', + displayName: 'Create Project', + description: 'Create a project.', + props: { + name: Property.ShortText({ + displayName: 'Name', + description: 'The name of the project.', + required: true, + }), + customerId: Property.Dropdown({ + displayName: 'Customer', + description: 'The customer this project belongs to.', + auth: timeOpsAuth, + required: true, + refreshers: [], + options: async ({ auth }) => { + if (!auth) { + return { + disabled: true, + placeholder: 'Please connect your account.', + options: [], + }; + } + + const response = await httpClient.sendRequest< + { id: number; name: string }[] + >({ + method: HttpMethod.GET, + url: `${BASE_URL}/Customers`, + headers: { + 'x-api-key': (auth as { secret_text: string }).secret_text, + }, + }); + + return { + disabled: false, + options: response.body.map((customer) => ({ + label: customer.name ?? `Customer ${customer.id}`, + value: customer.id, + })), + }; + }, + }), + billable: Property.Checkbox({ + displayName: 'Billable', + description: 'Whether the project is billable.', + required: false, + defaultValue: false, + }), + rate: Property.Number({ + displayName: 'Rate', + description: 'The hourly rate for this project.', + required: false, + }), + finishedAt: Property.DateTime({ + displayName: 'Finished At', + description: 'The date and time when the project was finished.', + required: false, + }), + }, + async run(context) { + const { name, customerId, billable, rate, finishedAt } = context.propsValue; + + return await timeOpsClient.makeRequest( + context.auth.secret_text, + HttpMethod.POST, + '/Projects', + { + name, + customerId, + billable: billable ?? false, + rate: rate ?? null, + finishedAt: finishedAt ?? null, + } + ); + }, +}); diff --git a/packages/pieces/community/time-ops/src/lib/actions/create-registration.ts b/packages/pieces/community/time-ops/src/lib/actions/create-registration.ts new file mode 100644 index 00000000000..35f982c1f5a --- /dev/null +++ b/packages/pieces/community/time-ops/src/lib/actions/create-registration.ts @@ -0,0 +1,127 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { httpClient, HttpMethod } from '@activepieces/pieces-common'; +import { timeOpsAuth } from '../..'; +import { BASE_URL, timeOpsClient } from '../common'; + +export const createRegistration = createAction({ + auth: timeOpsAuth, + name: 'create_registration', + displayName: 'Create Registration', + description: 'Creates a registration.', + props: { + userId: Property.Dropdown({ + displayName: 'User', + description: 'The user for this registration.', + auth: timeOpsAuth, + required: true, + refreshers: [], + options: async ({ auth }) => { + if (!auth) { + return { + disabled: true, + placeholder: 'Please connect your account.', + options: [], + }; + } + + const response = await httpClient.sendRequest< + { id: number; name: string }[] + >({ + method: HttpMethod.GET, + url: `${BASE_URL}/Users`, + headers: { + 'x-api-key': (auth as { secret_text: string }).secret_text, + }, + }); + + return { + disabled: false, + options: response.body.map((user) => ({ + label: user.name ?? `User ${user.id}`, + value: user.id, + })), + }; + }, + }), + startedAt: Property.DateTime({ + displayName: 'Started At', + description: 'The start date and time of the registration.', + required: true, + }), + stoppedAt: Property.DateTime({ + displayName: 'Stopped At', + description: 'The end date and time of the registration.', + required: false, + }), + projectId: Property.Dropdown({ + displayName: 'Project', + description: 'The project to associate with this registration.', + auth: timeOpsAuth, + required: false, + refreshers: [], + options: async ({ auth }) => { + if (!auth) { + return { + disabled: true, + placeholder: 'Please connect your account.', + options: [], + }; + } + + const response = await httpClient.sendRequest< + { id: number; name: string }[] + >({ + method: HttpMethod.GET, + url: `${BASE_URL}/Projects`, + headers: { + 'x-api-key': (auth as { secret_text: string }).secret_text, + }, + }); + + return { + disabled: false, + options: response.body.map((project) => ({ + label: project.name ?? `Project ${project.id}`, + value: project.id, + })), + }; + }, + }), + description: Property.LongText({ + displayName: 'Description', + description: 'Description of the registration.', + required: false, + }), + billable: Property.Checkbox({ + displayName: 'Billable', + description: 'Whether this registration is billable.', + required: false, + defaultValue: false, + }), + tags: Property.Array({ + displayName: 'Tags', + description: 'Tag IDs to associate with this registration.', + required: false, + }), + }, + async run(context) { + const { userId, startedAt, stoppedAt, projectId, description, billable, tags } = context.propsValue; + + const tagIds = tags?.map((tag) => parseInt(tag as string, 10)).filter((id) => !isNaN(id)) ?? null; + + return await timeOpsClient.makeRequest( + context.auth.secret_text, + HttpMethod.POST, + '/Registrations', + { + userId, + startedAt, + stoppedAt: stoppedAt ?? null, + projectId: projectId ?? null, + description: description ?? null, + billable: billable ?? false, + tags: tagIds, + } + ); + }, +}); diff --git a/packages/pieces/community/time-ops/src/lib/actions/start-timer.ts b/packages/pieces/community/time-ops/src/lib/actions/start-timer.ts new file mode 100644 index 00000000000..31c36ba85f0 --- /dev/null +++ b/packages/pieces/community/time-ops/src/lib/actions/start-timer.ts @@ -0,0 +1,114 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { httpClient, HttpMethod } from '@activepieces/pieces-common'; +import { timeOpsAuth } from '../..'; +import { BASE_URL, timeOpsClient } from '../common'; + +export const startTimer = createAction({ + auth: timeOpsAuth, + name: 'start_timer', + displayName: 'Start Timer', + description: 'Starts the timer by creating a new Registration.', + props: { + userId: Property.Dropdown({ + displayName: 'User', + description: 'The user to start the timer for.', + auth: timeOpsAuth, + required: true, + refreshers: [], + options: async ({ auth }) => { + if (!auth) { + return { + disabled: true, + placeholder: 'Please connect your account.', + options: [], + }; + } + + const response = await httpClient.sendRequest< + { id: number; name: string }[] + >({ + method: HttpMethod.GET, + url: `${BASE_URL}/Users`, + headers: { + 'x-api-key': (auth as { secret_text: string }).secret_text, + }, + }); + + return { + disabled: false, + options: response.body.map((user) => ({ + label: user.name ?? `User ${user.id}`, + value: user.id, + })), + }; + }, + }), + projectId: Property.Dropdown({ + displayName: 'Project', + description: 'The project to associate with this timer.', + auth: timeOpsAuth, + required: false, + refreshers: [], + options: async ({ auth }) => { + if (!auth) { + return { + disabled: true, + placeholder: 'Please connect your account.', + options: [], + }; + } + + const response = await httpClient.sendRequest< + { id: number; name: string }[] + >({ + method: HttpMethod.GET, + url: `${BASE_URL}/Projects`, + headers: { + 'x-api-key': (auth as { secret_text: string }).secret_text, + }, + }); + + return { + disabled: false, + options: response.body.map((project) => ({ + label: project.name ?? `Project ${project.id}`, + value: project.id, + })), + }; + }, + }), + description: Property.LongText({ + displayName: 'Description', + description: 'Description of the time entry.', + required: false, + }), + billable: Property.Checkbox({ + displayName: 'Billable', + description: 'Whether this time entry is billable.', + required: false, + defaultValue: false, + }), + tags: Property.Array({ + displayName: 'Tags', + description: 'Tag IDs to associate with this timer.', + required: false, + }), + }, + async run(context) { + const { userId, projectId, description, billable, tags } = context.propsValue; + + const tagIds = tags?.map((tag) => parseInt(tag as string, 10)).filter((id) => !isNaN(id)) ?? null; + + return await timeOpsClient.makeRequest( + context.auth.secret_text, + HttpMethod.POST, + `/Registrations/start/${userId}`, + { + description: description ?? null, + projectId: projectId ?? null, + billable: billable ?? false, + tags: tagIds, + } + ); + }, +}); diff --git a/packages/pieces/community/time-ops/src/lib/actions/stop-timer.ts b/packages/pieces/community/time-ops/src/lib/actions/stop-timer.ts new file mode 100644 index 00000000000..facdba1cd3c --- /dev/null +++ b/packages/pieces/community/time-ops/src/lib/actions/stop-timer.ts @@ -0,0 +1,57 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { httpClient, HttpMethod } from '@activepieces/pieces-common'; +import { timeOpsAuth } from '../..'; +import { BASE_URL, timeOpsClient } from '../common'; + +export const stopTimer = createAction({ + auth: timeOpsAuth, + name: 'stop_timer', + displayName: 'Stop Timer', + description: 'Stop the currently running timer.', + props: { + userId: Property.Dropdown({ + displayName: 'User', + description: 'The user to stop the timer for.', + auth: timeOpsAuth, + required: true, + refreshers: [], + options: async ({ auth }) => { + if (!auth) { + return { + disabled: true, + placeholder: 'Please connect your account.', + options: [], + }; + } + + const response = await httpClient.sendRequest< + { id: number; name: string }[] + >({ + method: HttpMethod.GET, + url: `${BASE_URL}/Users`, + headers: { + 'x-api-key': (auth as { secret_text: string }).secret_text, + }, + }); + + return { + disabled: false, + options: response.body.map((user) => ({ + label: user.name ?? `User ${user.id}`, + value: user.id, + })), + }; + }, + }), + }, + async run(context) { + const { userId } = context.propsValue; + + return await timeOpsClient.makeRequest( + context.auth.secret_text, + HttpMethod.POST, + `/Registrations/stop/${userId}`, + {} + ); + }, +}); diff --git a/packages/pieces/community/time-ops/src/lib/common/index.ts b/packages/pieces/community/time-ops/src/lib/common/index.ts new file mode 100644 index 00000000000..b1d83eb0ef0 --- /dev/null +++ b/packages/pieces/community/time-ops/src/lib/common/index.ts @@ -0,0 +1,22 @@ +import { httpClient, HttpMethod } from '@activepieces/pieces-common'; + +export const BASE_URL = 'https://api.timeops.dk/api/v1'; + +export const timeOpsClient = { + async makeRequest( + auth: string, + method: HttpMethod, + endpoint: string, + body?: unknown + ): Promise { + const response = await httpClient.sendRequest({ + method, + url: `${BASE_URL}${endpoint}`, + headers: { + 'x-api-key': auth, + }, + body, + }); + return response.body; + }, +}; diff --git a/packages/pieces/community/time-ops/src/lib/triggers/new-customer.ts b/packages/pieces/community/time-ops/src/lib/triggers/new-customer.ts new file mode 100644 index 00000000000..38de3a0587a --- /dev/null +++ b/packages/pieces/community/time-ops/src/lib/triggers/new-customer.ts @@ -0,0 +1,88 @@ +import { + AppConnectionValueForAuthProperty, + createTrigger, + TriggerStrategy, +} from '@activepieces/pieces-framework'; +import { + DedupeStrategy, + httpClient, + HttpMethod, + Polling, + pollingHelper, +} from '@activepieces/pieces-common'; +import { timeOpsAuth } from '../..'; +import { BASE_URL } from '../common'; + +interface Customer { + id: number; + vatNumber: string | null; + name: string | null; + defaultRate: number | null; +} + +const polling: Polling, Record> = { + strategy: DedupeStrategy.LAST_ITEM, + items: async ({ auth }) => { + const response = await httpClient.sendRequest({ + method: HttpMethod.GET, + url: `${BASE_URL}/Customers`, + headers: { + 'x-api-key': auth.secret_text, + }, + }); + + const customers = response.body ?? []; + + return customers + .sort((a, b) => b.id - a.id) + .map((customer) => ({ + id: customer.id, + data: customer, + })); + }, +}; + +export const newCustomer = createTrigger({ + auth: timeOpsAuth, + name: 'new_customer', + displayName: 'New Customer', + description: 'Triggers when a new customer is created.', + type: TriggerStrategy.POLLING, + props: {}, + sampleData: { + id: 1, + vatNumber: 'DK12345678', + name: 'Example Customer', + defaultRate: 100, + }, + async onEnable(context) { + await pollingHelper.onEnable(polling, { + auth: context.auth, + store: context.store, + propsValue: context.propsValue, + }); + }, + async onDisable(context) { + await pollingHelper.onDisable(polling, { + auth: context.auth, + store: context.store, + propsValue: context.propsValue, + }); + }, + async run(context) { + return await pollingHelper.poll(polling, { + auth: context.auth, + store: context.store, + propsValue: context.propsValue, + files: context.files, + }); + }, + async test(context) { + return await pollingHelper.test(polling, { + auth: context.auth, + store: context.store, + propsValue: context.propsValue, + files: context.files, + }); + }, +}); diff --git a/packages/pieces/community/time-ops/src/lib/triggers/new-project.ts b/packages/pieces/community/time-ops/src/lib/triggers/new-project.ts new file mode 100644 index 00000000000..a855349c05a --- /dev/null +++ b/packages/pieces/community/time-ops/src/lib/triggers/new-project.ts @@ -0,0 +1,92 @@ +import { + AppConnectionValueForAuthProperty, + createTrigger, + TriggerStrategy, +} from '@activepieces/pieces-framework'; +import { + DedupeStrategy, + httpClient, + HttpMethod, + Polling, + pollingHelper, +} from '@activepieces/pieces-common'; +import { timeOpsAuth } from '../..'; +import { BASE_URL } from '../common'; + +interface Project { + id: number; + name: string | null; + customerId: number; + finishedAt: string | null; + billable: boolean; + rate: number | null; +} + +const polling: Polling, Record> = { + strategy: DedupeStrategy.LAST_ITEM, + items: async ({ auth }) => { + const response = await httpClient.sendRequest({ + method: HttpMethod.GET, + url: `${BASE_URL}/Projects`, + headers: { + 'x-api-key': auth.secret_text, + }, + }); + + const projects = response.body ?? []; + + return projects + .sort((a, b) => b.id - a.id) + .map((project) => ({ + id: project.id, + data: project, + })); + }, +}; + +export const newProject = createTrigger({ + auth: timeOpsAuth, + name: 'new_project', + displayName: 'New Project', + description: 'Triggers when a new project is created.', + type: TriggerStrategy.POLLING, + props: {}, + sampleData: { + id: 1, + name: 'Example Project', + customerId: 1, + finishedAt: null, + billable: true, + rate: 100, + }, + async onEnable(context) { + await pollingHelper.onEnable(polling, { + auth: context.auth, + store: context.store, + propsValue: context.propsValue, + }); + }, + async onDisable(context) { + await pollingHelper.onDisable(polling, { + auth: context.auth, + store: context.store, + propsValue: context.propsValue, + }); + }, + async run(context) { + return await pollingHelper.poll(polling, { + auth: context.auth, + store: context.store, + propsValue: context.propsValue, + files: context.files, + }); + }, + async test(context) { + return await pollingHelper.test(polling, { + auth: context.auth, + store: context.store, + propsValue: context.propsValue, + files: context.files, + }); + }, +}); diff --git a/packages/pieces/community/time-ops/src/lib/triggers/new-registration.ts b/packages/pieces/community/time-ops/src/lib/triggers/new-registration.ts new file mode 100644 index 00000000000..894e389bbc3 --- /dev/null +++ b/packages/pieces/community/time-ops/src/lib/triggers/new-registration.ts @@ -0,0 +1,107 @@ +import { + AppConnectionValueForAuthProperty, + createTrigger, + TriggerStrategy, +} from '@activepieces/pieces-framework'; +import { + DedupeStrategy, + httpClient, + HttpMethod, + Polling, + pollingHelper, +} from '@activepieces/pieces-common'; +import { timeOpsAuth } from '../..'; +import { BASE_URL } from '../common'; + +interface Registration { + id: number; + description: string | null; + startedAt: string; + stoppedAt: string | null; + projectId: number | null; + userId: number; + billable: boolean; + tags: number[] | null; +} + +interface RegistrationResponse { + pageSize: number; + page: number; + totalResults: number; + results: Registration[]; +} + +const polling: Polling, Record> = { + strategy: DedupeStrategy.LAST_ITEM, + items: async ({ auth }) => { + const response = await httpClient.sendRequest({ + method: HttpMethod.GET, + url: `${BASE_URL}/Registrations`, + headers: { + 'x-api-key': auth.secret_text, + }, + queryParams: { + pageSize: '100', + page: '0', + }, + }); + + const registrations = response.body.results ?? []; + + return registrations + .sort((a, b) => b.id - a.id) + .map((registration) => ({ + id: registration.id, + data: registration, + })); + }, +}; + +export const newRegistration = createTrigger({ + auth: timeOpsAuth, + name: 'new_registration', + displayName: 'New Registration', + description: 'Triggers when new registrations are added.', + type: TriggerStrategy.POLLING, + props: {}, + sampleData: { + id: 1, + description: 'Working on project', + startedAt: '2024-01-15T09:00:00Z', + stoppedAt: '2024-01-15T17:00:00Z', + projectId: 1, + userId: 1, + billable: true, + tags: [], + }, + async onEnable(context) { + await pollingHelper.onEnable(polling, { + auth: context.auth, + store: context.store, + propsValue: context.propsValue, + }); + }, + async onDisable(context) { + await pollingHelper.onDisable(polling, { + auth: context.auth, + store: context.store, + propsValue: context.propsValue, + }); + }, + async run(context) { + return await pollingHelper.poll(polling, { + auth: context.auth, + store: context.store, + propsValue: context.propsValue, + files: context.files, + }); + }, + async test(context) { + return await pollingHelper.test(polling, { + auth: context.auth, + store: context.store, + propsValue: context.propsValue, + files: context.files, + }); + }, +}); diff --git a/packages/pieces/community/time-ops/src/lib/triggers/new-user.ts b/packages/pieces/community/time-ops/src/lib/triggers/new-user.ts new file mode 100644 index 00000000000..16be5ab7710 --- /dev/null +++ b/packages/pieces/community/time-ops/src/lib/triggers/new-user.ts @@ -0,0 +1,90 @@ +import { + AppConnectionValueForAuthProperty, + createTrigger, + TriggerStrategy, +} from '@activepieces/pieces-framework'; +import { + DedupeStrategy, + httpClient, + HttpMethod, + Polling, + pollingHelper, +} from '@activepieces/pieces-common'; +import { timeOpsAuth } from '../..'; +import { BASE_URL } from '../common'; + +interface User { + id: number; + name: string | null; + eMail: string | null; + accessLevel: 'User' | 'ProjectManager' | 'Admin'; + deactivatedAt: string | null; +} + +const polling: Polling, Record> = { + strategy: DedupeStrategy.LAST_ITEM, + items: async ({ auth }) => { + const response = await httpClient.sendRequest({ + method: HttpMethod.GET, + url: `${BASE_URL}/users`, + headers: { + 'x-api-key': auth.secret_text, + }, + }); + + const users = response.body ?? []; + + return users + .sort((a, b) => b.id - a.id) + .map((user) => ({ + id: user.id, + data: user, + })); + }, +}; + +export const newUser = createTrigger({ + auth: timeOpsAuth, + name: 'new_user', + displayName: 'New User', + description: 'Triggers when a new user has been created.', + type: TriggerStrategy.POLLING, + props: {}, + sampleData: { + id: 1, + name: 'John Doe', + eMail: 'john@example.com', + accessLevel: 'User', + deactivatedAt: null, + }, + async onEnable(context) { + await pollingHelper.onEnable(polling, { + auth: context.auth, + store: context.store, + propsValue: context.propsValue, + }); + }, + async onDisable(context) { + await pollingHelper.onDisable(polling, { + auth: context.auth, + store: context.store, + propsValue: context.propsValue, + }); + }, + async run(context) { + return await pollingHelper.poll(polling, { + auth: context.auth, + store: context.store, + propsValue: context.propsValue, + files: context.files, + }); + }, + async test(context) { + return await pollingHelper.test(polling, { + auth: context.auth, + store: context.store, + propsValue: context.propsValue, + files: context.files, + }); + }, +}); diff --git a/packages/pieces/community/time-ops/tsconfig.json b/packages/pieces/community/time-ops/tsconfig.json new file mode 100644 index 00000000000..eff240ac143 --- /dev/null +++ b/packages/pieces/community/time-ops/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "forceConsistentCasingInFileNames": true, + "strict": true, + "importHelpers": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noPropertyAccessFromIndexSignature": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ] +} diff --git a/packages/pieces/community/time-ops/tsconfig.lib.json b/packages/pieces/community/time-ops/tsconfig.lib.json new file mode 100644 index 00000000000..5995d959bf0 --- /dev/null +++ b/packages/pieces/community/time-ops/tsconfig.lib.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../dist/out-tsc", + "declaration": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"] +} diff --git a/packages/pieces/community/vouchery-io/.eslintrc.json b/packages/pieces/community/vouchery-io/.eslintrc.json new file mode 100644 index 00000000000..4a4e695c547 --- /dev/null +++ b/packages/pieces/community/vouchery-io/.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/vouchery-io/README.md b/packages/pieces/community/vouchery-io/README.md new file mode 100644 index 00000000000..7931e2b8a0c --- /dev/null +++ b/packages/pieces/community/vouchery-io/README.md @@ -0,0 +1,7 @@ +# pieces-vouchery-io + +This library was generated with [Nx](https://nx.dev). + +## Building + +Run `nx build pieces-vouchery-io` to build the library. diff --git a/packages/pieces/community/vouchery-io/package.json b/packages/pieces/community/vouchery-io/package.json new file mode 100644 index 00000000000..d4c9a18a860 --- /dev/null +++ b/packages/pieces/community/vouchery-io/package.json @@ -0,0 +1,10 @@ +{ + "name": "@activepieces/piece-vouchery-io", + "version": "0.0.1", + "type": "commonjs", + "main": "./src/index.js", + "types": "./src/index.d.ts", + "dependencies": { + "tslib": "^2.3.0" + } +} diff --git a/packages/pieces/community/vouchery-io/project.json b/packages/pieces/community/vouchery-io/project.json new file mode 100644 index 00000000000..0cf68758301 --- /dev/null +++ b/packages/pieces/community/vouchery-io/project.json @@ -0,0 +1,66 @@ +{ + "name": "pieces-vouchery-io", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "packages/pieces/community/vouchery-io/src", + "projectType": "library", + "release": { + "version": { + "manifestRootsToUpdate": [ + "dist/{projectRoot}" + ], + "currentVersionResolver": "git-tag", + "fallbackCurrentVersionResolver": "disk" + } + }, + "tags": [], + "targets": { + "build": { + "executor": "@nx/js:tsc", + "outputs": [ + "{options.outputPath}" + ], + "options": { + "outputPath": "dist/packages/pieces/community/vouchery-io", + "tsConfig": "packages/pieces/community/vouchery-io/tsconfig.lib.json", + "packageJson": "packages/pieces/community/vouchery-io/package.json", + "main": "packages/pieces/community/vouchery-io/src/index.ts", + "assets": [ + "packages/pieces/community/vouchery-io/*.md", + { + "input": "packages/pieces/community/vouchery-io/src/i18n", + "output": "./src/i18n", + "glob": "**/!(i18n.json)" + } + ], + "buildableProjectDepsInPackageJsonType": "dependencies", + "updateBuildableProjectDepsInPackageJson": true, + "clean": false + }, + "dependsOn": [ + "prebuild", + "^build" + ] + }, + "nx-release-publish": { + "options": { + "packageRoot": "dist/{projectRoot}" + } + }, + "prebuild": { + "dependsOn": [ + "^build" + ], + "executor": "nx:run-commands", + "options": { + "cwd": "packages/pieces/community/vouchery-io", + "command": "bun install --no-save --silent" + } + }, + "lint": { + "executor": "@nx/eslint:lint", + "outputs": [ + "{options.outputFile}" + ] + } + } +} \ No newline at end of file diff --git a/packages/pieces/community/vouchery-io/src/index.ts b/packages/pieces/community/vouchery-io/src/index.ts new file mode 100644 index 00000000000..2e513b95c5a --- /dev/null +++ b/packages/pieces/community/vouchery-io/src/index.ts @@ -0,0 +1,31 @@ +import { createPiece, PieceAuth } from '@activepieces/pieces-framework'; +import { voucheryIoAuth } from './lib/common/auth'; +import { findVoucher } from './lib/actions/find-voucher'; +import { createCustomer } from './lib/actions/create-customer'; +import { createAVoucher } from './lib/actions/create-a-voucher'; +import { createCustomApiCallAction } from '@activepieces/pieces-common'; + +export const voucheryIo = createPiece({ + displayName: 'Vouchery', + auth: voucheryIoAuth, + minimumSupportedRelease: '0.36.1', + logoUrl: 'https://cdn.activepieces.com/pieces/vouchery-io.png', + authors: ['sanket-a11y'], + description: + 'Vouchery is a voucher and gift card management platform.', + actions: [ + createAVoucher, + createCustomer, + findVoucher, + createCustomApiCallAction({ + auth: voucheryIoAuth, + baseUrl: () => 'https://admin.sandbox.vouchery.app/api/v2.1', + authMapping: async (auth) => { + return { + Authorization: `Bearer ${auth.secret_text}`, + }; + }, + }), + ], + triggers: [], +}); diff --git a/packages/pieces/community/vouchery-io/src/lib/actions/create-a-voucher.ts b/packages/pieces/community/vouchery-io/src/lib/actions/create-a-voucher.ts new file mode 100644 index 00000000000..daca9174792 --- /dev/null +++ b/packages/pieces/community/vouchery-io/src/lib/actions/create-a-voucher.ts @@ -0,0 +1,71 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { makeRequest } from '../common/client'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { voucheryIoAuth } from '../common/auth'; +import { comapaignIdDropdown } from '../common/props'; +import { tr } from 'zod/v4/locales'; + +export const createAVoucher = createAction({ + auth: voucheryIoAuth, + name: 'createAVoucher', + displayName: 'Create a voucher', + description: 'Create a new voucher in a campaign', + props: { + campaignId: comapaignIdDropdown, + voucherCode: Property.ShortText({ + displayName: 'Voucher Code', + description: 'The code for the voucher', + required: true, + }), + + voucher_valid_until: Property.DateTime({ + displayName: 'Expiration Date', + description: 'The date and time when the voucher expires', + required: false, + }), + customer_identifier: Property.Number({ + displayName: 'Customer Identifier', + description: 'The identifier for the customer', + required: false, + }), + gift_card_value: Property.Number({ + displayName: 'Gift Card Value', + description: 'The value of the gift card', + required: false, + }), + activates_at: Property.DateTime({ + displayName: 'Activation Date', + description: 'The date and time when the voucher becomes active', + required: false, + }), + }, + async run(context) { + const { + campaignId, + voucherCode, + voucher_valid_until, + customer_identifier, + gift_card_value, + activates_at, + } = context.propsValue; + + const body: any = { + code: voucherCode, + }; + + if (voucher_valid_until) + body.valid_until = new Date(voucher_valid_until).toISOString(); + if (customer_identifier) body.customer_identifier = customer_identifier; + if (gift_card_value) body.gift_card_value = gift_card_value; + if (activates_at) body.activates_at = new Date(activates_at).toISOString(); + + const response = await makeRequest( + context.auth.secret_text, + HttpMethod.POST, + `/campaigns/${campaignId}/vouchers`, + body + ); + + return response; + }, +}); diff --git a/packages/pieces/community/vouchery-io/src/lib/actions/create-customer.ts b/packages/pieces/community/vouchery-io/src/lib/actions/create-customer.ts new file mode 100644 index 00000000000..0039ee8d766 --- /dev/null +++ b/packages/pieces/community/vouchery-io/src/lib/actions/create-customer.ts @@ -0,0 +1,101 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { makeRequest } from '../common/client'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { voucheryIoAuth } from '../common/auth'; + +export const createCustomer = createAction({ + auth: voucheryIoAuth, + name: 'createCustomer', + displayName: 'Create Customer', + description: 'Create a new customer', + props: { + identifier: Property.ShortText({ + displayName: 'Customer Identifier', + description: + 'Unique customer identifier in your application. Can be hash, id, email or any other unique value', + required: true, + }), + name: Property.ShortText({ + displayName: 'Customer Name', + description: 'Customer full name', + required: false, + }), + email: Property.ShortText({ + displayName: 'Email', + description: 'Customer email address', + required: false, + }), + birthdate: Property.Object({ + displayName: 'Birthdate', + description: 'Customer birthdate object', + required: false, + }), + categories: Property.Array({ + displayName: 'Categories', + description: + 'Array of category objects to determine how customer is related to specific categories', + required: false, + properties: { + name: Property.ShortText({ + displayName: 'Category Name', + description: 'Name of the category', + required: true, + }), + tag: Property.ShortText({ + displayName: 'tag', + description: 'tag', + required: true, + }), + }, + }), + metadata: Property.Object({ + displayName: 'Metadata', + description: 'Additional metadata for the customer', + required: false, + }), + referrer_code: Property.ShortText({ + displayName: 'Referrer Code', + description: 'A referral code from the recommending user', + required: false, + }), + loyalty_points: Property.Number({ + displayName: 'Loyalty Points', + description: + '[DEPRECATED - use grant-points endpoint instead] Number of loyalty points customer will have', + required: false, + }), + }, + async run(context) { + const { + identifier, + name, + email, + birthdate, + categories, + metadata, + referrer_code, + loyalty_points, + } = context.propsValue; + + const body: any = { + identifier, + categories, + metadata, + }; + + if (name) body.name = name; + if (email) body.email = email; + if (birthdate) body.birthdate = birthdate; + if (referrer_code) body.referrer_code = referrer_code; + if (loyalty_points !== undefined) body.loyalty_points = loyalty_points; + + const response = await makeRequest( + context.auth.secret_text, + HttpMethod.POST, + `/customers`, + body + ); + + return response; + }, +}); diff --git a/packages/pieces/community/vouchery-io/src/lib/actions/find-voucher.ts b/packages/pieces/community/vouchery-io/src/lib/actions/find-voucher.ts new file mode 100644 index 00000000000..d19707673f2 --- /dev/null +++ b/packages/pieces/community/vouchery-io/src/lib/actions/find-voucher.ts @@ -0,0 +1,73 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { makeRequest } from '../common/client'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { voucheryIoAuth } from '../common/auth'; + +export const findVoucher = createAction({ + auth: voucheryIoAuth, + name: 'findVoucher', + displayName: 'Find Voucher', + description: + 'Find a voucher by campaign metadata. At least one metadata must be provided', + props: { + purpose: Property.ShortText({ + displayName: 'Purpose', + description: 'Purpose metadata (multiple values allowed)', + required: false, + }), + team: Property.ShortText({ + displayName: 'Team', + description: 'Team metadata (multiple values allowed)', + required: false, + }), + channel: Property.ShortText({ + displayName: 'Channel', + description: 'Channel metadata (multiple values allowed)', + required: false, + }), + medium: Property.ShortText({ + displayName: 'Medium', + description: 'Medium metadata (multiple values allowed)', + required: false, + }), + customer_identifier: Property.ShortText({ + displayName: 'Customer Identifier', + description: + 'Optional: Assign the found voucher to a customer by providing their identifier', + required: false, + }), + }, + async run(context) { + const { purpose, team, channel, medium, customer_identifier } = + context.propsValue; + + const params = new URLSearchParams(); + + if (purpose && purpose.length > 0) { + params.append('purpose', purpose); + } + if (team && team.length > 0) { + params.append('team', team); + } + if (channel && channel.length > 0) { + params.append('channel', channel); + } + if (medium && medium.length > 0) { + params.append('medium', medium); + } + if (customer_identifier) { + params.append('customer_identifier', customer_identifier); + } + + const queryString = params.toString(); + const path = `/vouchers/find${queryString ? '?' + queryString : ''}`; + + const response = await makeRequest( + context.auth.secret_text, + HttpMethod.GET, + path + ); + + return response; + }, +}); diff --git a/packages/pieces/community/vouchery-io/src/lib/common/auth.ts b/packages/pieces/community/vouchery-io/src/lib/common/auth.ts new file mode 100644 index 00000000000..eebc14d8850 --- /dev/null +++ b/packages/pieces/community/vouchery-io/src/lib/common/auth.ts @@ -0,0 +1,7 @@ +import { PieceAuth } from '@activepieces/pieces-framework'; + +export const voucheryIoAuth = PieceAuth.SecretText({ + displayName: 'API Key', + description: 'Vouchery-io API Key', + required: true, +}); diff --git a/packages/pieces/community/vouchery-io/src/lib/common/client.ts b/packages/pieces/community/vouchery-io/src/lib/common/client.ts new file mode 100644 index 00000000000..309cd26903e --- /dev/null +++ b/packages/pieces/community/vouchery-io/src/lib/common/client.ts @@ -0,0 +1,25 @@ +import { HttpMethod, httpClient } from '@activepieces/pieces-common'; + +const BASE_URL = `https://admin.sandbox.vouchery.app/api/v2.1`; + +export async function makeRequest( + api_key: string, + method: HttpMethod, + path: string, + body?: unknown +) { + try { + const response = await httpClient.sendRequest({ + method, + url: `${BASE_URL}${path}`, + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${api_key}`, + }, + body, + }); + return response.body; + } catch (error: any) { + throw new Error(`Unexpected error: ${error.message || String(error)}`); + } +} diff --git a/packages/pieces/community/vouchery-io/src/lib/common/props.ts b/packages/pieces/community/vouchery-io/src/lib/common/props.ts new file mode 100644 index 00000000000..e76fb7b8b25 --- /dev/null +++ b/packages/pieces/community/vouchery-io/src/lib/common/props.ts @@ -0,0 +1,32 @@ +import { Property } from '@activepieces/pieces-framework'; +import { voucheryIoAuth } from './auth'; +import { makeRequest } from './client'; +import { HttpMethod } from '@activepieces/pieces-common'; + +export const comapaignIdDropdown = Property.Dropdown({ + auth: voucheryIoAuth, + displayName: 'Campaign', + description: 'Select the campaign', + required: true, + refreshers: [], + options: async ({ auth }) => { + if (!auth) { + return { + disabled: true, + options: [], + }; + } + const response = await makeRequest( + auth?.secret_text, + HttpMethod.GET, + `/campaigns` + ) as any[]; + return { + disabled: false, + options: response.map((campaign ) => ({ + label: campaign.name, + value: campaign.id, + })), + }; + }, +}); diff --git a/packages/pieces/community/vouchery-io/tsconfig.json b/packages/pieces/community/vouchery-io/tsconfig.json new file mode 100644 index 00000000000..eff240ac143 --- /dev/null +++ b/packages/pieces/community/vouchery-io/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "forceConsistentCasingInFileNames": true, + "strict": true, + "importHelpers": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noPropertyAccessFromIndexSignature": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ] +} diff --git a/packages/pieces/community/vouchery-io/tsconfig.lib.json b/packages/pieces/community/vouchery-io/tsconfig.lib.json new file mode 100644 index 00000000000..5995d959bf0 --- /dev/null +++ b/packages/pieces/community/vouchery-io/tsconfig.lib.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../dist/out-tsc", + "declaration": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"] +} diff --git a/packages/react-ui/src/app/components/sidebar/builder/flows-navigation.tsx b/packages/react-ui/src/app/components/sidebar/builder/flows-navigation.tsx index f82e22d883a..a803cb799d1 100644 --- a/packages/react-ui/src/app/components/sidebar/builder/flows-navigation.tsx +++ b/packages/react-ui/src/app/components/sidebar/builder/flows-navigation.tsx @@ -23,6 +23,7 @@ import { SidebarSkeleton, } from '@/components/ui/sidebar-shadcn'; import { flowHooks } from '@/features/flows/lib/flow-hooks'; +import { ImportFlowButton } from '@/features/flows/lib/Import-flow-button'; import { NewFlowButton } from '@/features/flows/lib/new-flow-button'; import { CreateFolderDialog } from '@/features/folders/component/create-folder-dialog'; import { FolderActions } from '@/features/folders/component/folder-actions'; @@ -236,6 +237,12 @@ function DefaultFolder({ {t('Uncategorized')}
+ {folder.displayName}
+ { - const newFlow = await flowsApi.create({ - displayName: flowTemplate.displayName, - projectId: projectId, - folderName: folderName, - }); - - return flowsApi.update(newFlow.id, { - type: FlowOperationType.IMPORT_FLOW, - request: { - displayName: flowTemplate.displayName, - trigger: flowTemplate.trigger, - schemaVersion: flowTemplate.schemaVersion, - }, - }); - }), - ); + return await flowHooks.importFlowsFromTemplates({ + templates: [template], + projectId, + folderName, + }); }, onSuccess: (flows) => { onOpenChange(false); diff --git a/packages/react-ui/src/features/flows/components/import-flow-dialog.tsx b/packages/react-ui/src/features/flows/components/import-flow-dialog.tsx index 59034e8c1a5..99ce6c6b2a8 100644 --- a/packages/react-ui/src/features/flows/components/import-flow-dialog.tsx +++ b/packages/react-ui/src/features/flows/components/import-flow-dialog.tsx @@ -36,7 +36,6 @@ import { templatesApi } from '@/features/templates/lib/templates-api'; import { api } from '@/lib/api'; import { authenticationSession } from '@/lib/authentication-session'; import { - FlowOperationType, isNil, PopulatedFlow, TelemetryEventName, @@ -45,7 +44,7 @@ import { } from '@activepieces/shared'; import { FormError } from '../../../components/ui/form'; -import { flowsApi } from '../lib/flows-api'; +import { flowHooks } from '../lib/flow-hooks'; import { templateUtils } from '../lib/template-parser'; export type ImportFlowDialogProps = @@ -97,40 +96,27 @@ const ImportFlowDialog = ( Template[] >({ mutationFn: async (templates: Template[]) => { - const importPromises = templates.flatMap(async (template) => { - const flowImportPromises = (template.flows || []).map( - async (templateFlow) => { - let flow: PopulatedFlow | null = null; - if (props.insideBuilder) { - flow = await flowsApi.get(props.flowId); - } else { - const folder = - !isNil(selectedFolderId) && - selectedFolderId !== UncategorizedFolderId - ? await foldersApi.get(selectedFolderId) - : undefined; - flow = await flowsApi.create({ - displayName: templateFlow.displayName, - projectId: authenticationSession.getProjectId()!, - folderName: folder?.displayName, - }); - } - return await flowsApi.update(flow.id, { - type: FlowOperationType.IMPORT_FLOW, - request: { - displayName: templateFlow.displayName, - trigger: templateFlow.trigger, - schemaVersion: templateFlow.schemaVersion, - }, - }); - }, - ); + if (props.insideBuilder) { + if (templates.length === 0) { + throw new Error('No template selected'); + } + const flow = await flowHooks.importFlowIntoExisting({ + template: templates[0], + existingFlowId: props.flowId, + }); + return [flow]; + } - return Promise.all(flowImportPromises); - }); + const folder = + !isNil(selectedFolderId) && selectedFolderId !== UncategorizedFolderId + ? await foldersApi.get(selectedFolderId) + : undefined; - const results = await Promise.all(importPromises); - return results.flat(); + return flowHooks.importFlowsFromTemplates({ + templates, + projectId: authenticationSession.getProjectId()!, + folderName: folder?.displayName, + }); }, onSuccess: (flows: PopulatedFlow[]) => { diff --git a/packages/react-ui/src/features/flows/components/share-template-dialog.tsx b/packages/react-ui/src/features/flows/components/share-template-dialog.tsx index 6e4d85b2d8d..f49e76d1fd0 100644 --- a/packages/react-ui/src/features/flows/components/share-template-dialog.tsx +++ b/packages/react-ui/src/features/flows/components/share-template-dialog.tsx @@ -61,7 +61,7 @@ const ShareTemplateDialog: React.FC<{ summary: template.summary, tags: template.tags, blogUrl: template.blogUrl ?? undefined, - metadata: null, + metadata: template.metadata, author, categories: template.categories, type: template.type, diff --git a/packages/react-ui/src/features/flows/lib/Import-flow-button.tsx b/packages/react-ui/src/features/flows/lib/Import-flow-button.tsx new file mode 100644 index 00000000000..9e4b702ccb7 --- /dev/null +++ b/packages/react-ui/src/features/flows/lib/Import-flow-button.tsx @@ -0,0 +1,77 @@ +import { t } from 'i18next'; +import { Upload } from 'lucide-react'; + +import { PermissionNeededTooltip } from '@/components/custom/permission-needed-tooltip'; +import { useEmbedding } from '@/components/embed-provider'; +import { Button } from '@/components/ui/button'; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from '@/components/ui/tooltip'; +import { useAuthorization } from '@/hooks/authorization-hooks'; +import { cn } from '@/lib/utils'; +import { Permission } from '@activepieces/shared'; + +import { ImportFlowDialog } from '../components/import-flow-dialog'; + +type ImportFlowButtonProps = { + variant?: 'default' | 'small'; + className?: string; + folderId: string; + onRefresh?: () => void; +}; + +export const ImportFlowButton = ({ + variant = 'default', + className, + folderId, + onRefresh, +}: ImportFlowButtonProps) => { + const { checkAccess } = useAuthorization(); + const { embedState } = useEmbedding(); + const doesUserHavePermissionToWriteFlow = checkAccess(Permission.WRITE_FLOW); + + if (embedState.hideExportAndImportFlow) { + return null; + } + + return ( + + + { + if (onRefresh) onRefresh(); + }} + folderId={folderId} + > + + + + + + {t('Import flow')} + + + + ); +}; diff --git a/packages/react-ui/src/features/flows/lib/flow-hooks.tsx b/packages/react-ui/src/features/flows/lib/flow-hooks.tsx index d3f10cfed3d..73442ad7991 100644 --- a/packages/react-ui/src/features/flows/lib/flow-hooks.tsx +++ b/packages/react-ui/src/features/flows/lib/flow-hooks.tsx @@ -21,6 +21,7 @@ import { FlowStatus, FlowVersion, FlowVersionMetadata, + FlowVersionTemplate, ListFlowsRequest, PopulatedFlow, FlowTrigger, @@ -30,6 +31,7 @@ import { isNil, ErrorCode, SeekPage, + Template, UncategorizedFolderId, } from '@activepieces/shared'; @@ -333,6 +335,112 @@ export const flowHooks = { }, }); }, + importFlowIntoExisting: async ({ + template, + existingFlowId, + }: { + template: Template; + existingFlowId: string; + }): Promise => { + const flows = template.flows || []; + if (flows.length === 0) { + throw new Error('Template has no flows'); + } + + const templateFlow = flows[0]; + const flow = await flowsApi.get(existingFlowId); + + const oldExternalId = !isNil(template.metadata?.externalId) + ? (template.metadata['externalId'] as string) + : flow.externalId; + + const triggerString = JSON.stringify(templateFlow.trigger).replaceAll( + oldExternalId, + flow.externalId, + ); + const updatedTrigger = JSON.parse(triggerString); + + return await flowsApi.update(flow.id, { + type: FlowOperationType.IMPORT_FLOW, + request: { + displayName: templateFlow.displayName, + trigger: updatedTrigger, + schemaVersion: templateFlow.schemaVersion, + }, + }); + }, + importFlowsFromTemplates: async ({ + templates, + projectId, + folderName, + }: { + templates: Template[]; + projectId: string; + folderName?: string; + }): Promise => { + if (templates.length === 0) { + return []; + } + + const allFlowsToImport: Array<{ + flow: PopulatedFlow; + templateFlow: FlowVersionTemplate; + oldExternalId: string; + }> = []; + + for (const template of templates) { + const flows = template.flows || []; + if (flows.length === 0) { + continue; + } + + for (const templateFlow of flows) { + const flow = await flowsApi.create({ + displayName: templateFlow.displayName, + projectId, + folderName, + }); + + const oldExternalId = !isNil(template.metadata?.externalId) + ? (template.metadata['externalId'] as string) + : flow.externalId; + + allFlowsToImport.push({ + flow, + templateFlow, + oldExternalId, + }); + } + } + + const externalIdMap = new Map(); + for (const { oldExternalId, flow } of allFlowsToImport) { + externalIdMap.set(oldExternalId, flow.externalId); + } + + const importPromises = allFlowsToImport.map( + async ({ flow, templateFlow }) => { + let triggerString = JSON.stringify(templateFlow.trigger); + + for (const [oldId, newId] of externalIdMap.entries()) { + triggerString = triggerString.replaceAll(oldId, newId); + } + + const updatedTrigger = JSON.parse(triggerString); + + return await flowsApi.update(flow.id, { + type: FlowOperationType.IMPORT_FLOW, + request: { + displayName: templateFlow.displayName, + trigger: updatedTrigger, + schemaVersion: templateFlow.schemaVersion, + }, + }); + }, + ); + + return await Promise.all(importPromises); + }, }; type UseChangeFlowStatusParams = { diff --git a/packages/react-ui/src/features/flows/lib/use-flows-bulk-actions.tsx b/packages/react-ui/src/features/flows/lib/use-flows-bulk-actions.tsx index 3cc075b2f93..192a7561231 100644 --- a/packages/react-ui/src/features/flows/lib/use-flows-bulk-actions.tsx +++ b/packages/react-ui/src/features/flows/lib/use-flows-bulk-actions.tsx @@ -25,6 +25,7 @@ import { MoveFlowDialog } from '../components/move-flow-dialog'; import { flowHooks } from './flow-hooks'; import { flowsApi } from './flows-api'; +import { ImportFlowButton } from './Import-flow-button'; import { NewFlowButton } from './new-flow-button'; export const useFlowsBulkActions = ({ @@ -177,6 +178,7 @@ export const useFlowsBulkActions = ({ )} +
); diff --git a/packages/react-ui/src/features/templates/components/share-template.tsx b/packages/react-ui/src/features/templates/components/share-template.tsx index 642aabec1e3..8c2f3ad0f35 100644 --- a/packages/react-ui/src/features/templates/components/share-template.tsx +++ b/packages/react-ui/src/features/templates/components/share-template.tsx @@ -8,16 +8,15 @@ import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader } from '@/components/ui/card'; import { Separator } from '@/components/ui/separator'; import { internalErrorToast } from '@/components/ui/sonner'; -import { flowsApi } from '@/features/flows/lib/flows-api'; +import { flowHooks } from '@/features/flows/lib/flow-hooks'; import { api } from '@/lib/api'; import { authenticationSession } from '@/lib/authentication-session'; import { FROM_QUERY_PARAM } from '@/lib/navigation-utils'; import { ApErrorParams, ErrorCode, - FlowOperationType, - Template, isNil, + Template, } from '@activepieces/shared'; import { PieceIconList } from '../../pieces/components/piece-icon-list'; @@ -30,23 +29,11 @@ const TemplateViewer = ({ template }: { template: Template }) => { const { mutate, isPending } = useMutation({ mutationFn: async () => { - const flow = await flowsApi.create({ + const flows = await flowHooks.importFlowsFromTemplates({ + templates: [template], projectId: authenticationSession.getProjectId()!, - displayName: template.name, - }); - const flowTemplate = template.flows?.[0]; - if (!flowTemplate) { - throw new Error('Template does not have a flow template'); - } - const updatedFlow = await flowsApi.update(flow.id, { - type: FlowOperationType.IMPORT_FLOW, - request: { - displayName: flowTemplate.displayName, - trigger: flowTemplate.trigger, - schemaVersion: flowTemplate.schemaVersion, - }, }); - return updatedFlow; + return flows[0]; }, onSuccess: (data) => { templatesApi.incrementUsageCount(template.id); diff --git a/packages/server/api/src/app/flows/flow/flow.service.ts b/packages/server/api/src/app/flows/flow/flow.service.ts index 8063c084428..7796501cd17 100644 --- a/packages/server/api/src/app/flows/flow/flow.service.ts +++ b/packages/server/api/src/app/flows/flow/flow.service.ts @@ -554,7 +554,9 @@ export const flowService = (log: FastifyBaseLogger) => ({ flows: [flow.version], tags: [], blogUrl: '', - metadata: null, + metadata: { + externalId: flow.externalId, + }, author: userMetadata ? `${userMetadata.firstName} ${userMetadata.lastName}` : '', categories: [], type: TemplateType.SHARED, diff --git a/tsconfig.base.json b/tsconfig.base.json index 1d3234c1595..f079403b6d5 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -455,8 +455,26 @@ "@activepieces/piece-billplz": [ "packages/pieces/community/billplz/src/index.ts" ], + "@activepieces/piece-vouchery-io": [ + "packages/pieces/community/vouchery-io/src/index.ts" + ], + "@activepieces/piece-lokalise": [ + "packages/pieces/community/lokalise/src/index.ts" + ], + "@activepieces/piece-digital-ocean": [ + "packages/pieces/community/digital-ocean/src/index.ts" + ], "@activepieces/piece-hashi-corp-vault": [ "packages/pieces/community/hashi-corp-vault/src/index.ts" + ], + "@activepieces/piece-leap-ai": [ + "packages/pieces/community/leap-ai/src/index.ts" + ], + "@activepieces/piece-time-ops": [ + "packages/pieces/community/time-ops/src/index.ts" + ], + "@activepieces/piece-gender-api": [ + "packages/pieces/community/gender-api/src/index.ts" ] }, "resolveJsonModule": true