From d26cc64163c04efcbf36c6d6d6bb25f6933f1857 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Thu, 27 Aug 2020 02:20:29 -0400 Subject: [PATCH 1/3] :zap: Add custom fields to contact resource on Hubspot-Node (#893) --- .../nodes/Hubspot/ContactDescription.ts | 35 +++++++++++++++++++ .../nodes-base/nodes/Hubspot/Hubspot.node.ts | 33 +++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/packages/nodes-base/nodes/Hubspot/ContactDescription.ts b/packages/nodes-base/nodes/Hubspot/ContactDescription.ts index 7cd7670597..29be29c392 100644 --- a/packages/nodes-base/nodes/Hubspot/ContactDescription.ts +++ b/packages/nodes-base/nodes/Hubspot/ContactDescription.ts @@ -171,6 +171,41 @@ export const contactFields = [ type: 'string', default: '', }, + { + displayName: 'Custom Properties', + name: 'customPropertiesUi', + placeholder: 'Add Custom Property', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: {}, + options: [ + { + name: 'customPropertiesValues', + displayName: 'Custom Property', + values: [ + { + displayName: 'Property', + name: 'property', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getContactCustomProperties', + }, + default: '', + description: 'Name of the property.', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'Value of the property', + }, + ], + }, + ], + }, { displayName: 'Date of Birth', name: 'dateOfBirth', diff --git a/packages/nodes-base/nodes/Hubspot/Hubspot.node.ts b/packages/nodes-base/nodes/Hubspot/Hubspot.node.ts index cf53fdd3b4..ad62fd76a7 100644 --- a/packages/nodes-base/nodes/Hubspot/Hubspot.node.ts +++ b/packages/nodes-base/nodes/Hubspot/Hubspot.node.ts @@ -307,6 +307,25 @@ export class Hubspot implements INodeType { return returnData; }, + // Get all the contact properties to display them to user so that he can + // select them easily + async getContactCustomProperties(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const endpoint = '/properties/v2/contacts/properties'; + const properties = await hubspotApiRequest.call(this, 'GET', endpoint, {}); + for (const property of properties) { + if (property.hubspotDefined === null) { + const propertyName = property.label; + const propertyId = property.name; + returnData.push({ + name: propertyName, + value: propertyId, + }); + } + } + return returnData; + }, + // Get all the contact number of employees options to display them to user so that he can // select them easily async getContactNumberOfEmployees(this: ILoadOptionsFunctions): Promise { @@ -1064,6 +1083,20 @@ export class Hubspot implements INodeType { value: additionalFields.workEmail, }); } + + if (additionalFields.customPropertiesUi) { + const customProperties = (additionalFields.customPropertiesUi as IDataObject).customPropertiesValues as IDataObject[]; + + if (customProperties) { + for (const customProperty of customProperties) { + body.push({ + property: customProperty.property, + value: customProperty.value, + }); + } + } + } + const endpoint = `/contacts/v1/contact/createOrUpdate/email/${email}`; responseData = await hubspotApiRequest.call(this, 'POST', endpoint, { properties: body }); From 1727b4626b5eea0b01a87560bff9630f2c30ec45 Mon Sep 17 00:00:00 2001 From: YErii Date: Thu, 27 Aug 2020 14:23:02 +0800 Subject: [PATCH 2/3] :bug: Fix possible request problem on Salesforce-Node (#894) As mentioned in this [doc](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_query.htm), sfdc use query string to represent a SOQL query. It may get error response in some environment if we provide the body param such as sandbox env. --- packages/nodes-base/nodes/Salesforce/GenericFunctions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nodes-base/nodes/Salesforce/GenericFunctions.ts b/packages/nodes-base/nodes/Salesforce/GenericFunctions.ts index c86e8ce676..9b183ca73d 100644 --- a/packages/nodes-base/nodes/Salesforce/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Salesforce/GenericFunctions.ts @@ -13,7 +13,7 @@ export async function salesforceApiRequest(this: IExecuteFunctions | IExecuteSin const subdomain = ((credentials!.accessTokenUrl as string).match(/https:\/\/(.+).salesforce\.com/) || [])[1] const options: OptionsWithUri = { method, - body, + body: method === "GET" ? undefined : body, qs, uri: uri || `https://${subdomain}.salesforce.com/services/data/v39.0${resource}`, json: true From 74e2bcd1e0d01eeefec44344eb6da4b4e159801d Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Thu, 27 Aug 2020 02:53:23 -0400 Subject: [PATCH 3/3] :zap: Add user resource (#892) --- .../nodes/Zendesk/GenericFunctions.ts | 8 +- .../nodes/Zendesk/UserDescription.ts | 692 ++++++++++++++++++ .../nodes-base/nodes/Zendesk/Zendesk.node.ts | 129 +++- 3 files changed, 824 insertions(+), 5 deletions(-) create mode 100644 packages/nodes-base/nodes/Zendesk/UserDescription.ts diff --git a/packages/nodes-base/nodes/Zendesk/GenericFunctions.ts b/packages/nodes-base/nodes/Zendesk/GenericFunctions.ts index 5b4757abc8..3a1447997f 100644 --- a/packages/nodes-base/nodes/Zendesk/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Zendesk/GenericFunctions.ts @@ -23,7 +23,10 @@ export async function zendeskApiRequest(this: IHookFunctions | IExecuteFunctions body, //@ts-ignore uri, - json: true + json: true, + qsStringifyOptions: { + arrayFormat: 'brackets', + }, }; options = Object.assign({}, options, option); @@ -43,8 +46,6 @@ export async function zendeskApiRequest(this: IHookFunctions | IExecuteFunctions options.uri = `https://${credentials.subdomain}.zendesk.com/api/v2${resource}.json`; options.headers!['Authorization'] = `Basic ${base64Key}`; - //console.log(options); - return await this.helpers.request!(options); } else { const credentials = this.getCredentials('zendeskOAuth2Api'); @@ -58,6 +59,7 @@ export async function zendeskApiRequest(this: IHookFunctions | IExecuteFunctions return await this.helpers.requestOAuth2!.call(this, 'zendeskOAuth2Api', options); } } catch(err) { + let errorMessage = err.message; if (err.response && err.response.body && err.response.body.error) { errorMessage = err.response.body.error; diff --git a/packages/nodes-base/nodes/Zendesk/UserDescription.ts b/packages/nodes-base/nodes/Zendesk/UserDescription.ts new file mode 100644 index 0000000000..bc43738ae0 --- /dev/null +++ b/packages/nodes-base/nodes/Zendesk/UserDescription.ts @@ -0,0 +1,692 @@ +import { + INodeProperties, + } from 'n8n-workflow'; + +export const userOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'user', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a user', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a user', + }, + { + name: 'Get', + value: 'get', + description: 'Get a user', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all users', + }, + { + name: 'Update', + value: 'update', + description: 'Update a user', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const userFields = [ + +/* -------------------------------------------------------------------------- */ +/* user:create */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'create', + ], + }, + }, + required: true, + description: `The user's name`, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Alias', + name: 'alis', + type: 'string', + default: '', + description: `An alias displayed to end users`, + }, + { + displayName: 'Custom Role ID', + name: 'custom_role_id', + type: 'number', + default: 0, + description: `A custom role if the user is an agent on the Enterprise plan`, + }, + { + displayName: 'Details', + name: 'details', + type: 'string', + default: '', + description: 'Any details you want to store about the user, such as an address', + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + default: '', + description: `The user's primary email address`, + }, + { + displayName: 'External ID', + name: 'externalId', + type: 'string', + default: '', + description: 'A unique identifier from another system', + }, + { + displayName: 'Locale ID', + name: 'locale', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getLocales', + }, + default: '', + description: `The user's locale.`, + }, + { + displayName: 'Moderator', + name: 'moderator', + type: 'boolean', + default: false, + description: 'Designates whether the user has forum moderation capabilities', + }, + { + displayName: 'Notes', + name: 'notes', + type: 'string', + default: '', + description: 'Any notes you want to store about the user', + }, + { + displayName: 'Only Private Comments', + name: 'only_private_comments', + type: 'boolean', + default: false, + description: `true if the user can only create private comments`, + }, + { + displayName: 'Organization ID', + name: 'organizationId', + type: 'number', + default: 0, + description: `The id of the user's organization. If the user has more than one organization memberships, the id of the user's default organization`, + }, + { + displayName: 'Phone', + name: 'phone', + type: 'string', + default: '', + description: `The user's primary phone number.`, + }, + { + displayName: 'Report CSV', + name: 'report_csv', + type: 'boolean', + default: false, + description: `Whether or not the user can access the CSV report on the Search tab of the Reporting page in the Support admin interface.`, + }, + { + displayName: 'Restricted Agent', + name: 'restricted_agent', + type: 'boolean', + default: false, + description: `If the agent has any restrictions; false for admins and unrestricted agents, true for other agents`, + }, + { + displayName: 'Role', + name: 'role', + type: 'options', + options: [ + { + name: 'End User', + value: 'end-user', + }, + { + name: 'Agent', + value: 'agent', + }, + { + name: 'Admin', + value: 'admin', + }, + ], + default: '', + description: `The user's role`, + }, + { + displayName: 'Signature', + name: 'signature', + type: 'string', + default: '', + description: `The user's signature. Only agents and admins can have signatures`, + }, + { + displayName: 'Suspended', + name: 'suspended', + type: 'boolean', + default: false, + description: `If the agent is suspended. Tickets from suspended users are also suspended, and these users cannot sign in to the end user portal`, + }, + { + displayName: 'Tags', + name: 'tags', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getTags', + }, + default: [], + description: 'The array of tags applied to this user', + }, + { + displayName: 'Ticket Restriction', + name: 'ticket_restriction', + type: 'options', + options: [ + { + name: 'Organization', + value: 'organization', + }, + { + name: 'Groups', + value: 'groups', + }, + { + name: 'Assigned', + value: 'assigned', + }, + { + name: 'Requested', + value: 'requested', + }, + ], + default: '', + description: `Specifies which tickets the user has access to`, + }, + { + displayName: 'Timezone', + name: 'time_zone', + type: 'string', + default: '', + description: `The user's time zone.`, + }, + { + displayName: 'User Fields', + name: 'userFieldsUi', + placeholder: 'Add User Field', + description: `Values of custom fields in the user's profile.`, + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: {}, + options: [ + { + name: 'userFieldValues', + displayName: 'Field', + values: [ + { + displayName: 'Field', + name: 'field', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUserFields', + }, + default: '', + description: 'Name of the field to sort on.', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'Value of the field.', + }, + ], + }, + ], + }, + { + displayName: 'Verified', + name: 'verified', + type: 'boolean', + default: false, + description: `The user's primary identity is verified or not`, + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* user:update */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'User ID', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'update', + ], + }, + }, + description: 'User ID', + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'Alias', + name: 'alis', + type: 'string', + default: '', + description: `An alias displayed to end users`, + }, + { + displayName: 'Custom Role ID', + name: 'custom_role_id', + type: 'number', + default: 0, + description: `A custom role if the user is an agent on the Enterprise plan`, + }, + { + displayName: 'Details', + name: 'details', + type: 'string', + default: '', + description: 'Any details you want to store about the user, such as an address', + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + default: '', + description: `The user's primary email address`, + }, + { + displayName: 'External ID', + name: 'externalId', + type: 'string', + default: '', + description: 'A unique identifier from another system', + }, + { + displayName: 'Locale ID', + name: 'locale', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getLocales', + }, + default: '', + description: `The user's locale.`, + }, + { + displayName: 'Moderator', + name: 'moderator', + type: 'boolean', + default: false, + description: 'Designates whether the user has forum moderation capabilities', + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + description: `The user's name`, + }, + { + displayName: 'Notes', + name: 'notes', + type: 'string', + default: '', + description: 'Any notes you want to store about the user', + }, + { + displayName: 'Only Private Comments', + name: 'only_private_comments', + type: 'boolean', + default: false, + description: `true if the user can only create private comments`, + }, + { + displayName: 'Organization ID', + name: 'organizationId', + type: 'number', + default: 0, + description: `The id of the user's organization. If the user has more than one organization memberships, the id of the user's default organization`, + }, + { + displayName: 'Phone', + name: 'phone', + type: 'string', + default: '', + description: `The user's primary phone number.`, + }, + { + displayName: 'Report CSV', + name: 'report_csv', + type: 'boolean', + default: false, + description: `Whether or not the user can access the CSV report on the Search tab of the Reporting page in the Support admin interface.`, + }, + { + displayName: 'Restricted Agent', + name: 'restricted_agent', + type: 'boolean', + default: false, + description: `If the agent has any restrictions; false for admins and unrestricted agents, true for other agents`, + }, + { + displayName: 'Role', + name: 'role', + type: 'options', + options: [ + { + name: 'End User', + value: 'end-user', + }, + { + name: 'Agent', + value: 'agent', + }, + { + name: 'Admin', + value: 'admin', + }, + ], + default: '', + description: `The user's role`, + }, + { + displayName: 'Signature', + name: 'signature', + type: 'string', + default: '', + description: `The user's signature. Only agents and admins can have signatures`, + }, + { + displayName: 'Suspended', + name: 'suspended', + type: 'boolean', + default: false, + description: `If the agent is suspended. Tickets from suspended users are also suspended, and these users cannot sign in to the end user portal`, + }, + { + displayName: 'Tags', + name: 'tags', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getTags', + }, + default: [], + description: 'The array of tags applied to this user', + }, + { + displayName: 'Ticket Restriction', + name: 'ticket_restriction', + type: 'options', + options: [ + { + name: 'Organization', + value: 'organization', + }, + { + name: 'Groups', + value: 'groups', + }, + { + name: 'Assigned', + value: 'assigned', + }, + { + name: 'Requested', + value: 'requested', + }, + ], + default: '', + description: `Specifies which tickets the user has access to`, + }, + { + displayName: 'Timezone', + name: 'time_zone', + type: 'string', + default: '', + description: `The user's time zone.`, + }, + { + displayName: 'User Fields', + name: 'userFieldsUi', + placeholder: 'Add User Field', + description: `Values of custom fields in the user's profile.`, + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: {}, + options: [ + { + name: 'userFieldValues', + displayName: 'Field', + values: [ + { + displayName: 'Field', + name: 'field', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUserFields', + }, + default: '', + description: 'Name of the field to sort on.', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'Value of the field.', + }, + ], + }, + ], + }, + { + displayName: 'Verified', + name: 'verified', + type: 'boolean', + default: false, + description: `The user's primary identity is verified or not`, + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* user:get */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'User ID', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'get', + ], + }, + }, + description: 'User ID', + }, +/* -------------------------------------------------------------------------- */ +/* user:getAll */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 100, + description: 'How many results to return.', + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Filter', + default: {}, + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Roles', + name: 'role', + type: 'multiOptions', + options: [ + { + name: 'End User', + value: 'end-user', + }, + { + name: 'Agent', + value: 'agent', + }, + { + name: 'Admin', + value: 'admin', + }, + ], + default: [], + }, + ], + }, + +/* -------------------------------------------------------------------------- */ +/* user:delete */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'User ID', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'delete', + ], + }, + }, + description: 'User ID', + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Zendesk/Zendesk.node.ts b/packages/nodes-base/nodes/Zendesk/Zendesk.node.ts index d0c72c335f..e349d58cbe 100644 --- a/packages/nodes-base/nodes/Zendesk/Zendesk.node.ts +++ b/packages/nodes-base/nodes/Zendesk/Zendesk.node.ts @@ -27,11 +27,15 @@ import { ticketFieldOperations } from './TicketFieldDescription'; +import { + userFields, + userOperations +} from './UserDescription'; + import { ITicket, IComment, } from './TicketInterface'; -import { response } from 'express'; export class Zendesk implements INodeType { description: INodeTypeDescription = { @@ -105,6 +109,11 @@ export class Zendesk implements INodeType { value: 'ticketField', description: 'Manage system and custom ticket fields', }, + { + name: 'User', + value: 'user', + description: 'Manage users', + }, ], default: 'ticket', description: 'Resource to consume.', @@ -112,9 +121,12 @@ export class Zendesk implements INodeType { // TICKET ...ticketOperations, ...ticketFields, - // TICKET FIELDS + // TICKET FIELD ...ticketFieldOperations, ...ticketFieldFields, + // USER + ...userOperations, + ...userFields, ], }; @@ -177,6 +189,38 @@ export class Zendesk implements INodeType { } return returnData; }, + + // Get all the locales to display them to user so that he can + // select them easily + async getLocales(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const locales = await zendeskApiRequestAllItems.call(this, 'locales', 'GET', '/locales'); + for (const locale of locales) { + const localeName = `${locale.locale} - ${locale.name}`; + const localeId = locale.locale; + returnData.push({ + name: localeName, + value: localeId, + }); + } + return returnData; + }, + + // Get all the user fields to display them to user so that he can + // select them easily + async getUserFields(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const fields = await zendeskApiRequestAllItems.call(this, 'user_fields', 'GET', '/user_fields'); + for (const field of fields) { + const fieldName = field.title; + const fieldId = field.key; + returnData.push({ + name: fieldName, + value: fieldId, + }); + } + return returnData; + }, } }; @@ -359,6 +403,87 @@ export class Zendesk implements INodeType { } } } + //https://developer.zendesk.com/rest_api/docs/support/users + if (resource === 'user') { + //https://developer.zendesk.com/rest_api/docs/support/users#create-user + if (operation === 'create') { + const name = this.getNodeParameter('name', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + const body: IDataObject = { + name, + }; + + Object.assign(body, additionalFields); + + if (body.userFieldsUi) { + const userFields = (body.userFieldsUi as IDataObject).userFieldValues as IDataObject[]; + if (userFields) { + body.user_fields = {}; + for (const userField of userFields) { + //@ts-ignore + body.user_fields[userField.field] = userField.value; + } + delete body.userFieldsUi; + } + } + + responseData = await zendeskApiRequest.call(this, 'POST', '/users', { user: body }); + responseData = responseData.user; + } + //https://developer.zendesk.com/rest_api/docs/support/tickets#update-ticket + if (operation === 'update') { + const userId = this.getNodeParameter('id', i) as string; + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + + const body: IDataObject = {}; + + Object.assign(body, updateFields); + + if (body.userFieldsUi) { + const userFields = (body.userFieldsUi as IDataObject).userFieldValues as IDataObject[]; + if (userFields) { + body.user_fields = {}; + for (const userField of userFields) { + //@ts-ignore + body.user_fields[userField.field] = userField.value; + } + delete body.userFieldsUi; + } + } + + responseData = await zendeskApiRequest.call(this, 'PUT', `/users/${userId}`, { user: body }); + responseData = responseData.user; + } + //https://developer.zendesk.com/rest_api/docs/support/users#show-user + if (operation === 'get') { + const userId = this.getNodeParameter('id', i) as string; + responseData = await zendeskApiRequest.call(this, 'GET', `/users/${userId}`, {}); + responseData = responseData.user; + } + //https://developer.zendesk.com/rest_api/docs/support/users#list-users + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const options = this.getNodeParameter('filters', i) as IDataObject; + + Object.assign(qs, options); + + if (returnAll) { + responseData = await zendeskApiRequestAllItems.call(this, 'users', 'GET', `/users`, {}, qs); + } else { + const limit = this.getNodeParameter('limit', i) as number; + qs.per_page = limit; + responseData = await zendeskApiRequest.call(this, 'GET', `/users`, {}, qs); + responseData = responseData.users; + } + } + //https://developer.zendesk.com/rest_api/docs/support/users#delete-user + if (operation === 'delete') { + const userId = this.getNodeParameter('id', i) as string; + responseData = await zendeskApiRequest.call(this, 'DELETE', `/users/${userId}`, {}); + responseData = responseData.user; + } + } if (Array.isArray(responseData)) { returnData.push.apply(returnData, responseData as IDataObject[]); } else {