From b46a29b1a7d9cbe802a7284ab9c1cff7fd2b01f0 Mon Sep 17 00:00:00 2001 From: Rupenieks <32895755+Rupenieks@users.noreply.github.com> Date: Tue, 30 Jun 2020 13:34:09 +0200 Subject: [PATCH 01/19] :construction: Resource descriptions --- .../credentials/PaddleApi.credentials.ts | 23 + .../nodes/Paddle/CouponDescription.ts | 447 ++++++++++++++++++ .../nodes/Paddle/GenericFunctions.ts | 41 ++ .../nodes/Paddle/OrderDescription.ts | 52 ++ .../nodes-base/nodes/Paddle/Paddle.node.ts | 92 ++++ .../nodes/Paddle/PaddleTrigger.node.ts | 164 +++++++ .../nodes/Paddle/PaymentDescription.ts | 197 ++++++++ .../nodes/Paddle/PlanDescription.ts | 52 ++ .../nodes/Paddle/ProductDescription.ts | 31 ++ .../nodes/Paddle/UserDescription.ts | 136 ++++++ packages/nodes-base/nodes/Paddle/paddle.png | Bin 0 -> 3076 bytes packages/nodes-base/package.json | 2 + 12 files changed, 1237 insertions(+) create mode 100644 packages/nodes-base/credentials/PaddleApi.credentials.ts create mode 100644 packages/nodes-base/nodes/Paddle/CouponDescription.ts create mode 100644 packages/nodes-base/nodes/Paddle/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Paddle/OrderDescription.ts create mode 100644 packages/nodes-base/nodes/Paddle/Paddle.node.ts create mode 100644 packages/nodes-base/nodes/Paddle/PaddleTrigger.node.ts create mode 100644 packages/nodes-base/nodes/Paddle/PaymentDescription.ts create mode 100644 packages/nodes-base/nodes/Paddle/PlanDescription.ts create mode 100644 packages/nodes-base/nodes/Paddle/ProductDescription.ts create mode 100644 packages/nodes-base/nodes/Paddle/UserDescription.ts create mode 100644 packages/nodes-base/nodes/Paddle/paddle.png diff --git a/packages/nodes-base/credentials/PaddleApi.credentials.ts b/packages/nodes-base/credentials/PaddleApi.credentials.ts new file mode 100644 index 000000000..143a24b3b --- /dev/null +++ b/packages/nodes-base/credentials/PaddleApi.credentials.ts @@ -0,0 +1,23 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class PaddleApi implements ICredentialType { + name = 'paddleApi'; + displayName = 'Paddle API'; + properties = [ + { + displayName: 'Vendor Auth Code', + name: 'vendorAuthCode', + type: 'string' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Vendor ID', + name: 'vendorId', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Paddle/CouponDescription.ts b/packages/nodes-base/nodes/Paddle/CouponDescription.ts new file mode 100644 index 000000000..063ae2eda --- /dev/null +++ b/packages/nodes-base/nodes/Paddle/CouponDescription.ts @@ -0,0 +1,447 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const couponOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'coupon', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a coupon.', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all coupons.', + }, + { + name: 'Update', + value: 'update', + description: 'Update a coupon.', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const couponFields = [ + +/* -------------------------------------------------------------------------- */ +/* coupon:create */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Coupon Type', + name: 'couponType', + type: 'options', + displayOptions: { + show: { + resource: [ + 'coupon', + ], + operation: [ + `create` + ] + }, + }, + default: '', + description: 'Either product (valid for specified products or subscription plans) or checkout (valid for any checkout).', + options: [ + { + name: 'Checkout', + value: 'checkout' + }, + { + name: 'Product', + value: 'product' + }, + ] + }, + { + displayName: 'Product ID(s)', + name: 'productIds', + type: 'string', + displayOptions: { + show: { + resource: [ + 'coupon', + ], + operation: [ + `create` + ], + couponType: [ + 'product', + ], + }, + }, + default: '', + description: 'Comma-separated list of product IDs. Required if coupon_type is product.', + required: true, + }, + { + displayName: 'Discount Type', + name: 'discountType', + type: 'options', + displayOptions: { + show: { + resource: [ + 'coupon', + ], + operation: [ + `create` + ], + }, + }, + default: 'flat', + description: 'Either flat or percentage.', + options: [ + { + name: 'Flat', + value: 'flat' + }, + { + name: 'Percentage', + value: 'percentage' + }, + ] + }, + { + displayName: 'Discount Amount Currency', + name: 'discountAmount', + type: 'number', + default: '', + description: 'Discount amount in currency.', + typeOptions: { + minValue: 0 + }, + displayOptions: { + show: { + resource: [ + 'coupon', + ], + operation: [ + `create` + ], + discountType: [ + 'flat', + ] + }, + }, + }, + { + displayName: 'Discount Amount %', + name: 'discountAmount', + type: 'number', + default: '', + description: 'Discount amount in percentage.', + typeOptions: { + minValue: 0, + maxValue: 100 + }, + displayOptions: { + show: { + resource: [ + 'coupon', + ], + operation: [ + `create` + ], + discountType: [ + 'percentage', + ] + }, + }, + }, + { + displayName: 'Currency', + name: 'currency', + type: 'options', + default: 'eur', + description: 'The currency must match the balance currency specified in your account.', + options: [ + { + name: 'EUR', + value: 'eur' + }, + { + name: 'GBP', + value: 'gbp' + }, + { + name: 'USD', + value: 'usd' + }, + ], + displayOptions: { + show: { + resource: [ + 'coupon', + ], + operation: [ + `create` + ], + discountType: [ + 'flat', + ] + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + resource: [ + 'coupon', + ], + operation: [ + 'create', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Allowed Uses', + name: 'allowedUses', + type: 'number', + default: 1, + description: 'Number of times a coupon can be used in a checkout. This will be set to 999,999 by default, if not specified.', + }, + { + displayName: 'Coupon Code', + name: 'couponCode', + type: 'string', + default: '', + description: 'Will be randomly generated if not specified.', + }, + { + displayName: 'Coupon Prefix', + name: 'couponPrefix', + type: 'string', + default: '', + description: 'Prefix for generated codes. Not valid if coupon_code is specified.', + }, + { + displayName: 'Expires', + name: 'expires', + type: 'DateTime', + default: '', + description: 'The coupon will expire on the date at 00:00:00 UTC.', + }, + { + displayName: 'Group', + name: 'group', + type: 'string', + typeOptions: { + minValue: 1, + maxValue: 50 + }, + default: '', + description: 'The name of the coupon group this coupon should be assigned to.', + }, + { + displayName: 'Recurring', + name: 'recurring', + type: 'boolean', + default: false, + description: 'If the coupon is used on subscription products, this indicates whether the discount should apply to recurring payments after the initial purchase.', + }, + { + displayName: 'Number of Coupons', + name: 'numberOfCoupons', + type: 'number', + default: 1, + description: 'Number of coupons to generate. Not valid if coupon_code is specified.', + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + description: 'Description of the coupon. This will be displayed in the Seller Dashboard.', + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* coupon:update */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Update by', + name: 'updateBy', + type: 'options', + displayOptions: { + show: { + resource: [ + 'coupon', + ], + operation: [ + `update` + ], + }, + }, + default: 'couponCode', + description: 'Either flat or percentage.', + options: [ + { + name: 'Coupon Code', + value: 'couponCode' + }, + { + name: 'Group', + value: 'group' + }, + ] + }, + { + displayName: 'Coupon Code', + name: 'couponCode', + type: 'string', + displayOptions: { + show: { + resource: [ + 'coupon', + ], + operation: [ + 'update' + ], + updateBy: [ + 'couponCode' + ] + }, + }, + default: '', + description: 'Identify the coupon to update', + }, + { + displayName: 'Group', + name: 'group', + type: 'string', + displayOptions: { + show: { + resource: [ + 'coupon', + ], + operation: [ + 'update' + ], + updateBy: [ + 'group' + ] + }, + }, + default: '', + description: 'The name of the group of coupons you want to update.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + resource: [ + 'coupon', + ], + operation: [ + 'update', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Allowed Uses', + name: 'allowedUses', + type: 'number', + default: 1, + description: 'Number of times a coupon can be used in a checkout. This will be set to 999,999 by default, if not specified.', + }, + { + displayName: 'Currency', + name: 'currency', + type: 'options', + default: 'eur', + description: 'The currency must match the balance currency specified in your account.', + options: [ + { + name: 'EUR', + value: 'eur' + }, + { + name: 'GBP', + value: 'gbp' + }, + { + name: 'USD', + value: 'usd' + }, + ], + }, + + { + displayName: 'Discount Amount', + name: 'discountAmount', + type: 'number', + default: '', + description: 'Discount amount.', + typeOptions: { + minValue: 0 + }, + }, + { + displayName: 'Expires', + name: 'expires', + type: 'DateTime', + default: '', + description: 'The coupon will expire on the date at 00:00:00 UTC.', + }, + { + displayName: 'New Coupon Code', + name: 'newCouponCode', + type: 'string', + default: '', + description: 'New code to rename the coupon to.', + }, + { + displayName: 'New Group Name', + name: 'newGroup', + type: 'string', + typeOptions: { + minValue: 1, + maxValue: 50 + }, + default: '', + description: 'New group name to move coupon to.', + }, + { + displayName: 'Product ID(s)', + name: 'productIds', + type: 'string', + default: '', + description: 'Comma-separated list of product IDs. Required if coupon_type is product.', + }, + { + displayName: 'Recurring', + name: 'recurring', + type: 'boolean', + default: false, + description: 'If the coupon is used on subscription products, this indicates whether the discount should apply to recurring payments after the initial purchase.', + }, + ], + }, + +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Paddle/GenericFunctions.ts b/packages/nodes-base/nodes/Paddle/GenericFunctions.ts new file mode 100644 index 000000000..62f054414 --- /dev/null +++ b/packages/nodes-base/nodes/Paddle/GenericFunctions.ts @@ -0,0 +1,41 @@ +import { OptionsWithUri } from 'request'; + +import { + IExecuteFunctions, + IHookFunctions, + ILoadOptionsFunctions, + IExecuteSingleFunctions, + IWebhookFunctions, + BINARY_ENCODING +} from 'n8n-core'; + +import { + IDataObject, +} from 'n8n-workflow'; + +export async function paddleApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IWebhookFunctions, endpoint: string, method: string, body: any = {}, query?: IDataObject, uri?: string): Promise { // tslint:disable-line:no-any + const credentials = this.getCredentials('paddleApi'); + + const options = { + method, + qs: query || {}, + uri: uri || `${env}/v1${endpoint}`, + body, + json: true + }; + + try { + return await this.helpers.request!(options); + } catch (error) { + + if (error.response.body) { + let errorMessage = error.response.body.message; + if (error.response.body.details) { + errorMessage += ` - Details: ${JSON.stringify(error.response.body.details)}`; + } + throw new Error(errorMessage); + } + + throw error; + } +} diff --git a/packages/nodes-base/nodes/Paddle/OrderDescription.ts b/packages/nodes-base/nodes/Paddle/OrderDescription.ts new file mode 100644 index 000000000..367082a4b --- /dev/null +++ b/packages/nodes-base/nodes/Paddle/OrderDescription.ts @@ -0,0 +1,52 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const orderOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'order', + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Get an order', + } + ], + default: 'get', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const orderFields = [ + +/* -------------------------------------------------------------------------- */ +/* order:get */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Checkout ID', + name: 'checkoutId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'order', + ], + operation: [ + 'get', + ], + }, + }, + description: 'The identifier of the buyer’s checkout.', + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Paddle/Paddle.node.ts b/packages/nodes-base/nodes/Paddle/Paddle.node.ts new file mode 100644 index 000000000..b40102dec --- /dev/null +++ b/packages/nodes-base/nodes/Paddle/Paddle.node.ts @@ -0,0 +1,92 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, + INodePropertyOptions, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +export class Paddle implements INodeType { + description: INodeTypeDescription = { + displayName: 'Paddle', + name: 'paddle', + icon: 'file:paddle.png', + group: ['output'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Paddle API', + defaults: { + name: 'Paddle', + color: '#45567c', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'paddleApi', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Coupon', + value: 'coupon', + + }, + { + name: 'Payments', + value: 'payments', + }, + { + name: 'Plan', + value: 'plan', + }, + { + name: 'Product', + value: 'product', + }, + { + name: 'Order', + value: 'order', + }, + { + name: 'User', + value: 'user', + }, + ], + default: 'coupon', + description: 'Resource to consume.', + }, + + ], + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + const length = items.length as unknown as number; + let responseData; + const qs: IDataObject = {}; + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + for (let i = 0; i < length; i++) { + + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData as IDataObject[]); + } else { + returnData.push(responseData as unknown as IDataObject); + } + } + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Paddle/PaddleTrigger.node.ts b/packages/nodes-base/nodes/Paddle/PaddleTrigger.node.ts new file mode 100644 index 000000000..a2b5c6e48 --- /dev/null +++ b/packages/nodes-base/nodes/Paddle/PaddleTrigger.node.ts @@ -0,0 +1,164 @@ +import { + IHookFunctions, + IWebhookFunctions, + } from 'n8n-core'; + + import { + IDataObject, + INodeTypeDescription, + INodeType, + IWebhookResponseData, + ILoadOptionsFunctions, + INodePropertyOptions, + } from 'n8n-workflow'; + + export class PaddleTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'Paddle Trigger', + name: 'paddleTrigger', + icon: 'file:paddle.png', + group: ['trigger'], + version: 1, + description: 'Handle Paddle events via webhooks', + defaults: { + name: 'Paddle Trigger', + color: '#32325d', + }, + inputs: [], + outputs: ['main'], + credentials: [ + { + name: 'paddleApi', + required: true, + } + ], + webhooks: [ + { + name: 'default', + httpMethod: 'POST', + reponseMode: 'onReceived', + path: 'webhook', + }, + ], + properties: [ + { + displayName: 'Events', + name: 'events', + type: 'multiOptions', + required: true, + default: [], + description: 'The event to listen to.', + typeOptions: { + loadOptionsMethod: 'getEvents' + }, + options: [], + }, + ], + }; + + + + // @ts-ignore (because of request) + webhookMethods = { + default: { + async checkExists(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + if (webhookData.webhookId === undefined) { + // No webhook id is set so no webhook can exist + return false; + } + const endpoint = `/notifications/webhooks/${webhookData.webhookId}`; + try { + await payPalApiRequest.call(this, endpoint, 'GET'); + } catch (err) { + if (err.response && err.response.name === 'INVALID_RESOURCE_ID') { + // Webhook does not exist + delete webhookData.webhookId; + return false; + } + throw new Error(`Paddle Error: ${err}`); + } + return true; + }, + + async create(this: IHookFunctions): Promise { + let webhook; + const webhookUrl = this.getNodeWebhookUrl('default'); + const events = this.getNodeParameter('events', []) as string[]; + const body = { + url: webhookUrl, + event_types: events.map(event => { + return { name: event }; + }), + }; + const endpoint = '/notifications/webhooks'; + try { + webhook = await payPalApiRequest.call(this, endpoint, 'POST', body); + } catch (e) { + throw e; + } + + if (webhook.id === undefined) { + return false; + } + const webhookData = this.getWorkflowStaticData('node'); + webhookData.webhookId = webhook.id as string; + return true; + }, + + async delete(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + if (webhookData.webhookId !== undefined) { + const endpoint = `/notifications/webhooks/${webhookData.webhookId}`; + try { + await payPalApiRequest.call(this, endpoint, 'DELETE', {}); + } catch (e) { + return false; + } + delete webhookData.webhookId; + } + return true; + }, + }, + }; + + async webhook(this: IWebhookFunctions): Promise { + let webhook; + const webhookData = this.getWorkflowStaticData('node') as IDataObject; + const bodyData = this.getBodyData() as IDataObject; + const req = this.getRequestObject(); + const headerData = this.getHeaderData() as IDataObject; + const endpoint = '/notifications/verify-webhook-signature'; + + if (headerData['PAYPAL-AUTH-ALGO'] !== undefined + && headerData['PAYPAL-CERT-URL'] !== undefined + && headerData['PAYPAL-TRANSMISSION-ID'] !== undefined + && headerData['PAYPAL-TRANSMISSION-SIG'] !== undefined + && headerData['PAYPAL-TRANSMISSION-TIME'] !== undefined) { + const body = { + auth_algo: headerData['PAYPAL-AUTH-ALGO'], + cert_url: headerData['PAYPAL-CERT-URL'], + transmission_id: headerData['PAYPAL-TRANSMISSION-ID'], + transmission_sig: headerData['PAYPAL-TRANSMISSION-SIG'], + transmission_time: headerData['PAYPAL-TRANSMISSION-TIME'], + webhook_id: webhookData.webhookId, + webhook_event: bodyData, + }; + try { + webhook = await payPalApiRequest.call(this, endpoint, 'POST', body); + } catch (e) { + throw e; + } + if (webhook.verification_status !== 'SUCCESS') { + return {}; + } + } else { + return {}; + } + return { + workflowData: [ + this.helpers.returnJsonArray(req.body) + ], + }; + } + } diff --git a/packages/nodes-base/nodes/Paddle/PaymentDescription.ts b/packages/nodes-base/nodes/Paddle/PaymentDescription.ts new file mode 100644 index 000000000..ef4faa55b --- /dev/null +++ b/packages/nodes-base/nodes/Paddle/PaymentDescription.ts @@ -0,0 +1,197 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const paymentOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'payment', + ], + }, + }, + options: [ + { + name: 'Get All', + value: 'getAll', + description: 'Get all payments.', + }, + { + name: 'Reschedule', + value: 'reschedule', + description: 'Reschedule payment.', + } + ], + default: 'getAll', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const paymentFields = [ + +/* -------------------------------------------------------------------------- */ +/* payment:getAll */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Subscription ID', + name: 'subscriptionId', + type: 'number', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'getAll', + ], + }, + }, + description: 'A specific user subscription ID.', + }, + { + displayName: 'Plan', + name: 'planId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'getAll', + ], + }, + }, + description: 'Filter: The product/plan ID (single or comma-separated values).', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 1, + required: true, + typeOptions: { + minValue: 1, + maxValue: 200 + }, + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'getAll', + ], + }, + }, + description: 'Number of subscription records to return per page.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'user', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'State', + name: 'state', + type: 'options', + default: 'active', + description: 'Filter: The user subscription status. Returns all active, past_due, trialing and paused subscription plans if not specified.', + options: [ + { + name: 'Active', + value: 'active' + }, + { + name: 'Past Due', + value: 'past_due' + }, + { + name: 'Paused', + value: 'paused' + }, + { + name: 'Trialing', + value: 'trialing' + }, + ] + }, + { + displayName: 'Is Paid', + name: 'isPaid', + type: 'boolean', + default: false, + description: 'Payment is paid.', + }, + { + displayName: 'From', + name: 'from', + type: 'DateTime', + default: '', + description: 'Payments starting from date.', + }, + { + displayName: 'To', + name: 'to', + type: 'DateTime', + default: '', + description: 'Payments up until date.', + }, + { + displayName: 'One off charge', + name: 'isOneOffCharge', + type: 'boolean', + default: false, + description: 'Payment is paid.', + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* payment:reschedule */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Payment ID', + name: 'paymentId', + type: 'number', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'getAll', + ], + }, + }, + description: 'The upcoming subscription payment ID.', // Use loadoptions to select payment + }, + { + displayName: 'Date', + name: 'date', + type: 'DateTime', + default: '', + description: 'Date you want to move the payment to.', + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Paddle/PlanDescription.ts b/packages/nodes-base/nodes/Paddle/PlanDescription.ts new file mode 100644 index 000000000..9ddf82046 --- /dev/null +++ b/packages/nodes-base/nodes/Paddle/PlanDescription.ts @@ -0,0 +1,52 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const planOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'plan', + ], + }, + }, + options: [ + { + name: 'Get All', + value: 'getAll', + description: 'Get all plans.', + } + ], + default: 'getAll', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const planFields = [ + +/* -------------------------------------------------------------------------- */ +/* plan:getAll */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Plan ID', + name: 'planId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'plan', + ], + operation: [ + 'getAll', + ], + }, + }, + description: 'Filter: The subscription plan ID.', + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Paddle/ProductDescription.ts b/packages/nodes-base/nodes/Paddle/ProductDescription.ts new file mode 100644 index 000000000..b2565b6dd --- /dev/null +++ b/packages/nodes-base/nodes/Paddle/ProductDescription.ts @@ -0,0 +1,31 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const productOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'product', + ], + }, + }, + options: [ + { + name: 'Get All', + value: 'getAll', + description: 'Get all products.', + } + ], + default: 'getAll', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const productFields = [ + +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Paddle/UserDescription.ts b/packages/nodes-base/nodes/Paddle/UserDescription.ts new file mode 100644 index 000000000..b970be875 --- /dev/null +++ b/packages/nodes-base/nodes/Paddle/UserDescription.ts @@ -0,0 +1,136 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const userOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'user', + ], + }, + }, + options: [ + { + name: 'Get All', + value: 'getAll', + description: 'Get all users', + } + ], + default: 'getAll', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const userFields = [ + +/* -------------------------------------------------------------------------- */ +/* user:getAll */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Subscription ID', + name: 'subscriptionId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'getAll', + ], + }, + }, + description: 'A specific user subscription ID.', + }, + { + displayName: 'Plan ID', + name: 'planId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'getAll', + ], + }, + }, + description: 'Filter: The subscription plan ID.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 1, + required: true, + typeOptions: { + minValue: 1, + maxValue: 200 + }, + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'getAll', + ], + }, + }, + description: 'Number of subscription records to return per page.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'user', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'State', + name: 'state', + type: 'options', + default: 'active', + description: 'Filter: The user subscription status. Returns all active, past_due, trialing and paused subscription plans if not specified.', + options: [ + { + name: 'Active', + value: 'active' + }, + { + name: 'Past Due', + value: 'past_due' + }, + { + name: 'Paused', + value: 'paused' + }, + { + name: 'Trialing', + value: 'trialing' + }, + ] + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Paddle/paddle.png b/packages/nodes-base/nodes/Paddle/paddle.png new file mode 100644 index 0000000000000000000000000000000000000000..80426ca92c3439e320c00be5eb12476c4957f7ab GIT binary patch literal 3076 zcmcImX;c$g77nW*q9CGx3o)P~kbQwf77>CVgf(guu&q=o5J^%Y6-feuqK%3S>YxpX z*eEEXqJoIpE^R<*pVEIt2Na~5xUbu|IiUgjx<#Y~-BlCsgL;*>1C^9L8 z3ns;bOaac@3+tg`p#&000bo@Uu@qscJaJ!mS?FGOOvGWoKos$wI3JxuERPe0^@ZgS zmQG;cK{Cx9>&7II88i}=;fke@$V?*1ok*tQNn{p@!J@liM}9cen_M7dMfmxT_(JbI zad8TTj720Wl}dt=N`U3DL^6|!a!`mA3LZt^5w%nSsPIz6VU)oSLO{7lrVznWtd0@j z!)q0uI8^Cy2ol*CtrQta6Phrh3Xl=W1d=YKQ6Pu&b*Mx#hDH?ZMAXZO-oGS9xM~?h zjDQe$tsI2diI7y`FzQSIj@ioA%EezI6@WxY3`tNJf(j##xst`f3K)rl|0a&H$79jR zSiW)yP{49742wq#6*kI(rJzw`otBHF0$7R6*C`q?fcyXj5kKqORu3zWlQ1xgVGAO)d7KTjMgiXajRSWJNFMt0)^ zcsj%c@gxC_j|WIX8lLLzEg>5 z1y2{aK`0T8iFc>c=y-r3gxo;}olX@(qimsa5&Gr;;y<(MR0&Wi_)QyMmT+?omH@-r}h(XiyVgsBfv2+{kI&I~92uPi)!i zSBFrR_(}E$|;-xlH(rd|DMIkT)V(ymB@S7-IIh39$A zk1{XsO%7{o+R>M`xX`=3!Sv^F$WSxh&N1Po-TaAXqmskaRTGU~q}H}xVQ&p)8GkNM z=`jzk-MC?j>%`Xd{hVT^2c^JJ1uSB970)|Rq`vLW#QnRljR9f zQ_~J<>gU-lqc-z5Mw*nxG(UT<-rG)FPb~`&p)*Cs#L%eu#$8n&_1OKh-}SX^KZ4Y}%YSCKo6F5ic1sMj_;6{{ zcQyRMqdvgE`JrsX18es(Eo=0}!o?=krZqhKd`?Rf=&(lWQv1o;IRoQTudFYs@anS= zKdDj7ef=~S)9ltZ2Wc%a0DAk(mmJaxaY@OHnrD=!mpp8(>bArZo{YEGZ%oZ?i+XT! z5$~g=Ua($_on}Vfo86!EuU0SE=C~5W+truz{lSd58@G4G9(QnY-*oH5s*31}hF-r$ zHzl_Bs`>z@TiaDwwkdJF%c|d8hKfTZkDN_0h|48j<}`e6&0PpT)l#)?Z9vjd;jfCPItX334aSpHPYttgMajg z80#9VQ%-f!S6Y%&_%i2+*+XTUmhbLq$hbAzJui9Hx{|iSsuHf!=h3xvo6zbdZG~#v zUbB|0srlGOUsQ+TQrXX?&8)#$HE`D~-YpYry{+TCY|0s;r-?tE`C;Qq+LOBGDot5J zZhDmLRno4207tzWo%MH1ZB^1cL>qAWhrE&rdX+r|RpFRu%Zl&fmOgH{0#9f6%GRYk zeZ490Ftf6(lrLvj0_v=vw07Lyks_V;=m+9U4iOXnqbV@RviAFqU(gU6lov1(C!zR=!`8FZStkNs-?;nG~# znH@&8Yk#->Si;*%vrudO=%BXdvbO6@+G_6Il2pZu&jz@=2a@);PNdkac3HA0F`P~3 z1>O2kAn&nIy{b+M2gpqX3)^d%TaE^=OVt#FBFAU!&DoJ%c%r@kjWV1a5wR+LZNdti z%QG(bSWIeT_dlM5DYktg73+68*QEqrJm;0}Jb!uH@kM1VH9*%zZQ8vUbfp zyUW^~_7jI@x6iBUTEe85U4Ah*P+xBwKh5xSk0Na4_7g+CXKqp{noXuduAv&2GcHHA zw`P}Ve#mPgM{C|usP$RlTk z7j-^=pJUKyIPZP(Vy%-QyQH}MUqwr);`+<(^G#F*zNECAnlSJ-;32eqct5-U@M-?(!&^QdR_#p7 znwjT-`1dh;F`eIU47HqC=t@sny?I}^b%toxblG!Zrjrq8a$a+cS5OC-dxxRdp@~ZF zjF)T3M}AU0on!wX*&pZbY)}j3Hog&S%THD|*q_@W&Z#WT3cS|yxGL)W!Hvo})(&P1 zJSJ6%(xBD0JaoyL@!)N0DYVSHc4%C= XufyuO^DY|czS9HQA$}(ot={+t`pL%m literal 0 HcmV?d00001 diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index c19d8e234..c22657642 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -104,6 +104,7 @@ "dist/credentials/OAuth1Api.credentials.js", "dist/credentials/OAuth2Api.credentials.js", "dist/credentials/OpenWeatherMapApi.credentials.js", + "dist/credentials/PaddleApi.credentials.js", "dist/credentials/PagerDutyApi.credentials.js", "dist/credentials/PagerDutyOAuth2Api.credentials.js", "dist/credentials/PayPalApi.credentials.js", @@ -246,6 +247,7 @@ "dist/nodes/NextCloud/NextCloud.node.js", "dist/nodes/NoOp.node.js", "dist/nodes/OpenWeatherMap.node.js", + "dist/nodes/Paddle/Paddle.node.js", "dist/nodes/PagerDuty/PagerDuty.node.js", "dist/nodes/PayPal/PayPal.node.js", "dist/nodes/PayPal/PayPalTrigger.node.js", From 4a968ee8ea14965908ec6e3c0859a75a112b260d Mon Sep 17 00:00:00 2001 From: Rupenieks <32895755+Rupenieks@users.noreply.github.com> Date: Tue, 30 Jun 2020 17:38:55 +0200 Subject: [PATCH 02/19] :construction: Node logic / Genericfunctions setup --- .../nodes/Paddle/CouponDescription.ts | 24 +- .../nodes/Paddle/GenericFunctions.ts | 24 +- .../nodes-base/nodes/Paddle/Paddle.node.ts | 226 +++++++++++++++++- .../nodes/Paddle/PaddleTrigger.node.ts | 9 +- .../nodes/Paddle/PaymentDescription.ts | 23 -- 5 files changed, 254 insertions(+), 52 deletions(-) diff --git a/packages/nodes-base/nodes/Paddle/CouponDescription.ts b/packages/nodes-base/nodes/Paddle/CouponDescription.ts index 063ae2eda..553a0c0ff 100644 --- a/packages/nodes-base/nodes/Paddle/CouponDescription.ts +++ b/packages/nodes-base/nodes/Paddle/CouponDescription.ts @@ -37,7 +37,6 @@ export const couponOperations = [ ] as INodeProperties[]; export const couponFields = [ - /* -------------------------------------------------------------------------- */ /* coupon:create */ /* -------------------------------------------------------------------------- */ @@ -55,7 +54,7 @@ export const couponFields = [ ] }, }, - default: '', + default: 'checkout', description: 'Either product (valid for specified products or subscription plans) or checkout (valid for any checkout).', options: [ { @@ -277,6 +276,26 @@ export const couponFields = [ ], }, /* -------------------------------------------------------------------------- */ +/* coupon:getAll */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Product ID', + name: 'productId', + type: 'number', + displayOptions: { + show: { + resource: [ + 'coupon', + ], + operation: [ + `getAll` + ] + }, + }, + default: '', + description: 'The specific product/subscription ID.', + }, +/* -------------------------------------------------------------------------- */ /* coupon:update */ /* -------------------------------------------------------------------------- */ { @@ -391,7 +410,6 @@ export const couponFields = [ }, ], }, - { displayName: 'Discount Amount', name: 'discountAmount', diff --git a/packages/nodes-base/nodes/Paddle/GenericFunctions.ts b/packages/nodes-base/nodes/Paddle/GenericFunctions.ts index 62f054414..c56498935 100644 --- a/packages/nodes-base/nodes/Paddle/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Paddle/GenericFunctions.ts @@ -16,26 +16,24 @@ import { export async function paddleApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IWebhookFunctions, endpoint: string, method: string, body: any = {}, query?: IDataObject, uri?: string): Promise { // tslint:disable-line:no-any const credentials = this.getCredentials('paddleApi'); - const options = { + if (credentials === undefined) { + throw new Error('Could not retrieve credentials!'); + } + + const options : OptionsWithUri = { method, - qs: query || {}, - uri: uri || `${env}/v1${endpoint}`, + uri: `https://vendors.paddle.com/api${endpoint}` , body, json: true }; + body.vendor_id = credentials.vendorId; + body.vendor_auth_code = credentials.vendorAuthCode; + try { return await this.helpers.request!(options); } catch (error) { - - if (error.response.body) { - let errorMessage = error.response.body.message; - if (error.response.body.details) { - errorMessage += ` - Details: ${JSON.stringify(error.response.body.details)}`; - } - throw new Error(errorMessage); - } - - throw error; + console.log(error); + throw new Error(error); } } diff --git a/packages/nodes-base/nodes/Paddle/Paddle.node.ts b/packages/nodes-base/nodes/Paddle/Paddle.node.ts index b40102dec..0a781e9bb 100644 --- a/packages/nodes-base/nodes/Paddle/Paddle.node.ts +++ b/packages/nodes-base/nodes/Paddle/Paddle.node.ts @@ -1,15 +1,21 @@ -import { - IExecuteFunctions, -} from 'n8n-core'; - +import { IExecuteFunctions } from 'n8n-core'; import { IDataObject, - ILoadOptionsFunctions, + INodeExecutionData, - INodePropertyOptions, + INodeType, - INodeTypeDescription, + INodeTypeDescription } from 'n8n-workflow'; +import { couponFields, couponOperations } from './CouponDescription'; +import { paddleApiRequest } from './GenericFunctions'; +import { paymentFields, paymentOperations } from './PaymentDescription'; +import { planFields, planOperations } from './PlanDescription'; +import { productFields, productOperations } from './ProductDescription'; +import { userFields, userOperations } from './UserDescription'; + +import moment = require('moment'); +import { response } from 'express'; export class Paddle implements INodeType { description: INodeTypeDescription = { @@ -41,7 +47,6 @@ export class Paddle implements INodeType { { name: 'Coupon', value: 'coupon', - }, { name: 'Payments', @@ -68,6 +73,22 @@ export class Paddle implements INodeType { description: 'Resource to consume.', }, + // COUPON + couponFields, + couponOperations, + // PAYMENT + paymentFields, + paymentOperations, + // PLAN + planFields, + planOperations, + // PRODUCT + productFields, + productOperations, + // USER + userFields, + userOperations + ], }; @@ -76,10 +97,197 @@ export class Paddle implements INodeType { const returnData: IDataObject[] = []; const length = items.length as unknown as number; let responseData; - const qs: IDataObject = {}; + const body: IDataObject = {}; const resource = this.getNodeParameter('resource', 0) as string; const operation = this.getNodeParameter('operation', 0) as string; for (let i = 0; i < length; i++) { + if (resource === 'coupon') { + if (operation === 'create') { + const productIds = this.getNodeParameter('productIds', i) as string; + const discountType = this.getNodeParameter('discountType', i) as string; + const discountAmount = this.getNodeParameter('discountAmount', i) as number; + const currency = this.getNodeParameter('currency', i) as string; + + body.product_ids = productIds; + body.discount_type = discountType; + body.discount_amount = discountAmount; + body.currency = currency; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (additionalFields.allowedUses) { + body.allowed_uses = additionalFields.allowedUses as number; + } + if (additionalFields.couponCode) { + body.coupon_code = additionalFields.couponCode as string; + } + if (additionalFields.couponPrefix) { + body.coupon_prefix = additionalFields.couponPrefix as string; + } + if (additionalFields.expires) { + body.expires = moment(additionalFields.expires as Date).format('YYYY/MM/DD') as string; + } + if (additionalFields.group) { + body.group = additionalFields.group as string; + } + if (additionalFields.recurring) { + if (additionalFields.recurring === true) { + body.recurring = 1; + } else { + body.recurring = 0; + } + } + if (additionalFields.numberOfCoupons) { + body.num_coupons = additionalFields.numberOfCoupons as number; + } + if (additionalFields.description) { + body.description = additionalFields.description as string; + } + + const endpoint = '/2.1/product/create_coupon'; + + responseData = paddleApiRequest.call(this, endpoint, 'POST', body); + } + + if (operation === 'getAll') { + const productIds = this.getNodeParameter('productId', i) as string; + const endpoint = '/2.0/product/list_coupons'; + + body.product_ids = productIds as string; + + responseData = paddleApiRequest.call(this, endpoint, 'POST', body); + } + + if (operation === 'update') { + const updateBy = this.getNodeParameter('updateBy', i) as string; + + if (updateBy === 'group') { + body.group = this.getNodeParameter('group', i) as string; + } else { + body.coupon_code = this.getNodeParameter('couponCode', i) as string; + } + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (additionalFields.allowedUses) { + body.allowed_uses = additionalFields.allowedUses as number; + } + if (additionalFields.currency) { + body.currency = additionalFields.currency as string; + } + if (additionalFields.newCouponCode) { + body.new_coupon_code = additionalFields.newCouponCode as string; + } + if (additionalFields.expires) { + body.expires = moment(additionalFields.expires as Date).format('YYYY/MM/DD') as string; + } + if (additionalFields.newGroup) { + body.new_group = additionalFields.newGroup as string; + } + if (additionalFields.recurring) { + if (additionalFields.recurring === true) { + body.recurring = 1; + } else { + body.recurring = 0; + } + } + if (additionalFields.productIds) { + body.product_ids = additionalFields.productIds as number; + } + if (additionalFields.discountAmount) { + body.discount_amount = additionalFields.discountAmount as number; + } + + const endpoint = '/2.1/product/update_coupon'; + + responseData = paddleApiRequest.call(this, endpoint, 'POST', body); + } + } + if (resource === 'payment') { + if (operation === 'getAll') { + const subscriptionId = this.getNodeParameter('subscription', i) as string; + const planId = this.getNodeParameter('planId', i) as string; + + body.subscription_id = subscriptionId; + body.plan_id = planId; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (additionalFields.state) { + body.state = additionalFields.state as string; + } + if (additionalFields.isPaid) { + if (additionalFields.isPaid === true) { + body.is_paid = 0; + } else { + body.is_paid = 1; + } + } + if (additionalFields.from) { + body.from = moment(additionalFields.from as Date).format('YYYY/MM/DD') as string; + } + if (additionalFields.to) { + body.to = moment(additionalFields.to as Date).format('YYYY/MM/DD') as string; + } + if (additionalFields.isOneOffCharge) { + body.is_one_off_charge = additionalFields.isOneOffCharge as boolean; + } + + const endpoint = '/2.0/subscription/payments'; + responseData = paddleApiRequest.call(this, endpoint, 'POST', body); + } + if (operation === 'reschedule') { + const paymentId = this.getNodeParameter('paymentId', i) as number; + const date = this.getNodeParameter('date', i) as Date; + + body.payment_id = paymentId; + body.date = body.to = moment(date as Date).format('YYYY/MM/DD') as string; + + const endpoint = '/2.0/subscription/payments_reschedule'; + + responseData = paddleApiRequest.call(this, endpoint, 'POST', body); + } + } + if (resource === 'plan') { + if (operation === 'getAll') { + const planId = this.getNodeParameter('planId', i) as string; + + body.plan = planId; + + const endpoint = '/2.0/subscription/plans'; + + responseData = paddleApiRequest.call(this, endpoint, 'POST', body); + } + } + if (resource === 'product') { + if (operation === 'getAll') { + const endpoint = '/2.0/product/get_products'; + + responseData = paddleApiRequest.call(this, endpoint, 'POST', body); + } + } + + if (resource === 'user') { + if (operation === 'getAll') { + const subscriptionId = this.getNodeParameter('subscriptionId', i) as string; + const planId = this.getNodeParameter('planId', i) as string; + const limit = this.getNodeParameter('limit', i) as number; + + body.subscription_id = subscriptionId; + body.plan_id = planId; + body.results_per_page = limit; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (additionalFields.state) { + body.state = additionalFields.state as string; + } + + const endpoint = '/2.0/subscription/users'; + + responseData = paddleApiRequest.call(this, endpoint, 'POST', body); + } + } if (Array.isArray(responseData)) { returnData.push.apply(returnData, responseData as IDataObject[]); diff --git a/packages/nodes-base/nodes/Paddle/PaddleTrigger.node.ts b/packages/nodes-base/nodes/Paddle/PaddleTrigger.node.ts index a2b5c6e48..f439a5beb 100644 --- a/packages/nodes-base/nodes/Paddle/PaddleTrigger.node.ts +++ b/packages/nodes-base/nodes/Paddle/PaddleTrigger.node.ts @@ -11,6 +11,7 @@ import { ILoadOptionsFunctions, INodePropertyOptions, } from 'n8n-workflow'; +import { paddleApiRequest } from './GenericFunctions'; export class PaddleTrigger implements INodeType { description: INodeTypeDescription = { @@ -69,7 +70,7 @@ import { } const endpoint = `/notifications/webhooks/${webhookData.webhookId}`; try { - await payPalApiRequest.call(this, endpoint, 'GET'); + await paddleApiRequest.call(this, endpoint, 'GET'); } catch (err) { if (err.response && err.response.name === 'INVALID_RESOURCE_ID') { // Webhook does not exist @@ -93,7 +94,7 @@ import { }; const endpoint = '/notifications/webhooks'; try { - webhook = await payPalApiRequest.call(this, endpoint, 'POST', body); + webhook = await paddleApiRequest.call(this, endpoint, 'POST', body); } catch (e) { throw e; } @@ -111,7 +112,7 @@ import { if (webhookData.webhookId !== undefined) { const endpoint = `/notifications/webhooks/${webhookData.webhookId}`; try { - await payPalApiRequest.call(this, endpoint, 'DELETE', {}); + await paddleApiRequest.call(this, endpoint, 'DELETE', {}); } catch (e) { return false; } @@ -145,7 +146,7 @@ import { webhook_event: bodyData, }; try { - webhook = await payPalApiRequest.call(this, endpoint, 'POST', body); + webhook = await paddleApiRequest.call(this, endpoint, 'POST', body); } catch (e) { throw e; } diff --git a/packages/nodes-base/nodes/Paddle/PaymentDescription.ts b/packages/nodes-base/nodes/Paddle/PaymentDescription.ts index ef4faa55b..77ff5ca43 100644 --- a/packages/nodes-base/nodes/Paddle/PaymentDescription.ts +++ b/packages/nodes-base/nodes/Paddle/PaymentDescription.ts @@ -32,7 +32,6 @@ export const paymentOperations = [ ] as INodeProperties[]; export const paymentFields = [ - /* -------------------------------------------------------------------------- */ /* payment:getAll */ /* -------------------------------------------------------------------------- */ @@ -72,28 +71,6 @@ export const paymentFields = [ }, description: 'Filter: The product/plan ID (single or comma-separated values).', }, - { - displayName: 'Limit', - name: 'limit', - type: 'number', - default: 1, - required: true, - typeOptions: { - minValue: 1, - maxValue: 200 - }, - displayOptions: { - show: { - resource: [ - 'user', - ], - operation: [ - 'getAll', - ], - }, - }, - description: 'Number of subscription records to return per page.', - }, { displayName: 'Additional Fields', name: 'additionalFields', From 0b46f5d63a8a0052ce733fa82a9186392b4fa557 Mon Sep 17 00:00:00 2001 From: Rupenieks <32895755+Rupenieks@users.noreply.github.com> Date: Thu, 2 Jul 2020 11:12:27 +0200 Subject: [PATCH 03/19] :construction: Tests / changes --- .../nodes/Paddle/CouponDescription.ts | 143 +++++++- .../nodes/Paddle/GenericFunctions.ts | 29 +- .../nodes-base/nodes/Paddle/Paddle.node.ts | 328 +++++++++++------- .../nodes/Paddle/PaymentDescription.ts | 130 ++++--- .../nodes/Paddle/PlanDescription.ts | 9 +- .../nodes/Paddle/UserDescription.ts | 96 +++-- 6 files changed, 514 insertions(+), 221 deletions(-) diff --git a/packages/nodes-base/nodes/Paddle/CouponDescription.ts b/packages/nodes-base/nodes/Paddle/CouponDescription.ts index 553a0c0ff..64e87cf9a 100644 --- a/packages/nodes-base/nodes/Paddle/CouponDescription.ts +++ b/packages/nodes-base/nodes/Paddle/CouponDescription.ts @@ -51,6 +51,9 @@ export const couponFields = [ ], operation: [ `create` + ], + jsonParameters: [ + false ] }, }, @@ -82,6 +85,9 @@ export const couponFields = [ couponType: [ 'product', ], + jsonParameters: [ + false + ] }, }, default: '', @@ -100,6 +106,9 @@ export const couponFields = [ operation: [ `create` ], + jsonParameters: [ + false + ] }, }, default: 'flat', @@ -122,7 +131,7 @@ export const couponFields = [ default: '', description: 'Discount amount in currency.', typeOptions: { - minValue: 0 + minValue: 1 }, displayOptions: { show: { @@ -134,6 +143,9 @@ export const couponFields = [ ], discountType: [ 'flat', + ], + jsonParameters: [ + false ] }, }, @@ -145,7 +157,7 @@ export const couponFields = [ default: '', description: 'Discount amount in percentage.', typeOptions: { - minValue: 0, + minValue: 1, maxValue: 100 }, displayOptions: { @@ -158,6 +170,9 @@ export const couponFields = [ ], discountType: [ 'percentage', + ], + jsonParameters: [ + false ] }, }, @@ -166,20 +181,20 @@ export const couponFields = [ displayName: 'Currency', name: 'currency', type: 'options', - default: 'eur', + default: 'EUR', description: 'The currency must match the balance currency specified in your account.', options: [ { name: 'EUR', - value: 'eur' + value: 'EUR' }, { name: 'GBP', - value: 'gbp' + value: 'GBP' }, { name: 'USD', - value: 'usd' + value: 'USD' }, ], displayOptions: { @@ -192,10 +207,53 @@ export const couponFields = [ ], discountType: [ 'flat', + ], + jsonParameters: [ + false ] }, }, }, + { + displayName: 'JSON Parameters', + name: 'jsonParameters', + type: 'boolean', + default: false, + description: '', + displayOptions: { + show: { + resource: [ + 'coupon', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: ' Additional Fields', + name: 'additionalFieldsJson', + type: 'json', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + displayOptions: { + show: { + resource: [ + 'coupon', + ], + operation: [ + 'create', + ], + jsonParameters: [ + true, + ], + }, + }, + description: `Attributes in JSON form.`, + }, { displayName: 'Additional Fields', name: 'additionalFields', @@ -209,6 +267,9 @@ export const couponFields = [ operation: [ 'create', ], + jsonParameters: [ + false + ] }, }, default: {}, @@ -237,7 +298,7 @@ export const couponFields = [ { displayName: 'Expires', name: 'expires', - type: 'DateTime', + type: 'dateTime', default: '', description: 'The coupon will expire on the date at 00:00:00 UTC.', }, @@ -310,6 +371,9 @@ export const couponFields = [ operation: [ `update` ], + jsonParameters: [ + false, + ], }, }, default: 'couponCode', @@ -339,7 +403,10 @@ export const couponFields = [ ], updateBy: [ 'couponCode' - ] + ], + jsonParameters: [ + false, + ], }, }, default: '', @@ -359,12 +426,55 @@ export const couponFields = [ ], updateBy: [ 'group' - ] + ], + jsonParameters: [ + false, + ], }, }, default: '', description: 'The name of the group of coupons you want to update.', }, + { + displayName: 'JSON Parameters', + name: 'jsonParameters', + type: 'boolean', + default: false, + description: '', + displayOptions: { + show: { + resource: [ + 'coupon', + ], + operation: [ + 'update', + ], + }, + }, + }, + { + displayName: ' Additional Fields', + name: 'additionalFieldsJson', + type: 'json', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + displayOptions: { + show: { + resource: [ + 'coupon', + ], + operation: [ + 'update', + ], + jsonParameters: [ + true, + ], + }, + }, + description: `Attributes in JSON form.`, + }, { displayName: 'Additional Fields', name: 'additionalFields', @@ -378,6 +488,9 @@ export const couponFields = [ operation: [ 'update', ], + jsonParameters: [ + false + ] }, }, default: {}, @@ -393,20 +506,20 @@ export const couponFields = [ displayName: 'Currency', name: 'currency', type: 'options', - default: 'eur', + default: 'EUR', description: 'The currency must match the balance currency specified in your account.', options: [ { name: 'EUR', - value: 'eur' + value: 'EUR' }, { name: 'GBP', - value: 'gbp' + value: 'GBP' }, { name: 'USD', - value: 'usd' + value: 'USD' }, ], }, @@ -423,7 +536,7 @@ export const couponFields = [ { displayName: 'Expires', name: 'expires', - type: 'DateTime', + type: 'dateTime', default: '', description: 'The coupon will expire on the date at 00:00:00 UTC.', }, @@ -450,7 +563,7 @@ export const couponFields = [ name: 'productIds', type: 'string', default: '', - description: 'Comma-separated list of product IDs. Required if coupon_type is product.', + description: 'Comma-separated list of products e.g. 499531,1234,123546. If blank then remove associated products.', }, { displayName: 'Recurring', diff --git a/packages/nodes-base/nodes/Paddle/GenericFunctions.ts b/packages/nodes-base/nodes/Paddle/GenericFunctions.ts index c56498935..8aa131884 100644 --- a/packages/nodes-base/nodes/Paddle/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Paddle/GenericFunctions.ts @@ -22,18 +22,41 @@ export async function paddleApiRequest(this: IHookFunctions | IExecuteFunctions const options : OptionsWithUri = { method, + headers: { + 'content-type': 'application/json' + }, uri: `https://vendors.paddle.com/api${endpoint}` , body, json: true }; - body.vendor_id = credentials.vendorId; - body.vendor_auth_code = credentials.vendorAuthCode; + body['vendor_id'] = credentials.vendorId; + body['vendor_auth_code'] = credentials.vendorAuthCode; + + console.log(options.body); + console.log(options); try { - return await this.helpers.request!(options); + const response = await this.helpers.request!(options); + console.log(response); + + if (!response.success) { + throw new Error(`Code: ${response.error.code}. Message: ${response.error.message}`); + } + + return response.response; } catch (error) { console.log(error); throw new Error(error); } } + +export function validateJSON(json: string | undefined): any { // tslint:disable-line:no-any + let result; + try { + result = JSON.parse(json!); + } catch (exception) { + result = undefined; + } + return result; +} diff --git a/packages/nodes-base/nodes/Paddle/Paddle.node.ts b/packages/nodes-base/nodes/Paddle/Paddle.node.ts index 0a781e9bb..e5478aa11 100644 --- a/packages/nodes-base/nodes/Paddle/Paddle.node.ts +++ b/packages/nodes-base/nodes/Paddle/Paddle.node.ts @@ -1,21 +1,20 @@ import { IExecuteFunctions } from 'n8n-core'; import { IDataObject, - INodeExecutionData, - INodeType, INodeTypeDescription } from 'n8n-workflow'; + import { couponFields, couponOperations } from './CouponDescription'; -import { paddleApiRequest } from './GenericFunctions'; -import { paymentFields, paymentOperations } from './PaymentDescription'; +import { paddleApiRequest, validateJSON } from './GenericFunctions'; +import { paymentsFields, paymentsOperations } from './PaymentDescription'; import { planFields, planOperations } from './PlanDescription'; import { productFields, productOperations } from './ProductDescription'; import { userFields, userOperations } from './UserDescription'; import moment = require('moment'); -import { response } from 'express'; +import { orderOperations, orderFields } from './OrderDescription'; export class Paddle implements INodeType { description: INodeTypeDescription = { @@ -72,23 +71,24 @@ export class Paddle implements INodeType { default: 'coupon', description: 'Resource to consume.', }, - // COUPON - couponFields, - couponOperations, + ...couponOperations, + ...couponFields, // PAYMENT - paymentFields, - paymentOperations, + ...paymentsOperations, + ...paymentsFields, // PLAN - planFields, - planOperations, + ...planOperations, + ...planFields, // PRODUCT - productFields, - productOperations, + ...productOperations, + ...productFields, + // ORDER + ...orderOperations, + ...orderFields, // USER - userFields, - userOperations - + ...userOperations, + ...userFields ], }; @@ -103,192 +103,274 @@ export class Paddle implements INodeType { for (let i = 0; i < length; i++) { if (resource === 'coupon') { if (operation === 'create') { - const productIds = this.getNodeParameter('productIds', i) as string; - const discountType = this.getNodeParameter('discountType', i) as string; - const discountAmount = this.getNodeParameter('discountAmount', i) as number; - const currency = this.getNodeParameter('currency', i) as string; + const jsonParameters = this.getNodeParameter('jsonParameters', i) as boolean; - body.product_ids = productIds; - body.discount_type = discountType; - body.discount_amount = discountAmount; - body.currency = currency; + if (jsonParameters) { + const additionalFieldsJson = this.getNodeParameter('additionalFieldsJson', i) as string; - const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + if (additionalFieldsJson !== '') { + if (validateJSON(additionalFieldsJson) !== undefined) { + Object.assign(body, JSON.parse(additionalFieldsJson)); + } else { + throw new Error('Additional fields must be a valid JSON'); + } + } - if (additionalFields.allowedUses) { - body.allowed_uses = additionalFields.allowedUses as number; - } - if (additionalFields.couponCode) { - body.coupon_code = additionalFields.couponCode as string; - } - if (additionalFields.couponPrefix) { - body.coupon_prefix = additionalFields.couponPrefix as string; - } - if (additionalFields.expires) { - body.expires = moment(additionalFields.expires as Date).format('YYYY/MM/DD') as string; - } - if (additionalFields.group) { - body.group = additionalFields.group as string; - } - if (additionalFields.recurring) { - if (additionalFields.recurring === true) { + } else { + + const discountType = this.getNodeParameter('discountType', i) as string; + const couponType = this.getNodeParameter('couponType', i) as string; + const discountAmount = this.getNodeParameter('discountAmount', i) as number; + const currency = this.getNodeParameter('currency', i) as string; + + if (couponType === 'product') { + body.product_ids = this.getNodeParameter('productIds', i) as string; + } + + body.coupon_type = couponType; + body.discount_type = discountType; + body.discount_amount = discountAmount; + body.currency = currency; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (additionalFields.allowedUses) { + body.allowed_uses = additionalFields.allowedUses as number; + } + if (additionalFields.couponCode) { + body.coupon_code = additionalFields.couponCode as string; + } + if (additionalFields.couponPrefix) { + body.coupon_prefix = additionalFields.couponPrefix as string; + } + if (additionalFields.expires) { + body.expires = moment(additionalFields.expires as Date).format('YYYY-MM-DD') as string; + } + if (additionalFields.group) { + body.group = additionalFields.group as string; + } + if (additionalFields.recurring) { body.recurring = 1; } else { body.recurring = 0; } - } - if (additionalFields.numberOfCoupons) { - body.num_coupons = additionalFields.numberOfCoupons as number; - } - if (additionalFields.description) { - body.description = additionalFields.description as string; - } + if (additionalFields.numberOfCoupons) { + body.num_coupons = additionalFields.numberOfCoupons as number; + } + if (additionalFields.description) { + body.description = additionalFields.description as string; + } - const endpoint = '/2.1/product/create_coupon'; + const endpoint = '/2.1/product/create_coupon'; - responseData = paddleApiRequest.call(this, endpoint, 'POST', body); + responseData = await paddleApiRequest.call(this, endpoint, 'POST', body); + } } if (operation === 'getAll') { const productIds = this.getNodeParameter('productId', i) as string; const endpoint = '/2.0/product/list_coupons'; - body.product_ids = productIds as string; + body.product_id = productIds as string; - responseData = paddleApiRequest.call(this, endpoint, 'POST', body); + responseData = await paddleApiRequest.call(this, endpoint, 'POST', body); } if (operation === 'update') { - const updateBy = this.getNodeParameter('updateBy', i) as string; - if (updateBy === 'group') { - body.group = this.getNodeParameter('group', i) as string; + const jsonParameters = this.getNodeParameter('jsonParameters', i) as boolean; + + if (jsonParameters) { + const additionalFieldsJson = this.getNodeParameter('additionalFieldsJson', i) as string; + + if (additionalFieldsJson !== '') { + if (validateJSON(additionalFieldsJson) !== undefined) { + Object.assign(body, JSON.parse(additionalFieldsJson)); + } else { + throw new Error('Additional fields must be a valid JSON'); + } + } + } else { - body.coupon_code = this.getNodeParameter('couponCode', i) as string; - } + const updateBy = this.getNodeParameter('updateBy', i) as string; - const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + if (updateBy === 'group') { + body.group = this.getNodeParameter('group', i) as string; + } else { + body.coupon_code = this.getNodeParameter('couponCode', i) as string; + } - if (additionalFields.allowedUses) { - body.allowed_uses = additionalFields.allowedUses as number; - } - if (additionalFields.currency) { - body.currency = additionalFields.currency as string; - } - if (additionalFields.newCouponCode) { - body.new_coupon_code = additionalFields.newCouponCode as string; - } - if (additionalFields.expires) { - body.expires = moment(additionalFields.expires as Date).format('YYYY/MM/DD') as string; - } - if (additionalFields.newGroup) { - body.new_group = additionalFields.newGroup as string; - } - if (additionalFields.recurring) { - if (additionalFields.recurring === true) { + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (additionalFields.allowedUses) { + body.allowed_uses = additionalFields.allowedUses as number; + } + if (additionalFields.currency) { + body.currency = additionalFields.currency as string; + } + if (additionalFields.newCouponCode) { + body.new_coupon_code = additionalFields.newCouponCode as string; + } + if (additionalFields.expires) { + body.expires = moment(additionalFields.expires as Date).format('YYYY-MM-DD') as string; + } + if (additionalFields.newGroup) { + body.new_group = additionalFields.newGroup as string; + } + if (additionalFields.recurring) { body.recurring = 1; } else { body.recurring = 0; } - } - if (additionalFields.productIds) { - body.product_ids = additionalFields.productIds as number; - } - if (additionalFields.discountAmount) { - body.discount_amount = additionalFields.discountAmount as number; + if (additionalFields.productIds) { + body.product_ids = additionalFields.productIds as number; + } + if (additionalFields.discountAmount) { + body.discount_amount = additionalFields.discountAmount as number; + } } const endpoint = '/2.1/product/update_coupon'; - responseData = paddleApiRequest.call(this, endpoint, 'POST', body); + responseData = await paddleApiRequest.call(this, endpoint, 'POST', body); } } if (resource === 'payment') { if (operation === 'getAll') { - const subscriptionId = this.getNodeParameter('subscription', i) as string; - const planId = this.getNodeParameter('planId', i) as string; + const jsonParameters = this.getNodeParameter('jsonParameters', i) as boolean; - body.subscription_id = subscriptionId; - body.plan_id = planId; + if (jsonParameters) { + const additionalFieldsJson = this.getNodeParameter('additionalFieldsJson', i) as string; - const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; - - if (additionalFields.state) { - body.state = additionalFields.state as string; - } - if (additionalFields.isPaid) { - if (additionalFields.isPaid === true) { - body.is_paid = 0; - } else { - body.is_paid = 1; + if (additionalFieldsJson !== '') { + if (validateJSON(additionalFieldsJson) !== undefined) { + Object.assign(body, JSON.parse(additionalFieldsJson)); + } else { + throw new Error('Additional fields must be a valid JSON'); + } + } + + } else { + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (additionalFields.subscriptionId) { + body.subscription_id = additionalFields.subscriptionId as number; + } + if (additionalFields.plan) { + body.plan = additionalFields.plan as string; + } + if (additionalFields.state) { + body.state = additionalFields.state as string; + } + if (additionalFields.recurring) { + body.recurring = 1; + } else { + body.recurring = 0; + } + if (additionalFields.from) { + body.from = moment(additionalFields.from as Date).format('YYYY-MM-DD') as string; + } + if (additionalFields.to) { + body.to = moment(additionalFields.to as Date).format('YYYY-MM-DD') as string; + } + if (additionalFields.isOneOffCharge) { + body.is_one_off_charge = additionalFields.isOneOffCharge as boolean; } - } - if (additionalFields.from) { - body.from = moment(additionalFields.from as Date).format('YYYY/MM/DD') as string; - } - if (additionalFields.to) { - body.to = moment(additionalFields.to as Date).format('YYYY/MM/DD') as string; - } - if (additionalFields.isOneOffCharge) { - body.is_one_off_charge = additionalFields.isOneOffCharge as boolean; } const endpoint = '/2.0/subscription/payments'; - responseData = paddleApiRequest.call(this, endpoint, 'POST', body); + + responseData = await paddleApiRequest.call(this, endpoint, 'POST', body); } if (operation === 'reschedule') { const paymentId = this.getNodeParameter('paymentId', i) as number; const date = this.getNodeParameter('date', i) as Date; body.payment_id = paymentId; - body.date = body.to = moment(date as Date).format('YYYY/MM/DD') as string; + body.date = body.to = moment(date as Date).format('YYYY-MM-DD') as string; const endpoint = '/2.0/subscription/payments_reschedule'; - responseData = paddleApiRequest.call(this, endpoint, 'POST', body); + responseData = await paddleApiRequest.call(this, endpoint, 'POST', body); } } if (resource === 'plan') { if (operation === 'getAll') { + const endpoint = '/2.0/subscription/plans'; + + responseData = await paddleApiRequest.call(this, endpoint, 'POST', body); + } + if (operation === 'get') { const planId = this.getNodeParameter('planId', i) as string; body.plan = planId; const endpoint = '/2.0/subscription/plans'; - responseData = paddleApiRequest.call(this, endpoint, 'POST', body); + responseData = await paddleApiRequest.call(this, endpoint, 'POST', body); } } if (resource === 'product') { if (operation === 'getAll') { const endpoint = '/2.0/product/get_products'; - responseData = paddleApiRequest.call(this, endpoint, 'POST', body); + responseData = await paddleApiRequest.call(this, endpoint, 'POST', body); } } + if (resource === 'order') { + if (operation === 'get') { + const endpoint = '/1.0/order'; + const checkoutId = this.getNodeParameter('checkoutId', i) as string; + body.checkout_id = checkoutId; + + responseData = await paddleApiRequest.call(this, endpoint, 'GET', body); + } + } if (resource === 'user') { if (operation === 'getAll') { - const subscriptionId = this.getNodeParameter('subscriptionId', i) as string; - const planId = this.getNodeParameter('planId', i) as string; - const limit = this.getNodeParameter('limit', i) as number; - body.subscription_id = subscriptionId; - body.plan_id = planId; - body.results_per_page = limit; + const jsonParameters = this.getNodeParameter('jsonParameters', i) as boolean; - const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + if (jsonParameters) { + const additionalFieldsJson = this.getNodeParameter('additionalFieldsJson', i) as string; + + if (additionalFieldsJson !== '') { + if (validateJSON(additionalFieldsJson) !== undefined) { + Object.assign(body, JSON.parse(additionalFieldsJson)); + } else { + throw new Error('Additional fields must be a valid JSON'); + } + } + + } else { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + if (!returnAll) { + body.results_per_page = this.getNodeParameter('limit', i) as number; + } + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (additionalFields.state) { + body.state = additionalFields.state as string; + } + if (additionalFields.planId) { + body.plan_id = additionalFields.planId as string; + } + if (additionalFields.subscriptionId) { + body.subscription_id = additionalFields.subscriptionId as string; + } - if (additionalFields.state) { - body.state = additionalFields.state as string; } - const endpoint = '/2.0/subscription/users'; - responseData = paddleApiRequest.call(this, endpoint, 'POST', body); + responseData = await paddleApiRequest.call(this, endpoint, 'POST', body); } } + console.log(responseData); + if (Array.isArray(responseData)) { returnData.push.apply(returnData, responseData as IDataObject[]); } else { diff --git a/packages/nodes-base/nodes/Paddle/PaymentDescription.ts b/packages/nodes-base/nodes/Paddle/PaymentDescription.ts index 77ff5ca43..45d355385 100644 --- a/packages/nodes-base/nodes/Paddle/PaymentDescription.ts +++ b/packages/nodes-base/nodes/Paddle/PaymentDescription.ts @@ -2,7 +2,7 @@ import { INodeProperties, } from 'n8n-workflow'; -export const paymentOperations = [ +export const paymentsOperations = [ { displayName: 'Operation', name: 'operation', @@ -10,7 +10,7 @@ export const paymentOperations = [ displayOptions: { show: { resource: [ - 'payment', + 'payments', ], }, }, @@ -31,45 +31,49 @@ export const paymentOperations = [ }, ] as INodeProperties[]; -export const paymentFields = [ +export const paymentsFields = [ /* -------------------------------------------------------------------------- */ -/* payment:getAll */ +/* payments:getAll */ /* -------------------------------------------------------------------------- */ { - displayName: 'Subscription ID', - name: 'subscriptionId', - type: 'number', - default: '', - required: true, + displayName: 'JSON Parameters', + name: 'jsonParameters', + type: 'boolean', + default: false, + description: '', displayOptions: { show: { resource: [ - 'user', + 'payments', ], operation: [ 'getAll', ], }, }, - description: 'A specific user subscription ID.', }, { - displayName: 'Plan', - name: 'planId', - type: 'string', + displayName: ' Additional Fields', + name: 'additionalFieldsJson', + type: 'json', + typeOptions: { + alwaysOpenEditWindow: true, + }, default: '', - required: true, displayOptions: { show: { resource: [ - 'user', + 'payments', ], operation: [ 'getAll', ], + jsonParameters: [ + true, + ], }, }, - description: 'Filter: The product/plan ID (single or comma-separated values).', + description: `Attributes in JSON form.`, }, { displayName: 'Additional Fields', @@ -78,16 +82,54 @@ export const paymentFields = [ placeholder: 'Add Field', displayOptions: { show: { + resource: [ + 'payments', + ], operation: [ 'getAll', ], - resource: [ - 'user', - ], + jsonParameters: [ + false + ] }, }, default: {}, options: [ + { + displayName: 'Date From', + name: 'from', + type: 'dateTime', + default: '', + description: 'payments starting from date.', + }, + { + displayName: 'Date To', + name: 'to', + type: 'dateTime', + default: '', + description: 'payments up until date.', + }, + { + displayName: 'Is Paid', + name: 'isPaid', + type: 'boolean', + default: false, + description: 'payment is paid.', + }, + { + displayName: 'Plan', + name: 'plan', + type: 'string', + default: '', + description: 'Filter: The product/plan ID (single or comma-separated values).', + }, + { + displayName: 'Subscription ID', + name: 'subscriptionId', + type: 'number', + default: '', + description: 'A specific user subscription ID.', + }, { displayName: 'State', name: 'state', @@ -113,62 +155,50 @@ export const paymentFields = [ }, ] }, - { - displayName: 'Is Paid', - name: 'isPaid', - type: 'boolean', - default: false, - description: 'Payment is paid.', - }, - { - displayName: 'From', - name: 'from', - type: 'DateTime', - default: '', - description: 'Payments starting from date.', - }, - { - displayName: 'To', - name: 'to', - type: 'DateTime', - default: '', - description: 'Payments up until date.', - }, { displayName: 'One off charge', name: 'isOneOffCharge', type: 'boolean', default: false, - description: 'Payment is paid.', }, ], }, /* -------------------------------------------------------------------------- */ -/* payment:reschedule */ +/* payments:reschedule */ /* -------------------------------------------------------------------------- */ { - displayName: 'Payment ID', - name: 'paymentId', + displayName: 'payments ID', + name: 'paymentsId', type: 'number', default: '', required: true, displayOptions: { show: { resource: [ - 'user', + 'payments', ], operation: [ - 'getAll', + 'reschedule', ], }, }, - description: 'The upcoming subscription payment ID.', // Use loadoptions to select payment + description: 'The upcoming subscription payments ID.', // Use loadoptions to select payments }, { displayName: 'Date', name: 'date', - type: 'DateTime', + type: 'dateTime', default: '', - description: 'Date you want to move the payment to.', + displayOptions: { + show: { + resource: [ + 'payments', + ], + operation: [ + 'reschedule', + ], + }, + }, + description: 'Date you want to move the payments to.', }, ] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Paddle/PlanDescription.ts b/packages/nodes-base/nodes/Paddle/PlanDescription.ts index 9ddf82046..f6887ca64 100644 --- a/packages/nodes-base/nodes/Paddle/PlanDescription.ts +++ b/packages/nodes-base/nodes/Paddle/PlanDescription.ts @@ -15,6 +15,11 @@ export const planOperations = [ }, }, options: [ + { + name: 'Get', + value: 'get', + description: 'Get a plan.', + }, { name: 'Get All', value: 'getAll', @@ -29,7 +34,7 @@ export const planOperations = [ export const planFields = [ /* -------------------------------------------------------------------------- */ -/* plan:getAll */ +/* plan:get */ /* -------------------------------------------------------------------------- */ { displayName: 'Plan ID', @@ -43,7 +48,7 @@ export const planFields = [ 'plan', ], operation: [ - 'getAll', + 'get', ], }, }, diff --git a/packages/nodes-base/nodes/Paddle/UserDescription.ts b/packages/nodes-base/nodes/Paddle/UserDescription.ts index b970be875..d45cc2ddb 100644 --- a/packages/nodes-base/nodes/Paddle/UserDescription.ts +++ b/packages/nodes-base/nodes/Paddle/UserDescription.ts @@ -27,16 +27,13 @@ export const userOperations = [ ] as INodeProperties[]; export const userFields = [ - /* -------------------------------------------------------------------------- */ /* user:getAll */ /* -------------------------------------------------------------------------- */ { - displayName: 'Subscription ID', - name: 'subscriptionId', - type: 'string', - default: '', - required: true, + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', displayOptions: { show: { resource: [ @@ -47,25 +44,8 @@ export const userFields = [ ], }, }, - description: 'A specific user subscription ID.', - }, - { - displayName: 'Plan ID', - name: 'planId', - type: 'string', - default: '', - required: true, - displayOptions: { - show: { - resource: [ - 'user', - ], - operation: [ - 'getAll', - ], - }, - }, - description: 'Filter: The subscription plan ID.', + default: false, + description: 'If all results should be returned or only up to a given limit.', }, { displayName: 'Limit', @@ -85,10 +65,53 @@ export const userFields = [ operation: [ 'getAll', ], + returnAll: [ + false + ] }, }, description: 'Number of subscription records to return per page.', }, + { + displayName: 'JSON Parameters', + name: 'jsonParameters', + type: 'boolean', + default: false, + description: '', + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: ' Additional Fields', + name: 'additionalFieldsJson', + type: 'json', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'getAll', + ], + jsonParameters: [ + true, + ], + }, + }, + description: `Attributes in JSON form.`, + }, { displayName: 'Additional Fields', name: 'additionalFields', @@ -96,16 +119,33 @@ export const userFields = [ placeholder: 'Add Field', displayOptions: { show: { - operation: [ - 'getAll', - ], resource: [ 'user', ], + operation: [ + 'getAll', + ], + jsonParameters: [ + false + ] }, }, default: {}, options: [ + { + displayName: 'Plan ID', + name: 'planId', + type: 'string', + default: '', + description: 'Filter: The subscription plan ID.', + }, + { + displayName: 'Subscription ID', + name: 'subscriptionId', + type: 'string', + default: '', + description: 'A specific user subscription ID.', + }, { displayName: 'State', name: 'state', From f4022c6cd543886ba150ae3da554d973cb6aaa70 Mon Sep 17 00:00:00 2001 From: Erin Date: Tue, 7 Jul 2020 10:35:20 -0400 Subject: [PATCH 04/19] :wrench: Prompt User to Save Before Page Unload --- packages/editor-ui/src/views/NodeView.vue | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue index de89534e0..573849eb3 100644 --- a/packages/editor-ui/src/views/NodeView.vue +++ b/packages/editor-ui/src/views/NodeView.vue @@ -1328,6 +1328,10 @@ export default mixins( document.addEventListener('keydown', this.keyDown); document.addEventListener('keyup', this.keyUp); + window.onbeforeunload = this.confirmSave; + }, + async confirmSave(e: Event) { + window.confirm(); }, __addConnection (connection: [IConnection, IConnection], addVisualConnection = false) { if (addVisualConnection === true) { From ad1228e0ea310b10c81c9bbea3562396f2ece8e9 Mon Sep 17 00:00:00 2001 From: Erin Date: Thu, 9 Jul 2020 16:54:50 -0400 Subject: [PATCH 05/19] :sparkles: Everything works except refresh --- .../editor-ui/src/components/MainSidebar.vue | 27 +++++++++++++---- .../editor-ui/src/components/WorkflowOpen.vue | 17 +++++++++-- .../src/components/mixins/workflowHelpers.ts | 29 ++++++++++++++++++ packages/editor-ui/src/views/NodeView.vue | 30 ++++++++++++++----- 4 files changed, 87 insertions(+), 16 deletions(-) diff --git a/packages/editor-ui/src/components/MainSidebar.vue b/packages/editor-ui/src/components/MainSidebar.vue index 71388c2cd..57096c0f2 100644 --- a/packages/editor-ui/src/components/MainSidebar.vue +++ b/packages/editor-ui/src/components/MainSidebar.vue @@ -443,13 +443,28 @@ export default mixins( } else if (key === 'workflow-settings') { this.workflowSettingsDialogVisible = true; } else if (key === 'workflow-new') { - this.$router.push({ name: 'NodeViewNew' }); + const workflowId = this.$store.getters.workflowId; + const result = await this.dataHasChanged(workflowId); + if(result) { + const importConfirm = await this.confirmMessage(`When you switch workflows your current workflow changes will be lost.`, 'Save your Changes?', 'warning', 'Yes, switch workflows and forget changes'); + if (importConfirm === true) { + this.$router.push({ name: 'NodeViewNew' }); - this.$showMessage({ - title: 'Workflow created', - message: 'A new workflow got created!', - type: 'success', - }); + this.$showMessage({ + title: 'Workflow created', + message: 'A new workflow got created!', + type: 'success', + }); + } + } else { + this.$router.push({ name: 'NodeViewNew' }); + + this.$showMessage({ + title: 'Workflow created', + message: 'A new workflow got created!', + type: 'success', + }); + } } else if (key === 'credentials-open') { this.credentialOpenDialogVisible = true; } else if (key === 'credentials-new') { diff --git a/packages/editor-ui/src/components/WorkflowOpen.vue b/packages/editor-ui/src/components/WorkflowOpen.vue index 9592ecb4d..9ef5e94eb 100644 --- a/packages/editor-ui/src/components/WorkflowOpen.vue +++ b/packages/editor-ui/src/components/WorkflowOpen.vue @@ -33,6 +33,7 @@ import WorkflowActivator from '@/components/WorkflowActivator.vue'; import { restApi } from '@/components/mixins/restApi'; import { genericHelpers } from '@/components/mixins/genericHelpers'; +import { workflowHelpers } from '@/components/mixins/workflowHelpers'; import { showMessage } from '@/components/mixins/showMessage'; import { IWorkflowShortResponse } from '@/Interface'; @@ -42,6 +43,7 @@ export default mixins( genericHelpers, restApi, showMessage, + workflowHelpers, ).extend({ name: 'WorkflowOpen', props: [ @@ -87,9 +89,20 @@ export default mixins( this.$emit('closeDialog'); return false; }, - openWorkflow (data: IWorkflowShortResponse, column: any) { // tslint:disable-line:no-any + async openWorkflow (data: IWorkflowShortResponse, column: any) { // tslint:disable-line:no-any if (column.label !== 'Active') { - this.$emit('openWorkflow', data.id); + const workflowId = this.$store.getters.workflowId; + const result = await this.dataHasChanged(workflowId); + if(result) { + const importConfirm = await this.confirmMessage(`When you switch workflows your current workflow changes will be lost.`, 'Save your Changes?', 'warning', 'Yes, switch workflows and forget changes'); + if (importConfirm === false) { + return; + } else { + this.$emit('openWorkflow', data.id); + } + } else { + this.$emit('openWorkflow', data.id); + } } }, openDialog () { diff --git a/packages/editor-ui/src/components/mixins/workflowHelpers.ts b/packages/editor-ui/src/components/mixins/workflowHelpers.ts index 507d6d31e..76ad77cdb 100644 --- a/packages/editor-ui/src/components/mixins/workflowHelpers.ts +++ b/packages/editor-ui/src/components/mixins/workflowHelpers.ts @@ -22,6 +22,7 @@ import { INodeTypesMaxCount, INodeUi, IWorkflowData, + IWorkflowDb, IWorkflowDataUpdate, XYPositon, } from '../../Interface'; @@ -30,6 +31,8 @@ import { restApi } from '@/components/mixins/restApi'; import { nodeHelpers } from '@/components/mixins/nodeHelpers'; import { showMessage } from '@/components/mixins/showMessage'; +import { isEqual } from 'lodash'; + import mixins from 'vue-typed-mixins'; export const workflowHelpers = mixins( @@ -478,5 +481,31 @@ export const workflowHelpers = mixins( node.position[1] += offsetPosition[1]; } }, + async dataHasChanged(id: string) { + const currentData = await this.getWorkflowDataToSave(); + + let data: IWorkflowDb; + data = await this.restApi().getWorkflow(id); + + if(data !== undefined) { + console.log(currentData); + console.log(data); + const x = { + nodes: data.nodes, + connections: data.connections, + settings: data.settings, + name: data.name + }; + const y = { + nodes: currentData.nodes, + connections: currentData.connections, + settings: currentData.settings, + name: currentData.name + }; + return !isEqual(x, y); + } + + return true; + }, }, }); diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue index 573849eb3..8ce1378d5 100644 --- a/packages/editor-ui/src/views/NodeView.vue +++ b/packages/editor-ui/src/views/NodeView.vue @@ -126,7 +126,7 @@ import RunData from '@/components/RunData.vue'; import mixins from 'vue-typed-mixins'; -import { debounce } from 'lodash'; +import { debounce, isEqual } from 'lodash'; import axios from 'axios'; import { IConnection, @@ -330,6 +330,8 @@ export default mixins( this.$store.commit('setWorkflowSettings', data.settings || {}); await this.addNodes(data.nodes, data.connections); + + return data; }, mouseDown (e: MouseEvent) { // Save the location of the mouse click @@ -1309,6 +1311,7 @@ export default mixins( if (this.$route.name === 'ExecutionById') { // Load an execution const executionId = this.$route.params.id; + await this.openExecution(executionId); } else { // Load a workflow @@ -1316,7 +1319,6 @@ export default mixins( if (this.$route.params.name) { workflowId = this.$route.params.name; } - if (workflowId !== null) { // Open existing workflow await this.openWorkflow(workflowId); @@ -1328,10 +1330,22 @@ export default mixins( document.addEventListener('keydown', this.keyDown); document.addEventListener('keyup', this.keyUp); - window.onbeforeunload = this.confirmSave; - }, - async confirmSave(e: Event) { - window.confirm(); + + window.addEventListener("beforeunload", (e) => { + let workflowId = null as string | null; + if (this.$route.params.name) { + workflowId = this.$route.params.name; + } + if(workflowId !== null) { + //const dataHasChanged = await this.dataHasChanged(workflowId); + } + + const confirmationMessage = 'It looks like you have been editing something. ' + + 'If you leave before saving, your changes will be lost.'; + + (e || window.event).returnValue = confirmationMessage; //Gecko + IE + return confirmationMessage; //Gecko + Webkit, Safari, Chrome etc. + }); }, __addConnection (connection: [IConnection, IConnection], addVisualConnection = false) { if (addVisualConnection === true) { @@ -1876,13 +1890,13 @@ export default mixins( async mounted () { this.$root.$on('importWorkflowData', async (data: IDataObject) => { - await this.importWorkflowData(data.data as IWorkflowDataUpdate); + const resData = await this.importWorkflowData(data.data as IWorkflowDataUpdate); }); this.$root.$on('importWorkflowUrl', async (data: IDataObject) => { const workflowData = await this.getWorkflowDataFromUrl(data.url as string); if (workflowData !== undefined) { - await this.importWorkflowData(workflowData); + const resData = await this.importWorkflowData(workflowData); } }); From 70a584a46d419f83b05034a7cd3eacfd8bea248c Mon Sep 17 00:00:00 2001 From: Erin Date: Mon, 20 Jul 2020 10:57:58 -0400 Subject: [PATCH 06/19] :tada: Works with ctrl s, now working on a user saving from the side bar --- packages/editor-ui/src/views/NodeView.vue | 55 +++++++++++++++++------ 1 file changed, 42 insertions(+), 13 deletions(-) diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue index 8ce1378d5..5f2a08e3f 100644 --- a/packages/editor-ui/src/views/NodeView.vue +++ b/packages/editor-ui/src/views/NodeView.vue @@ -186,6 +186,38 @@ export default mixins( // When a node gets set as active deactivate the create-menu this.createNodeActive = false; }, + nodes: { + handler: async function (val, oldVal) { + // Load a workflow + let workflowId = null as string | null; + if (this.$route && this.$route.params.name) { + workflowId = this.$route.params.name; + } + if(workflowId !== null) { + this.isDirty = await this.dataHasChanged(workflowId); + } else { + this.isDirty = true; + } + console.log(this.isDirty); + }, + deep: true + }, + connections: { + handler: async function (val, oldVal) { + // Load a workflow + let workflowId = null as string | null; + if (this.$route && this.$route.params.name) { + workflowId = this.$route.params.name; + } + if(workflowId !== null) { + this.isDirty = await this.dataHasChanged(workflowId); + } else { + this.isDirty = true; + } + console.log(this.isDirty); + }, + deep: true + }, }, computed: { activeNode (): INodeUi | null { @@ -259,6 +291,7 @@ export default mixins( ctrlKeyPressed: false, debouncedFunctions: [] as any[], // tslint:disable-line:no-any stopExecutionInProgress: false, + isDirty: false, }; }, beforeDestroy () { @@ -433,6 +466,8 @@ export default mixins( e.stopPropagation(); e.preventDefault(); + this.isDirty = false; + this.callDebounced('saveCurrentWorkflow', 1000); } else if (e.key === 'Enter') { // Activate the last selected node @@ -1305,6 +1340,7 @@ export default mixins( if (this.$route.params.action === 'workflowSave') { // In case the workflow got saved we do not have to run init // as only the route changed but all the needed data is already loaded + this.isDirty = false; return Promise.resolve(); } @@ -1331,20 +1367,13 @@ export default mixins( document.addEventListener('keydown', this.keyDown); document.addEventListener('keyup', this.keyUp); - window.addEventListener("beforeunload", (e) => { - let workflowId = null as string | null; - if (this.$route.params.name) { - workflowId = this.$route.params.name; + window.addEventListener("beforeunload", (e) => { + if(this.isDirty === true) { + const confirmationMessage = 'It looks like you have been editing something. ' + + 'If you leave before saving, your changes will be lost.'; + (e || window.event).returnValue = confirmationMessage; //Gecko + IE + return confirmationMessage; //Gecko + Webkit, Safari, Chrome etc. } - if(workflowId !== null) { - //const dataHasChanged = await this.dataHasChanged(workflowId); - } - - const confirmationMessage = 'It looks like you have been editing something. ' - + 'If you leave before saving, your changes will be lost.'; - - (e || window.event).returnValue = confirmationMessage; //Gecko + IE - return confirmationMessage; //Gecko + Webkit, Safari, Chrome etc. }); }, __addConnection (connection: [IConnection, IConnection], addVisualConnection = false) { From 5f32341a9ea14a50b405ed0606949ae660706bd1 Mon Sep 17 00:00:00 2001 From: Erin Date: Mon, 20 Jul 2020 11:52:24 -0400 Subject: [PATCH 07/19] Remove logs --- packages/editor-ui/src/components/MainSidebar.vue | 2 ++ .../editor-ui/src/components/mixins/workflowHelpers.ts | 2 -- packages/editor-ui/src/views/NodeView.vue | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/editor-ui/src/components/MainSidebar.vue b/packages/editor-ui/src/components/MainSidebar.vue index 57096c0f2..5cb68b4ee 100644 --- a/packages/editor-ui/src/components/MainSidebar.vue +++ b/packages/editor-ui/src/components/MainSidebar.vue @@ -435,8 +435,10 @@ export default mixins( saveAs(blob, workflowName + '.json'); } else if (key === 'workflow-save') { + console.log("saving......"); this.saveCurrentWorkflow(); } else if (key === 'workflow-save-as') { + console.log("saving......"); this.saveCurrentWorkflow(true); } else if (key === 'help-about') { this.aboutDialogVisible = true; diff --git a/packages/editor-ui/src/components/mixins/workflowHelpers.ts b/packages/editor-ui/src/components/mixins/workflowHelpers.ts index 76ad77cdb..b3c2416b2 100644 --- a/packages/editor-ui/src/components/mixins/workflowHelpers.ts +++ b/packages/editor-ui/src/components/mixins/workflowHelpers.ts @@ -488,8 +488,6 @@ export const workflowHelpers = mixins( data = await this.restApi().getWorkflow(id); if(data !== undefined) { - console.log(currentData); - console.log(data); const x = { nodes: data.nodes, connections: data.connections, diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue index 5f2a08e3f..c75899424 100644 --- a/packages/editor-ui/src/views/NodeView.vue +++ b/packages/editor-ui/src/views/NodeView.vue @@ -187,7 +187,7 @@ export default mixins( this.createNodeActive = false; }, nodes: { - handler: async function (val, oldVal) { + async handler (val, oldVal) { // Load a workflow let workflowId = null as string | null; if (this.$route && this.$route.params.name) { @@ -198,12 +198,11 @@ export default mixins( } else { this.isDirty = true; } - console.log(this.isDirty); }, deep: true }, connections: { - handler: async function (val, oldVal) { + async handler (val, oldVal) { // Load a workflow let workflowId = null as string | null; if (this.$route && this.$route.params.name) { @@ -214,7 +213,6 @@ export default mixins( } else { this.isDirty = true; } - console.log(this.isDirty); }, deep: true }, @@ -1373,6 +1371,8 @@ export default mixins( + 'If you leave before saving, your changes will be lost.'; (e || window.event).returnValue = confirmationMessage; //Gecko + IE return confirmationMessage; //Gecko + Webkit, Safari, Chrome etc. + } else { + return; } }); }, From 3e1ada7c1af80c5e7b2c64b5d220dde3a298e753 Mon Sep 17 00:00:00 2001 From: Rupenieks Date: Tue, 1 Sep 2020 16:06:08 +0200 Subject: [PATCH 08/19] :construction: Added Vuex dirty state flag as central source of truth for if there are unsaved changes --- .../editor-ui/src/components/MainHeader.vue | 5 +- .../editor-ui/src/components/MainSidebar.vue | 7 +- .../editor-ui/src/components/WorkflowOpen.vue | 3 +- .../src/components/mixins/moveNodeWorkflow.ts | 4 +- .../src/components/mixins/workflowHelpers.ts | 8 +-- .../src/components/mixins/workflowSave.ts | 4 +- packages/editor-ui/src/store.ts | 69 ++++++++++++++++--- packages/editor-ui/src/views/NodeView.vue | 41 +++++------ 8 files changed, 92 insertions(+), 49 deletions(-) diff --git a/packages/editor-ui/src/components/MainHeader.vue b/packages/editor-ui/src/components/MainHeader.vue index 2b9696e2b..4624ad403 100644 --- a/packages/editor-ui/src/components/MainHeader.vue +++ b/packages/editor-ui/src/components/MainHeader.vue @@ -12,7 +12,7 @@ - of + of "{{workflowName}}" @@ -154,6 +154,9 @@ export default mixins( workflowRunning (): boolean { return this.$store.getters.isActionActive('workflowRunning'); }, + isDirty () : boolean { + return this.$store.getters.getStateIsDirty; + }, }, methods: { async openWorkflow (workflowId: string) { diff --git a/packages/editor-ui/src/components/MainSidebar.vue b/packages/editor-ui/src/components/MainSidebar.vue index ea2a8b4ce..92645942b 100644 --- a/packages/editor-ui/src/components/MainSidebar.vue +++ b/packages/editor-ui/src/components/MainSidebar.vue @@ -398,7 +398,7 @@ export default mixins( return; } - this.$store.commit('setWorkflowName', workflowName); + this.$store.commit('setWorkflowName', {newName: workflowName, setStateDirty: true}); this.$showMessage({ title: 'Workflow renamed', @@ -440,18 +440,15 @@ export default mixins( saveAs(blob, workflowName + '.json'); } else if (key === 'workflow-save') { - console.log("saving......"); this.saveCurrentWorkflow(); } else if (key === 'workflow-save-as') { - console.log("saving......"); this.saveCurrentWorkflow(true); } else if (key === 'help-about') { this.aboutDialogVisible = true; } else if (key === 'workflow-settings') { this.workflowSettingsDialogVisible = true; } else if (key === 'workflow-new') { - const workflowId = this.$store.getters.workflowId; - const result = await this.dataHasChanged(workflowId); + const result = this.$store.getters.getStateIsDirty; if(result) { const importConfirm = await this.confirmMessage(`When you switch workflows your current workflow changes will be lost.`, 'Save your Changes?', 'warning', 'Yes, switch workflows and forget changes'); if (importConfirm === true) { diff --git a/packages/editor-ui/src/components/WorkflowOpen.vue b/packages/editor-ui/src/components/WorkflowOpen.vue index 45f1cfa0f..5ef981ef5 100644 --- a/packages/editor-ui/src/components/WorkflowOpen.vue +++ b/packages/editor-ui/src/components/WorkflowOpen.vue @@ -92,8 +92,7 @@ export default mixins( }, async openWorkflow (data: IWorkflowShortResponse, column: any) { // tslint:disable-line:no-any if (column.label !== 'Active') { - const workflowId = this.$store.getters.workflowId; - const result = await this.dataHasChanged(workflowId); + const result = this.$store.getters.getStateIsDirty; if(result) { const importConfirm = await this.confirmMessage(`When you switch workflows your current workflow changes will be lost.`, 'Save your Changes?', 'warning', 'Yes, switch workflows and forget changes'); if (importConfirm === false) { diff --git a/packages/editor-ui/src/components/mixins/moveNodeWorkflow.ts b/packages/editor-ui/src/components/mixins/moveNodeWorkflow.ts index b526dd5c7..9dcf86aea 100644 --- a/packages/editor-ui/src/components/mixins/moveNodeWorkflow.ts +++ b/packages/editor-ui/src/components/mixins/moveNodeWorkflow.ts @@ -31,7 +31,7 @@ export const moveNodeWorkflow = mixins(nodeIndex).extend({ const nodeViewOffsetPositionX = offsetPosition[0] + (e.pageX - this.moveLastPosition[0]); const nodeViewOffsetPositionY = offsetPosition[1] + (e.pageY - this.moveLastPosition[1]); - this.$store.commit('setNodeViewOffsetPosition', [nodeViewOffsetPositionX, nodeViewOffsetPositionY]); + this.$store.commit('setNodeViewOffsetPosition', {newOffset: [nodeViewOffsetPositionX, nodeViewOffsetPositionY], setStateDirty: true}); // Update the last position this.moveLastPosition[0] = e.pageX; @@ -87,7 +87,7 @@ export const moveNodeWorkflow = mixins(nodeIndex).extend({ const offsetPosition = this.$store.getters.getNodeViewOffsetPosition; const nodeViewOffsetPositionX = offsetPosition[0] - e.deltaX; const nodeViewOffsetPositionY = offsetPosition[1] - e.deltaY; - this.$store.commit('setNodeViewOffsetPosition', [nodeViewOffsetPositionX, nodeViewOffsetPositionY]); + this.$store.commit('setNodeViewOffsetPosition', {newOffset: [nodeViewOffsetPositionX, nodeViewOffsetPositionY], setStateDirty: true}); }, }, }); diff --git a/packages/editor-ui/src/components/mixins/workflowHelpers.ts b/packages/editor-ui/src/components/mixins/workflowHelpers.ts index b3c2416b2..d5354bf2d 100644 --- a/packages/editor-ui/src/components/mixins/workflowHelpers.ts +++ b/packages/editor-ui/src/components/mixins/workflowHelpers.ts @@ -420,7 +420,7 @@ export const workflowHelpers = mixins( this.$store.commit('setActive', workflowData.active || false); this.$store.commit('setWorkflowId', workflowData.id); - this.$store.commit('setWorkflowName', workflowData.name); + this.$store.commit('setWorkflowName', {newName: workflowData.name, setStateDirty: false}); this.$store.commit('setWorkflowSettings', workflowData.settings || {}); } else { // Workflow exists already so update it @@ -435,7 +435,7 @@ export const workflowHelpers = mixins( } this.$store.commit('removeActiveAction', 'workflowSaving'); - + this.$store.commit('setStateDirty', false); this.$showMessage({ title: 'Workflow saved', message: `The workflow "${workflowData.name}" got saved!`, @@ -492,13 +492,13 @@ export const workflowHelpers = mixins( nodes: data.nodes, connections: data.connections, settings: data.settings, - name: data.name + name: data.name, }; const y = { nodes: currentData.nodes, connections: currentData.connections, settings: currentData.settings, - name: currentData.name + name: currentData.name, }; return !isEqual(x, y); } diff --git a/packages/editor-ui/src/components/mixins/workflowSave.ts b/packages/editor-ui/src/components/mixins/workflowSave.ts index 1584d9a7c..c4d400104 100644 --- a/packages/editor-ui/src/components/mixins/workflowSave.ts +++ b/packages/editor-ui/src/components/mixins/workflowSave.ts @@ -74,7 +74,7 @@ export const workflowSave = mixins( this.$store.commit('setActive', workflowData.active || false); this.$store.commit('setWorkflowId', workflowData.id); - this.$store.commit('setWorkflowName', workflowData.name); + this.$store.commit('setWorkflowName', {newName: workflowData.name, setStateDirty: false}); this.$store.commit('setWorkflowSettings', workflowData.settings || {}); } else { // Workflow exists already so update it @@ -89,7 +89,7 @@ export const workflowSave = mixins( } this.$store.commit('removeActiveAction', 'workflowSaving'); - + this.$store.commit('setStateDirty', false); this.$showMessage({ title: 'Workflow saved', message: `The workflow "${workflowData.name}" got saved!`, diff --git a/packages/editor-ui/src/store.ts b/packages/editor-ui/src/store.ts index 1b54f3c1a..935947834 100644 --- a/packages/editor-ui/src/store.ts +++ b/packages/editor-ui/src/store.ts @@ -52,6 +52,7 @@ export const store = new Vuex.Store({ saveDataSuccessExecution: 'all', saveManualExecutions: false, timezone: 'America/New_York', + stateIsDirty: false, executionTimeout: -1, maxExecutionTimeout: Number.MAX_SAFE_INTEGER, versionCli: '0.0.0', @@ -83,6 +84,7 @@ export const store = new Vuex.Store({ state.activeActions.push(action); } }, + removeActiveAction (state, action: string) { const actionIndex = state.activeActions.indexOf(action); if (actionIndex !== -1) { @@ -92,6 +94,7 @@ export const store = new Vuex.Store({ // Active Executions addActiveExecution (state, newActiveExecution: IExecutionsCurrentSummaryExtended) { + state.stateIsDirty = true; // Check if the execution exists already const activeExecution = state.activeExecutions.find(execution => { return execution.idActive === newActiveExecution.idActive; @@ -108,6 +111,7 @@ export const store = new Vuex.Store({ state.activeExecutions.unshift(newActiveExecution); }, finishActiveExecution (state, finishedActiveExecution: IPushDataExecutionFinished) { + state.stateIsDirty = true; // Find the execution to set to finished const activeExecution = state.activeExecutions.find(execution => { return execution.idActive === finishedActiveExecution.executionIdActive; @@ -126,6 +130,7 @@ export const store = new Vuex.Store({ Vue.set(activeExecution, 'stoppedAt', finishedActiveExecution.data.stoppedAt); }, setActiveExecutions (state, newActiveExecutions: IExecutionsCurrentSummaryExtended[]) { + state.stateIsDirty = true; Vue.set(state, 'activeExecutions', newActiveExecutions); }, @@ -134,23 +139,32 @@ export const store = new Vuex.Store({ state.activeWorkflows = newActiveWorkflows; }, setWorkflowActive (state, workflowId: string) { + state.stateIsDirty = true; const index = state.activeWorkflows.indexOf(workflowId); if (index === -1) { state.activeWorkflows.push(workflowId); } }, setWorkflowInactive (state, workflowId: string) { + state.stateIsDirty = true; const index = state.activeWorkflows.indexOf(workflowId); if (index !== -1) { state.selectedNodes.splice(index, 1); } }, + // Set state condition dirty or not + // ** Dirty: if current workflow state has been synchronized with database AKA has it been saved + setStateDirty (state, dirty : boolean) { + state.stateIsDirty = dirty; + }, // Selected Nodes addSelectedNode (state, node: INodeUi) { + state.stateIsDirty = true; state.selectedNodes.push(node); }, removeNodeFromSelection (state, node: INodeUi) { + state.stateIsDirty = true; let index; for (index in state.selectedNodes) { if (state.selectedNodes[index].name === node.name) { @@ -176,6 +190,10 @@ export const store = new Vuex.Store({ return; } + if (data.setStateDirty) { + state.stateIsDirty = true; + } + const sourceData: IConnection = data.connection[0]; const destinationData: IConnection = data.connection[1]; @@ -211,6 +229,7 @@ export const store = new Vuex.Store({ if (connectionExists === false) { state.workflow.connections[sourceData.node][sourceData.type][sourceData.index].push(destinationData); } + }, removeConnection (state, data) { const sourceData = data.connection[0]; @@ -226,6 +245,8 @@ export const store = new Vuex.Store({ return; } + state.stateIsDirty = true; + const connections = state.workflow.connections[sourceData.node][sourceData.type][sourceData.index]; for (const index in connections) { if (connections[index].node === destinationData.node && connections[index].type === destinationData.type && connections[index].index === destinationData.index) { @@ -233,11 +254,16 @@ export const store = new Vuex.Store({ connections.splice(parseInt(index, 10), 1); } } + }, - removeAllConnections (state) { + removeAllConnections (state, data) { + if (data.setStateDirty === true) { + state.stateIsDirty = true; + } state.workflow.connections = {}; }, removeAllNodeConnection (state, node: INodeUi) { + state.stateIsDirty = true; // Remove all source connections if (state.workflow.connections.hasOwnProperty(node.name)) { delete state.workflow.connections[node.name]; @@ -275,6 +301,7 @@ export const store = new Vuex.Store({ if (state.credentials === null) { return; } + for (let i = 0; i < state.credentials.length; i++) { if (state.credentials[i].id === credentialData.id) { state.credentials.splice(i, 1); @@ -286,6 +313,7 @@ export const store = new Vuex.Store({ if (state.credentials === null) { return; } + for (let i = 0; i < state.credentials.length; i++) { if (state.credentials[i].id === credentialData.id) { state.credentials[i] = credentialData; @@ -301,6 +329,7 @@ export const store = new Vuex.Store({ }, renameNodeSelectedAndExecution (state, nameData) { + state.stateIsDirty = true; // If node has any WorkflowResultData rename also that one that the data // does still get displayed also after node got renamed if (state.workflowExecutionData !== null && state.workflowExecutionData.data.resultData.runData.hasOwnProperty(nameData.old)) { @@ -318,10 +347,12 @@ export const store = new Vuex.Store({ state.workflow.nodes.forEach((node) => { node.issues = undefined; }); + return true; }, setNodeIssue (state, nodeIssueData: INodeIssueData) { + const node = state.workflow.nodes.find(node => { return node.name === nodeIssueData.node; }); @@ -345,6 +376,7 @@ export const store = new Vuex.Store({ // Set/Overwrite the value Vue.set(node.issues!, nodeIssueData.type, nodeIssueData.value); + state.stateIsDirty = true; } return true; @@ -356,8 +388,11 @@ export const store = new Vuex.Store({ }, // Name - setWorkflowName (state, newName: string) { - state.workflow.name = newName; + setWorkflowName (state, data) { + if (data.setStateDirty === true) { + state.stateIsDirty = true; + } + state.workflow.name = data.newName; }, // Nodes @@ -374,11 +409,15 @@ export const store = new Vuex.Store({ for (let i = 0; i < state.workflow.nodes.length; i++) { if (state.workflow.nodes[i].name === node.name) { state.workflow.nodes.splice(i, 1); + state.stateIsDirty = true; return; } } }, - removeAllNodes (state) { + removeAllNodes (state, data) { + if (data.setStateDirty === true) { + state.stateIsDirty = true; + } state.workflow.nodes.splice(0, state.workflow.nodes.length); }, updateNodeProperties (state, updateInformation: INodeUpdatePropertiesInformation) { @@ -388,6 +427,7 @@ export const store = new Vuex.Store({ }); if (node) { + state.stateIsDirty = true; for (const key of Object.keys(updateInformation.properties)) { Vue.set(node, key, updateInformation.properties[key]); } @@ -403,6 +443,7 @@ export const store = new Vuex.Store({ throw new Error(`Node with the name "${updateInformation.name}" could not be found to set parameter.`); } + state.stateIsDirty = true; Vue.set(node, updateInformation.key, updateInformation.value); }, setNodeParameters (state, updateInformation: IUpdateInformation) { @@ -415,6 +456,7 @@ export const store = new Vuex.Store({ throw new Error(`Node with the name "${updateInformation.name}" could not be found to set parameter.`); } + state.stateIsDirty = true; Vue.set(node, 'parameters', updateInformation.value); }, @@ -423,6 +465,7 @@ export const store = new Vuex.Store({ state.nodeIndex.push(nodeName); }, setNodeIndex (state, newData: { index: number, name: string | null}) { + state.stateIsDirty = true; state.nodeIndex[newData.index] = newData.name; }, resetNodeIndex (state) { @@ -433,8 +476,11 @@ export const store = new Vuex.Store({ setNodeViewMoveInProgress (state, value: boolean) { state.nodeViewMoveInProgress = value; }, - setNodeViewOffsetPosition (state, newOffset: XYPositon) { - state.nodeViewOffsetPosition = newOffset; + setNodeViewOffsetPosition (state, data) { + if (data.setStateDirty === true) { + state.stateIsDirty = true; + } + state.nodeViewOffsetPosition = data.newOffset; }, // Node-Types @@ -497,19 +543,22 @@ export const store = new Vuex.Store({ // TODO: Check if there is an error or whatever that is supposed to be returned return; } - + state.stateIsDirty = true; state.nodeTypes.push(typeData); }, setActiveNode (state, nodeName: string) { + state.stateIsDirty = true; state.activeNode = nodeName; }, setLastSelectedNode (state, nodeName: string) { + state.stateIsDirty = true; state.lastSelectedNode = nodeName; }, setLastSelectedNodeOutputIndex (state, outputIndex: number | null) { + state.stateIsDirty = true; state.lastSelectedNodeOutputIndex = outputIndex; }, @@ -523,7 +572,7 @@ export const store = new Vuex.Store({ if (state.workflowExecutionData.data.resultData.runData[pushData.nodeName] === undefined) { Vue.set(state.workflowExecutionData.data.resultData.runData, pushData.nodeName, []); } - + state.stateIsDirty = true; state.workflowExecutionData.data.resultData.runData[pushData.nodeName].push(pushData.data); }, @@ -588,6 +637,10 @@ export const store = new Vuex.Store({ return `${state.urlBaseWebhook}${state.endpointWebhookTest}`; }, + getStateIsDirty: (state) : boolean => { + return state.stateIsDirty; + }, + saveDataErrorExecution: (state): string => { return state.saveDataErrorExecution; }, diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue index 4b12a7d7b..914f42e67 100644 --- a/packages/editor-ui/src/views/NodeView.vue +++ b/packages/editor-ui/src/views/NodeView.vue @@ -127,7 +127,7 @@ import NodeSettings from '@/components/NodeSettings.vue'; import RunData from '@/components/RunData.vue'; import mixins from 'vue-typed-mixins'; - +import { uuid } from 'uuidv4'; import { debounce, isEqual } from 'lodash'; import axios from 'axios'; import { @@ -196,13 +196,8 @@ export default mixins( if (this.$route && this.$route.params.name) { workflowId = this.$route.params.name; } - if(workflowId !== null) { - this.isDirty = await this.dataHasChanged(workflowId); - } else { - this.isDirty = true; - } }, - deep: true + deep: true, }, connections: { async handler (val, oldVal) { @@ -211,13 +206,8 @@ export default mixins( if (this.$route && this.$route.params.name) { workflowId = this.$route.params.name; } - if(workflowId !== null) { - this.isDirty = await this.dataHasChanged(workflowId); - } else { - this.isDirty = true; - } }, - deep: true + deep: true, }, }, computed: { @@ -292,7 +282,6 @@ export default mixins( ctrlKeyPressed: false, debouncedFunctions: [] as any[], // tslint:disable-line:no-any stopExecutionInProgress: false, - isDirty: false, }; }, beforeDestroy () { @@ -336,7 +325,7 @@ export default mixins( throw new Error(`Execution with id "${executionId}" could not be found!`); } - this.$store.commit('setWorkflowName', data.workflowData.name); + this.$store.commit('setWorkflowName', {newName: data.workflowData.name, setStateDirty: false}); this.$store.commit('setWorkflowId', PLACEHOLDER_EMPTY_WORKFLOW_ID); this.$store.commit('setWorkflowExecutionData', data); @@ -360,7 +349,7 @@ export default mixins( this.$store.commit('setActive', data.active || false); this.$store.commit('setWorkflowId', workflowId); - this.$store.commit('setWorkflowName', data.name); + this.$store.commit('setWorkflowName', {newName: data.name, setStateDirty: false}); this.$store.commit('setWorkflowSettings', data.settings || {}); await this.addNodes(data.nodes, data.connections); @@ -467,7 +456,7 @@ export default mixins( e.stopPropagation(); e.preventDefault(); - this.isDirty = false; + this.$store.commit('setStateDirty', false); this.callDebounced('saveCurrentWorkflow', 1000); } else if (e.key === 'Enter') { @@ -985,7 +974,7 @@ export default mixins( newNodeData.name = this.getUniqueNodeName(newNodeData.name); if (nodeTypeData.webhooks && nodeTypeData.webhooks.length) { - newNodeData.webhookId = uuidv4(); + newNodeData.webhookId = uuid(); } await this.addNodes([newNodeData]); @@ -1345,7 +1334,7 @@ export default mixins( if (this.$route.params.action === 'workflowSave') { // In case the workflow got saved we do not have to run init // as only the route changed but all the needed data is already loaded - this.isDirty = false; + this.$store.commit('setStateDirty', false); return Promise.resolve(); } @@ -1375,7 +1364,7 @@ export default mixins( document.addEventListener('keyup', this.keyUp); window.addEventListener("beforeunload", (e) => { - if(this.isDirty === true) { + if(this.$store.getters.getStateIsDirty === true) { const confirmationMessage = 'It looks like you have been editing something. ' + 'If you leave before saving, your changes will be lost.'; (e || window.event).returnValue = confirmationMessage; //Gecko + IE @@ -1399,6 +1388,8 @@ export default mixins( detachable: !this.isReadOnly, }); } else { + // @ts-ignore + connection.setStateDirty = false; // When nodes get connected it gets saved automatically to the storage // so if we do not connect we have to save the connection manually this.$store.commit('addConnection', { connection }); @@ -1586,7 +1577,7 @@ export default mixins( this.instance.deleteEveryEndpoint(); } this.$store.commit('removeAllConnections'); - this.$store.commit('removeAllNodes'); + this.$store.commit('removeAllNodes', {setStateDirty: true}); // Wait a tick that the old nodes had time to get removed await Vue.nextTick(); @@ -1876,8 +1867,8 @@ export default mixins( }); } - this.$store.commit('removeAllConnections'); - this.$store.commit('removeAllNodes'); + this.$store.commit('removeAllConnections', {setStateDirty: false}); + this.$store.commit('removeAllNodes', {setStateDirty: false}); // Reset workflow execution data this.$store.commit('setWorkflowExecutionData', null); @@ -1886,7 +1877,7 @@ export default mixins( this.$store.commit('setActive', false); this.$store.commit('setWorkflowId', PLACEHOLDER_EMPTY_WORKFLOW_ID); - this.$store.commit('setWorkflowName', ''); + this.$store.commit('setWorkflowName', {newName: '', setStateDirty: false}); this.$store.commit('setWorkflowSettings', {}); this.$store.commit('setActiveExecutionId', null); @@ -1897,7 +1888,7 @@ export default mixins( this.$store.commit('resetNodeIndex'); this.$store.commit('resetSelectedNodes'); - this.$store.commit('setNodeViewOffsetPosition', [0, 0]); + this.$store.commit('setNodeViewOffsetPosition', {newOffset: [0, 0], setStateDirty: false}); return Promise.resolve(); }, From d756bea1f40e7e020016607025e64dc29c213756 Mon Sep 17 00:00:00 2001 From: Rupenieks Date: Tue, 1 Sep 2020 16:42:40 +0200 Subject: [PATCH 09/19] :zap: Added asterisk to indicate if workflow is saved or not --- packages/editor-ui/src/components/MainHeader.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/editor-ui/src/components/MainHeader.vue b/packages/editor-ui/src/components/MainHeader.vue index 4624ad403..97a4a55f5 100644 --- a/packages/editor-ui/src/components/MainHeader.vue +++ b/packages/editor-ui/src/components/MainHeader.vue @@ -19,7 +19,7 @@ workflow - Workflow: {{workflowName}} + Workflow: {{workflowName}}* Workflow was not saved! From 4343bec2e0625b6870ccb10cde7ccbfdf003b9b1 Mon Sep 17 00:00:00 2001 From: Rupenieks Date: Tue, 1 Sep 2020 17:00:52 +0200 Subject: [PATCH 10/19] Revert "Merge branch 'save-changes-warning' of https://github.com/n8n-io/n8n into save-changes-warning" This reverts commit ebc7e76968668eba345828895ade59f1e9a6d53c, reversing changes made to 18c8c408e28cd29458cbb5e4f7a88e8142b7a935. --- .../nodes/Paddle/CouponDescription.ts | 394 +++++++++++++++--- .../nodes/Paddle/GenericFunctions.ts | 38 +- .../nodes/Paddle/OrderDescription.ts | 6 +- .../nodes-base/nodes/Paddle/Paddle.node.ts | 217 ++++++++-- .../nodes/Paddle/PaddleTrigger.node.ts | 165 -------- .../nodes/Paddle/PaymentDescription.ts | 92 ++-- .../nodes/Paddle/PlanDescription.ts | 49 ++- .../nodes/Paddle/ProductDescription.ts | 41 ++ .../nodes/Paddle/UserDescription.ts | 10 +- 9 files changed, 707 insertions(+), 305 deletions(-) delete mode 100644 packages/nodes-base/nodes/Paddle/PaddleTrigger.node.ts diff --git a/packages/nodes-base/nodes/Paddle/CouponDescription.ts b/packages/nodes-base/nodes/Paddle/CouponDescription.ts index 64e87cf9a..179ec825b 100644 --- a/packages/nodes-base/nodes/Paddle/CouponDescription.ts +++ b/packages/nodes-base/nodes/Paddle/CouponDescription.ts @@ -37,9 +37,9 @@ export const couponOperations = [ ] as INodeProperties[]; export const couponFields = [ -/* -------------------------------------------------------------------------- */ -/* coupon:create */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* coupon:create */ + /* -------------------------------------------------------------------------- */ { displayName: 'Coupon Type', name: 'couponType', @@ -71,9 +71,12 @@ export const couponFields = [ ] }, { - displayName: 'Product ID(s)', + displayName: 'Product IDs', name: 'productIds', - type: 'string', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getProducts', + }, displayOptions: { show: { resource: [ @@ -184,6 +187,38 @@ export const couponFields = [ default: 'EUR', description: 'The currency must match the balance currency specified in your account.', options: [ + { + name: 'ARS', + value: 'ARS' + }, + { + name: 'AUD', + value: 'AUD' + }, + { + name: 'BRL', + value: 'BRL' + }, + { + name: 'CAD', + value: 'CAD' + }, + { + name: 'CHF', + value: 'CHF' + }, + { + name: 'CNY', + value: 'CNY' + }, + { + name: 'CZK', + value: 'CZK' + }, + { + name: 'DKK', + value: 'DKK' + }, { name: 'EUR', value: 'EUR' @@ -192,10 +227,70 @@ export const couponFields = [ name: 'GBP', value: 'GBP' }, + { + name: 'HKD', + value: 'HKD' + }, + { + name: 'HUF', + value: 'HUF' + }, + { + name: 'INR', + value: 'INR' + }, + { + name: 'JPY', + value: 'JPY' + }, + { + name: 'KRW', + value: 'KRW' + }, + { + name: 'MXN', + value: 'MXN' + }, + { + name: 'NOK', + value: 'NOK' + }, + { + name: 'NZD', + value: 'NZD' + }, + { + name: 'PLN', + value: 'PLN' + }, + { + name: 'RUB', + value: 'RUB' + }, + { + name: 'SEK', + value: 'SEK' + }, + { + name: 'SGD', + value: 'SGD' + }, + { + name: 'THB', + value: 'THB' + }, + { + name: 'TWD', + value: 'TWD' + }, { name: 'USD', value: 'USD' }, + { + name: 'ZAR', + value: 'ZAR' + }, ], displayOptions: { show: { @@ -295,6 +390,13 @@ export const couponFields = [ default: '', description: 'Prefix for generated codes. Not valid if coupon_code is specified.', }, + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + description: 'Description of the coupon. This will be displayed in the Seller Dashboard.', + }, { displayName: 'Expires', name: 'expires', @@ -313,13 +415,6 @@ export const couponFields = [ default: '', description: 'The name of the coupon group this coupon should be assigned to.', }, - { - displayName: 'Recurring', - name: 'recurring', - type: 'boolean', - default: false, - description: 'If the coupon is used on subscription products, this indicates whether the discount should apply to recurring payments after the initial purchase.', - }, { displayName: 'Number of Coupons', name: 'numberOfCoupons', @@ -328,21 +423,21 @@ export const couponFields = [ description: 'Number of coupons to generate. Not valid if coupon_code is specified.', }, { - displayName: 'Description', - name: 'description', - type: 'string', - default: '', - description: 'Description of the coupon. This will be displayed in the Seller Dashboard.', + displayName: 'Recurring', + name: 'recurring', + type: 'boolean', + default: false, + description: 'If the coupon is used on subscription products, this indicates whether the discount should apply to recurring payments after the initial purchase.', }, ], }, -/* -------------------------------------------------------------------------- */ -/* coupon:getAll */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* coupon:getAll */ + /* -------------------------------------------------------------------------- */ { displayName: 'Product ID', name: 'productId', - type: 'number', + type: 'string', displayOptions: { show: { resource: [ @@ -354,11 +449,53 @@ export const couponFields = [ }, }, default: '', + required: true, description: 'The specific product/subscription ID.', }, -/* -------------------------------------------------------------------------- */ -/* coupon:update */ -/* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'coupon', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'coupon', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 100, + description: 'How many results to return.', + }, + /* -------------------------------------------------------------------------- */ + /* coupon:update */ + /* -------------------------------------------------------------------------- */ { displayName: 'Update by', name: 'updateBy', @@ -453,7 +590,7 @@ export const couponFields = [ }, }, { - displayName: ' Additional Fields', + displayName: 'Additional Fields', name: 'additionalFieldsJson', type: 'json', typeOptions: { @@ -503,36 +640,191 @@ export const couponFields = [ description: 'Number of times a coupon can be used in a checkout. This will be set to 999,999 by default, if not specified.', }, { - displayName: 'Currency', - name: 'currency', - type: 'options', - default: 'EUR', - description: 'The currency must match the balance currency specified in your account.', + displayName: 'Discount', + name: 'discount', + type: 'fixedCollection', + default: 'discountProperties', options: [ { - name: 'EUR', - value: 'EUR' - }, - { - name: 'GBP', - value: 'GBP' - }, - { - name: 'USD', - value: 'USD' + displayName: 'Discount Properties', + name: 'discountProperties', + values: [ + { + displayName: 'Currency', + name: 'currency', + type: 'options', + default: 'EUR', + description: 'The currency must match the balance currency specified in your account.', + displayOptions: { + show: { + discountType: [ + 'flat', + ], + }, + }, + options: [ + { + name: 'ARS', + value: 'ARS' + }, + { + name: 'AUD', + value: 'AUD' + }, + { + name: 'BRL', + value: 'BRL' + }, + { + name: 'CAD', + value: 'CAD' + }, + { + name: 'CHF', + value: 'CHF' + }, + { + name: 'CNY', + value: 'CNY' + }, + { + name: 'CZK', + value: 'CZK' + }, + { + name: 'DKK', + value: 'DKK' + }, + { + name: 'EUR', + value: 'EUR' + }, + { + name: 'GBP', + value: 'GBP' + }, + { + name: 'HKD', + value: 'HKD' + }, + { + name: 'HUF', + value: 'HUF' + }, + { + name: 'INR', + value: 'INR' + }, + { + name: 'JPY', + value: 'JPY' + }, + { + name: 'KRW', + value: 'KRW' + }, + { + name: 'MXN', + value: 'MXN' + }, + { + name: 'NOK', + value: 'NOK' + }, + { + name: 'NZD', + value: 'NZD' + }, + { + name: 'PLN', + value: 'PLN' + }, + { + name: 'RUB', + value: 'RUB' + }, + { + name: 'SEK', + value: 'SEK' + }, + { + name: 'SGD', + value: 'SGD' + }, + { + name: 'THB', + value: 'THB' + }, + { + name: 'TWD', + value: 'TWD' + }, + { + name: 'USD', + value: 'USD' + }, + { + name: 'ZAR', + value: 'ZAR' + }, + ], + }, + { + displayName: 'Discount Amount Currency', + name: 'discountAmount', + type: 'number', + default: '', + description: 'Discount amount.', + displayOptions: { + show: { + discountType: [ + 'flat', + ], + }, + }, + typeOptions: { + minValue: 0 + }, + }, + { + displayName: 'Discount Amount Percentage', + name: 'discountAmount', + type: 'number', + default: '', + description: 'Discount amount.', + displayOptions: { + show: { + discountType: [ + 'percentage', + ], + }, + }, + typeOptions: { + minValue: 0, + maxValue: 100 + }, + }, + { + displayName: 'Discount Type', + name: 'discountType', + type: 'options', + default: 'flat', + description: 'Either flat or percentage.', + options: [ + { + name: 'Flat', + value: 'flat' + }, + { + name: 'Percentage', + value: 'percentage' + }, + ] + }, + ], }, ], }, - { - displayName: 'Discount Amount', - name: 'discountAmount', - type: 'number', - default: '', - description: 'Discount amount.', - typeOptions: { - minValue: 0 - }, - }, { displayName: 'Expires', name: 'expires', @@ -559,7 +851,7 @@ export const couponFields = [ description: 'New group name to move coupon to.', }, { - displayName: 'Product ID(s)', + displayName: 'Product IDs', name: 'productIds', type: 'string', default: '', diff --git a/packages/nodes-base/nodes/Paddle/GenericFunctions.ts b/packages/nodes-base/nodes/Paddle/GenericFunctions.ts index 8aa131884..243dc56ff 100644 --- a/packages/nodes-base/nodes/Paddle/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Paddle/GenericFunctions.ts @@ -1,4 +1,6 @@ -import { OptionsWithUri } from 'request'; +import { + OptionsWithUri, +} from 'request'; import { IExecuteFunctions, @@ -6,7 +8,6 @@ import { ILoadOptionsFunctions, IExecuteSingleFunctions, IWebhookFunctions, - BINARY_ENCODING } from 'n8n-core'; import { @@ -20,37 +21,50 @@ export async function paddleApiRequest(this: IHookFunctions | IExecuteFunctions throw new Error('Could not retrieve credentials!'); } - const options : OptionsWithUri = { + const options: OptionsWithUri = { method, headers: { 'content-type': 'application/json' }, - uri: `https://vendors.paddle.com/api${endpoint}` , + uri: `https://vendors.paddle.com/api${endpoint}`, body, json: true }; body['vendor_id'] = credentials.vendorId; body['vendor_auth_code'] = credentials.vendorAuthCode; - - console.log(options.body); - console.log(options); - try { const response = await this.helpers.request!(options); - console.log(response); if (!response.success) { throw new Error(`Code: ${response.error.code}. Message: ${response.error.message}`); } - return response.response; + return response; } catch (error) { - console.log(error); - throw new Error(error); + throw new Error(`ERROR: Code: ${error.code}. Message: ${error.message}`); } } +export async function paddleApiRequestAllItems(this: IHookFunctions | IExecuteFunctions, propertyName: string, endpoint: string, method: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + let responseData; + + body.results_per_page = 200; + body.page = 1; + + do { + responseData = await paddleApiRequest.call(this, endpoint, method, body, query); + returnData.push.apply(returnData, responseData[propertyName]); + } while ( + responseData[propertyName].length !== 0 + ); + + return returnData; +} + export function validateJSON(json: string | undefined): any { // tslint:disable-line:no-any let result; try { diff --git a/packages/nodes-base/nodes/Paddle/OrderDescription.ts b/packages/nodes-base/nodes/Paddle/OrderDescription.ts index 367082a4b..d4afd8f7c 100644 --- a/packages/nodes-base/nodes/Paddle/OrderDescription.ts +++ b/packages/nodes-base/nodes/Paddle/OrderDescription.ts @@ -28,9 +28,9 @@ export const orderOperations = [ export const orderFields = [ -/* -------------------------------------------------------------------------- */ -/* order:get */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* order:get */ + /* -------------------------------------------------------------------------- */ { displayName: 'Checkout ID', name: 'checkoutId', diff --git a/packages/nodes-base/nodes/Paddle/Paddle.node.ts b/packages/nodes-base/nodes/Paddle/Paddle.node.ts index e5478aa11..94e1246f4 100644 --- a/packages/nodes-base/nodes/Paddle/Paddle.node.ts +++ b/packages/nodes-base/nodes/Paddle/Paddle.node.ts @@ -1,20 +1,53 @@ -import { IExecuteFunctions } from 'n8n-core'; +import { + IExecuteFunctions +} from 'n8n-core'; + import { IDataObject, + ILoadOptionsFunctions, INodeExecutionData, + INodePropertyOptions, INodeType, - INodeTypeDescription + INodeTypeDescription, } from 'n8n-workflow'; -import { couponFields, couponOperations } from './CouponDescription'; -import { paddleApiRequest, validateJSON } from './GenericFunctions'; -import { paymentsFields, paymentsOperations } from './PaymentDescription'; -import { planFields, planOperations } from './PlanDescription'; -import { productFields, productOperations } from './ProductDescription'; -import { userFields, userOperations } from './UserDescription'; +import { + couponFields, + couponOperations, +} from './CouponDescription'; -import moment = require('moment'); -import { orderOperations, orderFields } from './OrderDescription'; +import { + paddleApiRequest, + paddleApiRequestAllItems, + validateJSON +} from './GenericFunctions'; + +import { + paymentFields, + paymentOperations, +} from './PaymentDescription'; + +import { + planFields, + planOperations, +} from './PlanDescription'; + +import { + productFields, + productOperations, +} from './ProductDescription'; + +import { + userFields, + userOperations, +} from './UserDescription'; + +// import { +// orderOperations, +// orderFields, +// } from './OrderDescription'; + +import * as moment from 'moment'; export class Paddle implements INodeType { description: INodeTypeDescription = { @@ -48,8 +81,8 @@ export class Paddle implements INodeType { value: 'coupon', }, { - name: 'Payments', - value: 'payments', + name: 'Payment', + value: 'payment', }, { name: 'Plan', @@ -59,10 +92,10 @@ export class Paddle implements INodeType { name: 'Product', value: 'product', }, - { - name: 'Order', - value: 'order', - }, + // { + // name: 'Order', + // value: 'order', + // }, { name: 'User', value: 'user', @@ -75,8 +108,8 @@ export class Paddle implements INodeType { ...couponOperations, ...couponFields, // PAYMENT - ...paymentsOperations, - ...paymentsFields, + ...paymentOperations, + ...paymentFields, // PLAN ...planOperations, ...planFields, @@ -84,14 +117,69 @@ export class Paddle implements INodeType { ...productOperations, ...productFields, // ORDER - ...orderOperations, - ...orderFields, + // ...orderOperations, + // ...orderFields, // USER ...userOperations, ...userFields ], }; + methods = { + loadOptions: { + /* -------------------------------------------------------------------------- */ + /* PAYMENT */ + /* -------------------------------------------------------------------------- */ + + // Get all payment so they can be selected in payment rescheduling + async getPayments(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const endpoint = '/2.0/subscription/payments'; + const paymentResponse = await paddleApiRequest.call(this, endpoint, 'POST', {}); + + // Alert user if there's no payments present to be loaded into payments property + if (paymentResponse.response === undefined || paymentResponse.response.length === 0) { + throw Error('No payments on account.'); + } + + for (const payment of paymentResponse.response) { + const id = payment.id; + returnData.push({ + name: id, + value: id, + }); + } + return returnData; + }, + + /* -------------------------------------------------------------------------- */ + /* PRODUCTS */ + /* -------------------------------------------------------------------------- */ + + // Get all Products so they can be selected in coupon creation when assigning products + async getProducts(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const endpoint = '/2.0/product/get_products'; + const products = await paddleApiRequest.call(this, endpoint, 'POST', {}); + + // Alert user if there's no products present to be loaded into payments property + if (products.length === 0) { + throw Error('No products on account.'); + } + + for (const product of products) { + const name = product.name; + const id = product.id; + returnData.push({ + name, + value: id, + }); + } + return returnData; + }, + } + }; + async execute(this: IExecuteFunctions): Promise { const items = this.getInputData(); const returnData: IDataObject[] = []; @@ -117,20 +205,21 @@ export class Paddle implements INodeType { } } else { - const discountType = this.getNodeParameter('discountType', i) as string; const couponType = this.getNodeParameter('couponType', i) as string; const discountAmount = this.getNodeParameter('discountAmount', i) as number; - const currency = this.getNodeParameter('currency', i) as string; if (couponType === 'product') { - body.product_ids = this.getNodeParameter('productIds', i) as string; + body.product_ids = (this.getNodeParameter('productIds', i) as string[]).join(); + } + + if (discountType === 'flat') { + body.currency = this.getNodeParameter('currency', i) as string; } body.coupon_type = couponType; body.discount_type = discountType; body.discount_amount = discountAmount; - body.currency = currency; const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; @@ -164,16 +253,25 @@ export class Paddle implements INodeType { const endpoint = '/2.1/product/create_coupon'; responseData = await paddleApiRequest.call(this, endpoint, 'POST', body); + responseData = responseData.response.coupon_codes; } } if (operation === 'getAll') { - const productIds = this.getNodeParameter('productId', i) as string; + const productId = this.getNodeParameter('productId', i) as string; + const returnAll = this.getNodeParameter('returnAll', i) as boolean; const endpoint = '/2.0/product/list_coupons'; - body.product_id = productIds as string; + body.product_id = productId as string; responseData = await paddleApiRequest.call(this, endpoint, 'POST', body); + + if (returnAll) { + responseData = responseData.response; + } else { + const limit = this.getNodeParameter('limit', i) as number; + responseData = responseData.response.splice(0, limit); + } } if (operation === 'update') { @@ -217,9 +315,9 @@ export class Paddle implements INodeType { if (additionalFields.newGroup) { body.new_group = additionalFields.newGroup as string; } - if (additionalFields.recurring) { + if (additionalFields.recurring === true) { body.recurring = 1; - } else { + } else if (additionalFields.recurring === false) { body.recurring = 0; } if (additionalFields.productIds) { @@ -228,15 +326,29 @@ export class Paddle implements INodeType { if (additionalFields.discountAmount) { body.discount_amount = additionalFields.discountAmount as number; } + if (additionalFields.discount) { + //@ts-ignore + if (additionalFields.discount.discountProperties.discountType === 'percentage') { + // @ts-ignore + body.discount_amount = additionalFields.discount.discountProperties.discountAmount as number; + } else { + //@ts-ignore + body.currency = additionalFields.discount.discountProperties.currency as string; + //@ts-ignore + body.discount_amount = additionalFields.discount.discountProperties.discountAmount as number; + } + } } const endpoint = '/2.1/product/update_coupon'; responseData = await paddleApiRequest.call(this, endpoint, 'POST', body); + responseData = responseData.response; } } if (resource === 'payment') { if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; const jsonParameters = this.getNodeParameter('jsonParameters', i) as boolean; if (jsonParameters) { @@ -262,10 +374,10 @@ export class Paddle implements INodeType { if (additionalFields.state) { body.state = additionalFields.state as string; } - if (additionalFields.recurring) { - body.recurring = 1; + if (additionalFields.isPaid) { + body.is_paid = 1; } else { - body.recurring = 0; + body.is_paid = 0; } if (additionalFields.from) { body.from = moment(additionalFields.from as Date).format('YYYY-MM-DD') as string; @@ -277,10 +389,16 @@ export class Paddle implements INodeType { body.is_one_off_charge = additionalFields.isOneOffCharge as boolean; } } - const endpoint = '/2.0/subscription/payments'; responseData = await paddleApiRequest.call(this, endpoint, 'POST', body); + + if (returnAll) { + responseData = responseData.response; + } else { + const limit = this.getNodeParameter('limit', i) as number; + responseData = responseData.response.splice(0, limit); + } } if (operation === 'reschedule') { const paymentId = this.getNodeParameter('paymentId', i) as number; @@ -296,9 +414,17 @@ export class Paddle implements INodeType { } if (resource === 'plan') { if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; const endpoint = '/2.0/subscription/plans'; responseData = await paddleApiRequest.call(this, endpoint, 'POST', body); + + if (returnAll) { + responseData = responseData.response; + } else { + const limit = this.getNodeParameter('limit', i) as number; + responseData = responseData.response.splice(0, limit); + } } if (operation === 'get') { const planId = this.getNodeParameter('planId', i) as string; @@ -308,13 +434,22 @@ export class Paddle implements INodeType { const endpoint = '/2.0/subscription/plans'; responseData = await paddleApiRequest.call(this, endpoint, 'POST', body); + responseData = responseData.response; } } if (resource === 'product') { if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; const endpoint = '/2.0/product/get_products'; responseData = await paddleApiRequest.call(this, endpoint, 'POST', body); + + if (returnAll) { + responseData = responseData.response.products; + } else { + const limit = this.getNodeParameter('limit', i) as number; + responseData = responseData.response.products.splice(0, limit); + } } } if (resource === 'order') { @@ -329,6 +464,7 @@ export class Paddle implements INodeType { } if (resource === 'user') { if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; const jsonParameters = this.getNodeParameter('jsonParameters', i) as boolean; @@ -344,11 +480,6 @@ export class Paddle implements INodeType { } } else { - const returnAll = this.getNodeParameter('returnAll', i) as boolean; - - if (!returnAll) { - body.results_per_page = this.getNodeParameter('limit', i) as number; - } const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; @@ -361,16 +492,20 @@ export class Paddle implements INodeType { if (additionalFields.subscriptionId) { body.subscription_id = additionalFields.subscriptionId as string; } - } + const endpoint = '/2.0/subscription/users'; - responseData = await paddleApiRequest.call(this, endpoint, 'POST', body); + if (returnAll) { + responseData = await paddleApiRequestAllItems.call(this, 'response', endpoint, 'POST', body); + } else { + body.results_per_page = this.getNodeParameter('limit', i) as number; + responseData = await paddleApiRequest.call(this, endpoint, 'POST', body); + responseData = responseData.response; + } } } - console.log(responseData); - if (Array.isArray(responseData)) { returnData.push.apply(returnData, responseData as IDataObject[]); } else { diff --git a/packages/nodes-base/nodes/Paddle/PaddleTrigger.node.ts b/packages/nodes-base/nodes/Paddle/PaddleTrigger.node.ts deleted file mode 100644 index f439a5beb..000000000 --- a/packages/nodes-base/nodes/Paddle/PaddleTrigger.node.ts +++ /dev/null @@ -1,165 +0,0 @@ -import { - IHookFunctions, - IWebhookFunctions, - } from 'n8n-core'; - - import { - IDataObject, - INodeTypeDescription, - INodeType, - IWebhookResponseData, - ILoadOptionsFunctions, - INodePropertyOptions, - } from 'n8n-workflow'; -import { paddleApiRequest } from './GenericFunctions'; - - export class PaddleTrigger implements INodeType { - description: INodeTypeDescription = { - displayName: 'Paddle Trigger', - name: 'paddleTrigger', - icon: 'file:paddle.png', - group: ['trigger'], - version: 1, - description: 'Handle Paddle events via webhooks', - defaults: { - name: 'Paddle Trigger', - color: '#32325d', - }, - inputs: [], - outputs: ['main'], - credentials: [ - { - name: 'paddleApi', - required: true, - } - ], - webhooks: [ - { - name: 'default', - httpMethod: 'POST', - reponseMode: 'onReceived', - path: 'webhook', - }, - ], - properties: [ - { - displayName: 'Events', - name: 'events', - type: 'multiOptions', - required: true, - default: [], - description: 'The event to listen to.', - typeOptions: { - loadOptionsMethod: 'getEvents' - }, - options: [], - }, - ], - }; - - - - // @ts-ignore (because of request) - webhookMethods = { - default: { - async checkExists(this: IHookFunctions): Promise { - const webhookData = this.getWorkflowStaticData('node'); - if (webhookData.webhookId === undefined) { - // No webhook id is set so no webhook can exist - return false; - } - const endpoint = `/notifications/webhooks/${webhookData.webhookId}`; - try { - await paddleApiRequest.call(this, endpoint, 'GET'); - } catch (err) { - if (err.response && err.response.name === 'INVALID_RESOURCE_ID') { - // Webhook does not exist - delete webhookData.webhookId; - return false; - } - throw new Error(`Paddle Error: ${err}`); - } - return true; - }, - - async create(this: IHookFunctions): Promise { - let webhook; - const webhookUrl = this.getNodeWebhookUrl('default'); - const events = this.getNodeParameter('events', []) as string[]; - const body = { - url: webhookUrl, - event_types: events.map(event => { - return { name: event }; - }), - }; - const endpoint = '/notifications/webhooks'; - try { - webhook = await paddleApiRequest.call(this, endpoint, 'POST', body); - } catch (e) { - throw e; - } - - if (webhook.id === undefined) { - return false; - } - const webhookData = this.getWorkflowStaticData('node'); - webhookData.webhookId = webhook.id as string; - return true; - }, - - async delete(this: IHookFunctions): Promise { - const webhookData = this.getWorkflowStaticData('node'); - if (webhookData.webhookId !== undefined) { - const endpoint = `/notifications/webhooks/${webhookData.webhookId}`; - try { - await paddleApiRequest.call(this, endpoint, 'DELETE', {}); - } catch (e) { - return false; - } - delete webhookData.webhookId; - } - return true; - }, - }, - }; - - async webhook(this: IWebhookFunctions): Promise { - let webhook; - const webhookData = this.getWorkflowStaticData('node') as IDataObject; - const bodyData = this.getBodyData() as IDataObject; - const req = this.getRequestObject(); - const headerData = this.getHeaderData() as IDataObject; - const endpoint = '/notifications/verify-webhook-signature'; - - if (headerData['PAYPAL-AUTH-ALGO'] !== undefined - && headerData['PAYPAL-CERT-URL'] !== undefined - && headerData['PAYPAL-TRANSMISSION-ID'] !== undefined - && headerData['PAYPAL-TRANSMISSION-SIG'] !== undefined - && headerData['PAYPAL-TRANSMISSION-TIME'] !== undefined) { - const body = { - auth_algo: headerData['PAYPAL-AUTH-ALGO'], - cert_url: headerData['PAYPAL-CERT-URL'], - transmission_id: headerData['PAYPAL-TRANSMISSION-ID'], - transmission_sig: headerData['PAYPAL-TRANSMISSION-SIG'], - transmission_time: headerData['PAYPAL-TRANSMISSION-TIME'], - webhook_id: webhookData.webhookId, - webhook_event: bodyData, - }; - try { - webhook = await paddleApiRequest.call(this, endpoint, 'POST', body); - } catch (e) { - throw e; - } - if (webhook.verification_status !== 'SUCCESS') { - return {}; - } - } else { - return {}; - } - return { - workflowData: [ - this.helpers.returnJsonArray(req.body) - ], - }; - } - } diff --git a/packages/nodes-base/nodes/Paddle/PaymentDescription.ts b/packages/nodes-base/nodes/Paddle/PaymentDescription.ts index 45d355385..b2020d4f5 100644 --- a/packages/nodes-base/nodes/Paddle/PaymentDescription.ts +++ b/packages/nodes-base/nodes/Paddle/PaymentDescription.ts @@ -2,7 +2,7 @@ import { INodeProperties, } from 'n8n-workflow'; -export const paymentsOperations = [ +export const paymentOperations = [ { displayName: 'Operation', name: 'operation', @@ -10,7 +10,7 @@ export const paymentsOperations = [ displayOptions: { show: { resource: [ - 'payments', + 'payment', ], }, }, @@ -18,7 +18,7 @@ export const paymentsOperations = [ { name: 'Get All', value: 'getAll', - description: 'Get all payments.', + description: 'Get all payment.', }, { name: 'Reschedule', @@ -31,10 +31,51 @@ export const paymentsOperations = [ }, ] as INodeProperties[]; -export const paymentsFields = [ -/* -------------------------------------------------------------------------- */ -/* payments:getAll */ -/* -------------------------------------------------------------------------- */ +export const paymentFields = [ + /* -------------------------------------------------------------------------- */ + /* payment:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'payment', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'payment', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 100, + description: 'How many results to return.', + }, { displayName: 'JSON Parameters', name: 'jsonParameters', @@ -44,7 +85,7 @@ export const paymentsFields = [ displayOptions: { show: { resource: [ - 'payments', + 'payment', ], operation: [ 'getAll', @@ -53,7 +94,7 @@ export const paymentsFields = [ }, }, { - displayName: ' Additional Fields', + displayName: 'Additional Fields', name: 'additionalFieldsJson', type: 'json', typeOptions: { @@ -63,7 +104,7 @@ export const paymentsFields = [ displayOptions: { show: { resource: [ - 'payments', + 'payment', ], operation: [ 'getAll', @@ -83,7 +124,7 @@ export const paymentsFields = [ displayOptions: { show: { resource: [ - 'payments', + 'payment', ], operation: [ 'getAll', @@ -100,14 +141,14 @@ export const paymentsFields = [ name: 'from', type: 'dateTime', default: '', - description: 'payments starting from date.', + description: 'payment starting from date.', }, { displayName: 'Date To', name: 'to', type: 'dateTime', default: '', - description: 'payments up until date.', + description: 'payment up until date.', }, { displayName: 'Is Paid', @@ -117,7 +158,7 @@ export const paymentsFields = [ description: 'payment is paid.', }, { - displayName: 'Plan', + displayName: 'Plan ID', name: 'plan', type: 'string', default: '', @@ -163,26 +204,29 @@ export const paymentsFields = [ }, ], }, -/* -------------------------------------------------------------------------- */ -/* payments:reschedule */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* payment:reschedule */ + /* -------------------------------------------------------------------------- */ { - displayName: 'payments ID', - name: 'paymentsId', - type: 'number', + displayName: 'Payment ID', + name: 'paymentId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getpayment', + }, default: '', required: true, displayOptions: { show: { resource: [ - 'payments', + 'payment', ], operation: [ 'reschedule', ], }, }, - description: 'The upcoming subscription payments ID.', // Use loadoptions to select payments + description: 'The upcoming subscription payment ID.', }, { displayName: 'Date', @@ -192,13 +236,13 @@ export const paymentsFields = [ displayOptions: { show: { resource: [ - 'payments', + 'payment', ], operation: [ 'reschedule', ], }, }, - description: 'Date you want to move the payments to.', + description: 'Date you want to move the payment to.', }, ] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Paddle/PlanDescription.ts b/packages/nodes-base/nodes/Paddle/PlanDescription.ts index f6887ca64..152db9a7f 100644 --- a/packages/nodes-base/nodes/Paddle/PlanDescription.ts +++ b/packages/nodes-base/nodes/Paddle/PlanDescription.ts @@ -26,16 +26,16 @@ export const planOperations = [ description: 'Get all plans.', } ], - default: 'getAll', + default: 'get', description: 'The operation to perform.', }, ] as INodeProperties[]; export const planFields = [ -/* -------------------------------------------------------------------------- */ -/* plan:get */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* plan:get */ + /* -------------------------------------------------------------------------- */ { displayName: 'Plan ID', name: 'planId', @@ -54,4 +54,45 @@ export const planFields = [ }, description: 'Filter: The subscription plan ID.', }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'plan', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'plan', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 100, + description: 'How many results to return.', + }, ] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Paddle/ProductDescription.ts b/packages/nodes-base/nodes/Paddle/ProductDescription.ts index 9d6ca39b3..9a627a86e 100644 --- a/packages/nodes-base/nodes/Paddle/ProductDescription.ts +++ b/packages/nodes-base/nodes/Paddle/ProductDescription.ts @@ -27,4 +27,45 @@ export const productOperations = [ ] as INodeProperties[]; export const productFields = [ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'product', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'product', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 100, + description: 'How many results to return.', + }, ] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Paddle/UserDescription.ts b/packages/nodes-base/nodes/Paddle/UserDescription.ts index d45cc2ddb..50b1a9295 100644 --- a/packages/nodes-base/nodes/Paddle/UserDescription.ts +++ b/packages/nodes-base/nodes/Paddle/UserDescription.ts @@ -27,9 +27,9 @@ export const userOperations = [ ] as INodeProperties[]; export const userFields = [ -/* -------------------------------------------------------------------------- */ -/* user:getAll */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* user:getAll */ + /* -------------------------------------------------------------------------- */ { displayName: 'Return All', name: 'returnAll', @@ -51,7 +51,7 @@ export const userFields = [ displayName: 'Limit', name: 'limit', type: 'number', - default: 1, + default: 100, required: true, typeOptions: { minValue: 1, @@ -90,7 +90,7 @@ export const userFields = [ }, }, { - displayName: ' Additional Fields', + displayName: 'Additional Fields', name: 'additionalFieldsJson', type: 'json', typeOptions: { From 1cdf0164e96219ae8b5407ffb58965e7af7de9ed Mon Sep 17 00:00:00 2001 From: Rupenieks Date: Wed, 2 Sep 2020 09:16:16 +0200 Subject: [PATCH 11/19] :zap: Returned to using correct uuid lib --- packages/editor-ui/src/views/NodeView.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue index 914f42e67..2ab3b3952 100644 --- a/packages/editor-ui/src/views/NodeView.vue +++ b/packages/editor-ui/src/views/NodeView.vue @@ -127,7 +127,7 @@ import NodeSettings from '@/components/NodeSettings.vue'; import RunData from '@/components/RunData.vue'; import mixins from 'vue-typed-mixins'; -import { uuid } from 'uuidv4'; +import { v4 as uuidv4} from 'uuid'; import { debounce, isEqual } from 'lodash'; import axios from 'axios'; import { @@ -974,7 +974,7 @@ export default mixins( newNodeData.name = this.getUniqueNodeName(newNodeData.name); if (nodeTypeData.webhooks && nodeTypeData.webhooks.length) { - newNodeData.webhookId = uuid(); + newNodeData.webhookId = uuidv4(); } await this.addNodes([newNodeData]); From 4c04cc015db0b552fa1bc87fa9a924bc113070a6 Mon Sep 17 00:00:00 2001 From: Rupenieks Date: Wed, 2 Sep 2020 15:26:17 +0200 Subject: [PATCH 12/19] :zap: Reduced dirty state sensetivity - No longer considers opening a node a state change - No longer considers selecting a node a state change --- packages/editor-ui/src/store.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/editor-ui/src/store.ts b/packages/editor-ui/src/store.ts index 935947834..4c4736541 100644 --- a/packages/editor-ui/src/store.ts +++ b/packages/editor-ui/src/store.ts @@ -160,7 +160,6 @@ export const store = new Vuex.Store({ // Selected Nodes addSelectedNode (state, node: INodeUi) { - state.stateIsDirty = true; state.selectedNodes.push(node); }, removeNodeFromSelection (state, node: INodeUi) { @@ -427,8 +426,8 @@ export const store = new Vuex.Store({ }); if (node) { - state.stateIsDirty = true; for (const key of Object.keys(updateInformation.properties)) { + state.stateIsDirty = true; Vue.set(node, key, updateInformation.properties[key]); } } @@ -548,17 +547,14 @@ export const store = new Vuex.Store({ }, setActiveNode (state, nodeName: string) { - state.stateIsDirty = true; state.activeNode = nodeName; }, setLastSelectedNode (state, nodeName: string) { - state.stateIsDirty = true; state.lastSelectedNode = nodeName; }, setLastSelectedNodeOutputIndex (state, outputIndex: number | null) { - state.stateIsDirty = true; state.lastSelectedNodeOutputIndex = outputIndex; }, From d1f9cef891e4f30dab1724bf055a426fb6884e25 Mon Sep 17 00:00:00 2001 From: Rupenieks Date: Wed, 2 Sep 2020 15:59:20 +0200 Subject: [PATCH 13/19] :zap: Fixed dirty state not being triggered with node connection --- packages/editor-ui/src/store.ts | 3 ++- packages/editor-ui/src/views/NodeView.vue | 7 +++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/editor-ui/src/store.ts b/packages/editor-ui/src/store.ts index 4c4736541..4960aa8ac 100644 --- a/packages/editor-ui/src/store.ts +++ b/packages/editor-ui/src/store.ts @@ -160,6 +160,7 @@ export const store = new Vuex.Store({ // Selected Nodes addSelectedNode (state, node: INodeUi) { + state.stateIsDirty = true; state.selectedNodes.push(node); }, removeNodeFromSelection (state, node: INodeUi) { @@ -189,7 +190,7 @@ export const store = new Vuex.Store({ return; } - if (data.setStateDirty) { + if (data.setStateDirty === true) { state.stateIsDirty = true; } diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue index 2ab3b3952..4aa2b0285 100644 --- a/packages/editor-ui/src/views/NodeView.vue +++ b/packages/editor-ui/src/views/NodeView.vue @@ -1239,7 +1239,6 @@ export default mixins( if (![null, undefined].includes(inputNameOverlay)) { inputNameOverlay.setVisible(false); } - this.$store.commit('addConnection', { connection: [ { @@ -1253,6 +1252,7 @@ export default mixins( index: targetInfo.index, }, ], + setStateDirty: true, }); }); @@ -1388,11 +1388,10 @@ export default mixins( detachable: !this.isReadOnly, }); } else { - // @ts-ignore - connection.setStateDirty = false; + let connectionProperties = {connection, setStateDirty: false}; // When nodes get connected it gets saved automatically to the storage // so if we do not connect we have to save the connection manually - this.$store.commit('addConnection', { connection }); + this.$store.commit('addConnection', connectionProperties); } }, __removeConnection (connection: [IConnection, IConnection], removeVisualConnection = false) { From 4c63db8908c9c9f1e69b9bd50e6d1acd71afcf2c Mon Sep 17 00:00:00 2001 From: Rupenieks Date: Wed, 2 Sep 2020 16:06:18 +0200 Subject: [PATCH 14/19] :zap: Build error fixed (let to const) --- packages/editor-ui/src/views/NodeView.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue index 4aa2b0285..d0b4133b1 100644 --- a/packages/editor-ui/src/views/NodeView.vue +++ b/packages/editor-ui/src/views/NodeView.vue @@ -1388,7 +1388,7 @@ export default mixins( detachable: !this.isReadOnly, }); } else { - let connectionProperties = {connection, setStateDirty: false}; + const connectionProperties = {connection, setStateDirty: false}; // When nodes get connected it gets saved automatically to the storage // so if we do not connect we have to save the connection manually this.$store.commit('addConnection', connectionProperties); From 17ab16d2480852ee75e391905a846142d8c154e3 Mon Sep 17 00:00:00 2001 From: Rupenieks Date: Wed, 9 Sep 2020 14:05:11 +0200 Subject: [PATCH 15/19] :zap: Deconstructed store mutation data parameters for better readability --- .../editor-ui/src/components/MainSidebar.vue | 2 +- .../src/components/mixins/moveNodeWorkflow.ts | 4 ++-- .../src/components/mixins/workflowHelpers.ts | 2 +- .../src/components/mixins/workflowSave.ts | 2 +- packages/editor-ui/src/store.ts | 22 +++++++++---------- packages/editor-ui/src/views/NodeView.vue | 11 +++++----- 6 files changed, 21 insertions(+), 22 deletions(-) diff --git a/packages/editor-ui/src/components/MainSidebar.vue b/packages/editor-ui/src/components/MainSidebar.vue index 92645942b..df2e75bfb 100644 --- a/packages/editor-ui/src/components/MainSidebar.vue +++ b/packages/editor-ui/src/components/MainSidebar.vue @@ -398,7 +398,7 @@ export default mixins( return; } - this.$store.commit('setWorkflowName', {newName: workflowName, setStateDirty: true}); + this.$store.commit('setWorkflowName', {name: workflowName, setStateDirty: true}); this.$showMessage({ title: 'Workflow renamed', diff --git a/packages/editor-ui/src/components/mixins/moveNodeWorkflow.ts b/packages/editor-ui/src/components/mixins/moveNodeWorkflow.ts index 9dcf86aea..140888437 100644 --- a/packages/editor-ui/src/components/mixins/moveNodeWorkflow.ts +++ b/packages/editor-ui/src/components/mixins/moveNodeWorkflow.ts @@ -31,7 +31,7 @@ export const moveNodeWorkflow = mixins(nodeIndex).extend({ const nodeViewOffsetPositionX = offsetPosition[0] + (e.pageX - this.moveLastPosition[0]); const nodeViewOffsetPositionY = offsetPosition[1] + (e.pageY - this.moveLastPosition[1]); - this.$store.commit('setNodeViewOffsetPosition', {newOffset: [nodeViewOffsetPositionX, nodeViewOffsetPositionY], setStateDirty: true}); + this.$store.commit('setNodeViewOffsetPosition', {offset: [nodeViewOffsetPositionX, nodeViewOffsetPositionY], setStateDirty: true}); // Update the last position this.moveLastPosition[0] = e.pageX; @@ -87,7 +87,7 @@ export const moveNodeWorkflow = mixins(nodeIndex).extend({ const offsetPosition = this.$store.getters.getNodeViewOffsetPosition; const nodeViewOffsetPositionX = offsetPosition[0] - e.deltaX; const nodeViewOffsetPositionY = offsetPosition[1] - e.deltaY; - this.$store.commit('setNodeViewOffsetPosition', {newOffset: [nodeViewOffsetPositionX, nodeViewOffsetPositionY], setStateDirty: true}); + this.$store.commit('setNodeViewOffsetPosition', {offset: [nodeViewOffsetPositionX, nodeViewOffsetPositionY], setStateDirty: true}); }, }, }); diff --git a/packages/editor-ui/src/components/mixins/workflowHelpers.ts b/packages/editor-ui/src/components/mixins/workflowHelpers.ts index d5354bf2d..939d08ccf 100644 --- a/packages/editor-ui/src/components/mixins/workflowHelpers.ts +++ b/packages/editor-ui/src/components/mixins/workflowHelpers.ts @@ -420,7 +420,7 @@ export const workflowHelpers = mixins( this.$store.commit('setActive', workflowData.active || false); this.$store.commit('setWorkflowId', workflowData.id); - this.$store.commit('setWorkflowName', {newName: workflowData.name, setStateDirty: false}); + this.$store.commit('setWorkflowName', {name: workflowData.name, setStateDirty: false}); this.$store.commit('setWorkflowSettings', workflowData.settings || {}); } else { // Workflow exists already so update it diff --git a/packages/editor-ui/src/components/mixins/workflowSave.ts b/packages/editor-ui/src/components/mixins/workflowSave.ts index c4d400104..45526f2b7 100644 --- a/packages/editor-ui/src/components/mixins/workflowSave.ts +++ b/packages/editor-ui/src/components/mixins/workflowSave.ts @@ -74,7 +74,7 @@ export const workflowSave = mixins( this.$store.commit('setActive', workflowData.active || false); this.$store.commit('setWorkflowId', workflowData.id); - this.$store.commit('setWorkflowName', {newName: workflowData.name, setStateDirty: false}); + this.$store.commit('setWorkflowName', {name: workflowData.name, setStateDirty: false}); this.$store.commit('setWorkflowSettings', workflowData.settings || {}); } else { // Workflow exists already so update it diff --git a/packages/editor-ui/src/store.ts b/packages/editor-ui/src/store.ts index 4960aa8ac..4a9e5c76c 100644 --- a/packages/editor-ui/src/store.ts +++ b/packages/editor-ui/src/store.ts @@ -183,19 +183,19 @@ export const store = new Vuex.Store({ }, // Connections - addConnection (state, data) { - if (data.connection.length !== 2) { + addConnection (state, {connection, setStateDirty}) { + if (connection.length !== 2) { // All connections need two entries // TODO: Check if there is an error or whatever that is supposed to be returned return; } - if (data.setStateDirty === true) { + if (setStateDirty === true) { state.stateIsDirty = true; } - const sourceData: IConnection = data.connection[0]; - const destinationData: IConnection = data.connection[1]; + const sourceData: IConnection = connection[0]; + const destinationData: IConnection = connection[1]; // Check if source node and type exist already and if not add them if (!state.workflow.connections.hasOwnProperty(sourceData.node)) { @@ -388,11 +388,11 @@ export const store = new Vuex.Store({ }, // Name - setWorkflowName (state, data) { - if (data.setStateDirty === true) { + setWorkflowName (state, {name, setStateDirty}) { + if (setStateDirty === true) { state.stateIsDirty = true; } - state.workflow.name = data.newName; + state.workflow.name = name; }, // Nodes @@ -476,11 +476,11 @@ export const store = new Vuex.Store({ setNodeViewMoveInProgress (state, value: boolean) { state.nodeViewMoveInProgress = value; }, - setNodeViewOffsetPosition (state, data) { - if (data.setStateDirty === true) { + setNodeViewOffsetPosition (state, {offset, setStateDirty}) { + if (setStateDirty === true) { state.stateIsDirty = true; } - state.nodeViewOffsetPosition = data.newOffset; + state.nodeViewOffsetPosition = offset; }, // Node-Types diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue index d0b4133b1..edf0927c2 100644 --- a/packages/editor-ui/src/views/NodeView.vue +++ b/packages/editor-ui/src/views/NodeView.vue @@ -325,7 +325,7 @@ export default mixins( throw new Error(`Execution with id "${executionId}" could not be found!`); } - this.$store.commit('setWorkflowName', {newName: data.workflowData.name, setStateDirty: false}); + this.$store.commit('setWorkflowName', {name: data.workflowData.name, setStateDirty: false}); this.$store.commit('setWorkflowId', PLACEHOLDER_EMPTY_WORKFLOW_ID); this.$store.commit('setWorkflowExecutionData', data); @@ -349,7 +349,7 @@ export default mixins( this.$store.commit('setActive', data.active || false); this.$store.commit('setWorkflowId', workflowId); - this.$store.commit('setWorkflowName', {newName: data.name, setStateDirty: false}); + this.$store.commit('setWorkflowName', {name: data.name, setStateDirty: false}); this.$store.commit('setWorkflowSettings', data.settings || {}); await this.addNodes(data.nodes, data.connections); @@ -1388,10 +1388,9 @@ export default mixins( detachable: !this.isReadOnly, }); } else { - const connectionProperties = {connection, setStateDirty: false}; // When nodes get connected it gets saved automatically to the storage // so if we do not connect we have to save the connection manually - this.$store.commit('addConnection', connectionProperties); + this.$store.commit('addConnection', {connection, setStateDirty: false}); } }, __removeConnection (connection: [IConnection, IConnection], removeVisualConnection = false) { @@ -1876,7 +1875,7 @@ export default mixins( this.$store.commit('setActive', false); this.$store.commit('setWorkflowId', PLACEHOLDER_EMPTY_WORKFLOW_ID); - this.$store.commit('setWorkflowName', {newName: '', setStateDirty: false}); + this.$store.commit('setWorkflowName', {name: '', setStateDirty: false}); this.$store.commit('setWorkflowSettings', {}); this.$store.commit('setActiveExecutionId', null); @@ -1887,7 +1886,7 @@ export default mixins( this.$store.commit('resetNodeIndex'); this.$store.commit('resetSelectedNodes'); - this.$store.commit('setNodeViewOffsetPosition', {newOffset: [0, 0], setStateDirty: false}); + this.$store.commit('setNodeViewOffsetPosition', {offset: [0, 0], setStateDirty: false}); return Promise.resolve(); }, From 33582655f284cab49cb4d55f69973cde1b47b013 Mon Sep 17 00:00:00 2001 From: Rupenieks Date: Wed, 9 Sep 2020 14:28:13 +0200 Subject: [PATCH 16/19] Revert ":zap: Deconstructed store mutation data parameters for better readability" This reverts commit 17ab16d2480852ee75e391905a846142d8c154e3. --- .../editor-ui/src/components/MainSidebar.vue | 2 +- .../src/components/mixins/moveNodeWorkflow.ts | 4 ++-- .../src/components/mixins/workflowHelpers.ts | 2 +- .../src/components/mixins/workflowSave.ts | 2 +- packages/editor-ui/src/store.ts | 22 +++++++++---------- packages/editor-ui/src/views/NodeView.vue | 11 +++++----- 6 files changed, 22 insertions(+), 21 deletions(-) diff --git a/packages/editor-ui/src/components/MainSidebar.vue b/packages/editor-ui/src/components/MainSidebar.vue index df2e75bfb..92645942b 100644 --- a/packages/editor-ui/src/components/MainSidebar.vue +++ b/packages/editor-ui/src/components/MainSidebar.vue @@ -398,7 +398,7 @@ export default mixins( return; } - this.$store.commit('setWorkflowName', {name: workflowName, setStateDirty: true}); + this.$store.commit('setWorkflowName', {newName: workflowName, setStateDirty: true}); this.$showMessage({ title: 'Workflow renamed', diff --git a/packages/editor-ui/src/components/mixins/moveNodeWorkflow.ts b/packages/editor-ui/src/components/mixins/moveNodeWorkflow.ts index 140888437..9dcf86aea 100644 --- a/packages/editor-ui/src/components/mixins/moveNodeWorkflow.ts +++ b/packages/editor-ui/src/components/mixins/moveNodeWorkflow.ts @@ -31,7 +31,7 @@ export const moveNodeWorkflow = mixins(nodeIndex).extend({ const nodeViewOffsetPositionX = offsetPosition[0] + (e.pageX - this.moveLastPosition[0]); const nodeViewOffsetPositionY = offsetPosition[1] + (e.pageY - this.moveLastPosition[1]); - this.$store.commit('setNodeViewOffsetPosition', {offset: [nodeViewOffsetPositionX, nodeViewOffsetPositionY], setStateDirty: true}); + this.$store.commit('setNodeViewOffsetPosition', {newOffset: [nodeViewOffsetPositionX, nodeViewOffsetPositionY], setStateDirty: true}); // Update the last position this.moveLastPosition[0] = e.pageX; @@ -87,7 +87,7 @@ export const moveNodeWorkflow = mixins(nodeIndex).extend({ const offsetPosition = this.$store.getters.getNodeViewOffsetPosition; const nodeViewOffsetPositionX = offsetPosition[0] - e.deltaX; const nodeViewOffsetPositionY = offsetPosition[1] - e.deltaY; - this.$store.commit('setNodeViewOffsetPosition', {offset: [nodeViewOffsetPositionX, nodeViewOffsetPositionY], setStateDirty: true}); + this.$store.commit('setNodeViewOffsetPosition', {newOffset: [nodeViewOffsetPositionX, nodeViewOffsetPositionY], setStateDirty: true}); }, }, }); diff --git a/packages/editor-ui/src/components/mixins/workflowHelpers.ts b/packages/editor-ui/src/components/mixins/workflowHelpers.ts index 939d08ccf..d5354bf2d 100644 --- a/packages/editor-ui/src/components/mixins/workflowHelpers.ts +++ b/packages/editor-ui/src/components/mixins/workflowHelpers.ts @@ -420,7 +420,7 @@ export const workflowHelpers = mixins( this.$store.commit('setActive', workflowData.active || false); this.$store.commit('setWorkflowId', workflowData.id); - this.$store.commit('setWorkflowName', {name: workflowData.name, setStateDirty: false}); + this.$store.commit('setWorkflowName', {newName: workflowData.name, setStateDirty: false}); this.$store.commit('setWorkflowSettings', workflowData.settings || {}); } else { // Workflow exists already so update it diff --git a/packages/editor-ui/src/components/mixins/workflowSave.ts b/packages/editor-ui/src/components/mixins/workflowSave.ts index 45526f2b7..c4d400104 100644 --- a/packages/editor-ui/src/components/mixins/workflowSave.ts +++ b/packages/editor-ui/src/components/mixins/workflowSave.ts @@ -74,7 +74,7 @@ export const workflowSave = mixins( this.$store.commit('setActive', workflowData.active || false); this.$store.commit('setWorkflowId', workflowData.id); - this.$store.commit('setWorkflowName', {name: workflowData.name, setStateDirty: false}); + this.$store.commit('setWorkflowName', {newName: workflowData.name, setStateDirty: false}); this.$store.commit('setWorkflowSettings', workflowData.settings || {}); } else { // Workflow exists already so update it diff --git a/packages/editor-ui/src/store.ts b/packages/editor-ui/src/store.ts index 4a9e5c76c..4960aa8ac 100644 --- a/packages/editor-ui/src/store.ts +++ b/packages/editor-ui/src/store.ts @@ -183,19 +183,19 @@ export const store = new Vuex.Store({ }, // Connections - addConnection (state, {connection, setStateDirty}) { - if (connection.length !== 2) { + addConnection (state, data) { + if (data.connection.length !== 2) { // All connections need two entries // TODO: Check if there is an error or whatever that is supposed to be returned return; } - if (setStateDirty === true) { + if (data.setStateDirty === true) { state.stateIsDirty = true; } - const sourceData: IConnection = connection[0]; - const destinationData: IConnection = connection[1]; + const sourceData: IConnection = data.connection[0]; + const destinationData: IConnection = data.connection[1]; // Check if source node and type exist already and if not add them if (!state.workflow.connections.hasOwnProperty(sourceData.node)) { @@ -388,11 +388,11 @@ export const store = new Vuex.Store({ }, // Name - setWorkflowName (state, {name, setStateDirty}) { - if (setStateDirty === true) { + setWorkflowName (state, data) { + if (data.setStateDirty === true) { state.stateIsDirty = true; } - state.workflow.name = name; + state.workflow.name = data.newName; }, // Nodes @@ -476,11 +476,11 @@ export const store = new Vuex.Store({ setNodeViewMoveInProgress (state, value: boolean) { state.nodeViewMoveInProgress = value; }, - setNodeViewOffsetPosition (state, {offset, setStateDirty}) { - if (setStateDirty === true) { + setNodeViewOffsetPosition (state, data) { + if (data.setStateDirty === true) { state.stateIsDirty = true; } - state.nodeViewOffsetPosition = offset; + state.nodeViewOffsetPosition = data.newOffset; }, // Node-Types diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue index edf0927c2..d0b4133b1 100644 --- a/packages/editor-ui/src/views/NodeView.vue +++ b/packages/editor-ui/src/views/NodeView.vue @@ -325,7 +325,7 @@ export default mixins( throw new Error(`Execution with id "${executionId}" could not be found!`); } - this.$store.commit('setWorkflowName', {name: data.workflowData.name, setStateDirty: false}); + this.$store.commit('setWorkflowName', {newName: data.workflowData.name, setStateDirty: false}); this.$store.commit('setWorkflowId', PLACEHOLDER_EMPTY_WORKFLOW_ID); this.$store.commit('setWorkflowExecutionData', data); @@ -349,7 +349,7 @@ export default mixins( this.$store.commit('setActive', data.active || false); this.$store.commit('setWorkflowId', workflowId); - this.$store.commit('setWorkflowName', {name: data.name, setStateDirty: false}); + this.$store.commit('setWorkflowName', {newName: data.name, setStateDirty: false}); this.$store.commit('setWorkflowSettings', data.settings || {}); await this.addNodes(data.nodes, data.connections); @@ -1388,9 +1388,10 @@ export default mixins( detachable: !this.isReadOnly, }); } else { + const connectionProperties = {connection, setStateDirty: false}; // When nodes get connected it gets saved automatically to the storage // so if we do not connect we have to save the connection manually - this.$store.commit('addConnection', {connection, setStateDirty: false}); + this.$store.commit('addConnection', connectionProperties); } }, __removeConnection (connection: [IConnection, IConnection], removeVisualConnection = false) { @@ -1875,7 +1876,7 @@ export default mixins( this.$store.commit('setActive', false); this.$store.commit('setWorkflowId', PLACEHOLDER_EMPTY_WORKFLOW_ID); - this.$store.commit('setWorkflowName', {name: '', setStateDirty: false}); + this.$store.commit('setWorkflowName', {newName: '', setStateDirty: false}); this.$store.commit('setWorkflowSettings', {}); this.$store.commit('setActiveExecutionId', null); @@ -1886,7 +1887,7 @@ export default mixins( this.$store.commit('resetNodeIndex'); this.$store.commit('resetSelectedNodes'); - this.$store.commit('setNodeViewOffsetPosition', {offset: [0, 0], setStateDirty: false}); + this.$store.commit('setNodeViewOffsetPosition', {newOffset: [0, 0], setStateDirty: false}); return Promise.resolve(); }, From 16195c8b55b0f8d484d4b41f9cfe68c4a5efa914 Mon Sep 17 00:00:00 2001 From: Rupenieks Date: Wed, 9 Sep 2020 14:45:49 +0200 Subject: [PATCH 17/19] :zap: Fix store state dirty when selecting a node --- packages/editor-ui/src/store.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/editor-ui/src/store.ts b/packages/editor-ui/src/store.ts index 4960aa8ac..efa656e3e 100644 --- a/packages/editor-ui/src/store.ts +++ b/packages/editor-ui/src/store.ts @@ -160,7 +160,6 @@ export const store = new Vuex.Store({ // Selected Nodes addSelectedNode (state, node: INodeUi) { - state.stateIsDirty = true; state.selectedNodes.push(node); }, removeNodeFromSelection (state, node: INodeUi) { From 72ac20b070470bf82a286d79c66d427fc3340b40 Mon Sep 17 00:00:00 2001 From: Jan Date: Sun, 25 Oct 2020 12:47:49 +0100 Subject: [PATCH 18/19] :zap: Improvements on #911 to display unsaved changes. Now works with back button. (#1098) Co-authored-by: Omar Ajoue --- .../editor-ui/src/components/MainSidebar.vue | 3 +- .../editor-ui/src/components/WorkflowOpen.vue | 16 ++++++++++ .../src/components/mixins/workflowSave.ts | 3 +- packages/editor-ui/src/views/NodeView.vue | 29 ++++++++++++++++++- 4 files changed, 48 insertions(+), 3 deletions(-) diff --git a/packages/editor-ui/src/components/MainSidebar.vue b/packages/editor-ui/src/components/MainSidebar.vue index 92645942b..863d028e1 100644 --- a/packages/editor-ui/src/components/MainSidebar.vue +++ b/packages/editor-ui/src/components/MainSidebar.vue @@ -398,7 +398,7 @@ export default mixins( return; } - this.$store.commit('setWorkflowName', {newName: workflowName, setStateDirty: true}); + this.$store.commit('setWorkflowName', {newName: workflowName, setStateDirty: false}); this.$showMessage({ title: 'Workflow renamed', @@ -452,6 +452,7 @@ export default mixins( if(result) { const importConfirm = await this.confirmMessage(`When you switch workflows your current workflow changes will be lost.`, 'Save your Changes?', 'warning', 'Yes, switch workflows and forget changes'); if (importConfirm === true) { + this.$store.commit('setStateDirty', false); this.$router.push({ name: 'NodeViewNew' }); this.$showMessage({ diff --git a/packages/editor-ui/src/components/WorkflowOpen.vue b/packages/editor-ui/src/components/WorkflowOpen.vue index 5ef981ef5..3f82f80a6 100644 --- a/packages/editor-ui/src/components/WorkflowOpen.vue +++ b/packages/editor-ui/src/components/WorkflowOpen.vue @@ -92,12 +92,28 @@ export default mixins( }, async openWorkflow (data: IWorkflowShortResponse, column: any) { // tslint:disable-line:no-any if (column.label !== 'Active') { + + const currentWorkflowId = this.$store.getters.workflowId; + + if (data.id === currentWorkflowId) { + this.$showMessage({ + title: 'Already open', + message: 'This is the current workflow', + type: 'error', + duration: 800, + }); + // Do nothing if current workflow is the one user chose to open + return; + } + const result = this.$store.getters.getStateIsDirty; if(result) { const importConfirm = await this.confirmMessage(`When you switch workflows your current workflow changes will be lost.`, 'Save your Changes?', 'warning', 'Yes, switch workflows and forget changes'); if (importConfirm === false) { return; } else { + // This is used to avoid duplicating the message + this.$store.commit('setStateDirty', false); this.$emit('openWorkflow', data.id); } } else { diff --git a/packages/editor-ui/src/components/mixins/workflowSave.ts b/packages/editor-ui/src/components/mixins/workflowSave.ts index c4d400104..60dd135fe 100644 --- a/packages/editor-ui/src/components/mixins/workflowSave.ts +++ b/packages/editor-ui/src/components/mixins/workflowSave.ts @@ -80,6 +80,8 @@ export const workflowSave = mixins( // Workflow exists already so update it await this.restApi().updateWorkflow(currentWorkflow, workflowData); } + // Set dirty = false before pushing route so unsaved changes message doesnt trigger. + this.$store.commit('setStateDirty', false); if (this.$route.params.name !== workflowData.id) { this.$router.push({ @@ -89,7 +91,6 @@ export const workflowSave = mixins( } this.$store.commit('removeActiveAction', 'workflowSaving'); - this.$store.commit('setStateDirty', false); this.$showMessage({ title: 'Workflow saved', message: `The workflow "${workflowData.name}" got saved!`, diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue index 8c82de22f..a41aa2f91 100644 --- a/packages/editor-ui/src/views/NodeView.vue +++ b/packages/editor-ui/src/views/NodeView.vue @@ -214,6 +214,21 @@ export default mixins( deep: true, }, }, + async beforeRouteLeave(to, from, next) { + const result = this.$store.getters.getStateIsDirty; + if(result) { + const importConfirm = await this.confirmMessage(`When you switch workflows your current workflow changes will be lost.`, 'Save your Changes?', 'warning', 'Yes, switch workflows and forget changes'); + if (importConfirm === false) { + next(false); + } else { + // Prevent other popups from displaying + this.$store.commit('setStateDirty', false); + next(); + } + } else { + next(); + } + }, computed: { activeNode (): INodeUi | null { return this.$store.getters.activeNode; @@ -358,6 +373,8 @@ export default mixins( await this.addNodes(data.nodes, data.connections); + this.$store.commit('setStateDirty', false); + return data; }, touchTap (e: MouseEvent | TouchEvent) { @@ -1339,6 +1356,8 @@ export default mixins( ]; await this.addNodes(defaultNodes); + this.$store.commit('setStateDirty', false); + }, async initView (): Promise { if (this.$route.params.action === 'workflowSave') { @@ -1351,9 +1370,17 @@ export default mixins( if (this.$route.name === 'ExecutionById') { // Load an execution const executionId = this.$route.params.id; - await this.openExecution(executionId); } else { + + const result = this.$store.getters.getStateIsDirty; + if(result) { + const importConfirm = await this.confirmMessage(`When you switch workflows your current workflow changes will be lost.`, 'Save your Changes?', 'warning', 'Yes, switch workflows and forget changes'); + if (importConfirm === false) { + return Promise.resolve(); + } + } + // Load a workflow let workflowId = null as string | null; if (this.$route.params.name) { From c339d764561482398dfb0638e95bc5056dfcbaa5 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Sun, 25 Oct 2020 12:58:02 +0100 Subject: [PATCH 19/19] :zap: Minior improvements --- packages/editor-ui/src/components/WorkflowOpen.vue | 2 +- packages/editor-ui/src/views/NodeView.vue | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/editor-ui/src/components/WorkflowOpen.vue b/packages/editor-ui/src/components/WorkflowOpen.vue index 3f82f80a6..6d7a1efe8 100644 --- a/packages/editor-ui/src/components/WorkflowOpen.vue +++ b/packages/editor-ui/src/components/WorkflowOpen.vue @@ -100,7 +100,7 @@ export default mixins( title: 'Already open', message: 'This is the current workflow', type: 'error', - duration: 800, + duration: 1500, }); // Do nothing if current workflow is the one user chose to open return; diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue index a41aa2f91..2cf41645e 100644 --- a/packages/editor-ui/src/views/NodeView.vue +++ b/packages/editor-ui/src/views/NodeView.vue @@ -132,7 +132,7 @@ import RunData from '@/components/RunData.vue'; import mixins from 'vue-typed-mixins'; import { v4 as uuidv4} from 'uuid'; -import { debounce, isEqual } from 'lodash'; +import { debounce } from 'lodash'; import axios from 'axios'; import { IConnection,