diff --git a/packages/nodes-base/nodes/Github/GenericFunctions.ts b/packages/nodes-base/nodes/Github/GenericFunctions.ts index 3a12a86164..d25f4e588b 100644 --- a/packages/nodes-base/nodes/Github/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Github/GenericFunctions.ts @@ -2,7 +2,7 @@ import type { OptionsWithUri } from 'request'; import type { IExecuteFunctions, IHookFunctions } from 'n8n-core'; -import type { IDataObject } from 'n8n-workflow'; +import type { IDataObject, ILoadOptionsFunctions } from 'n8n-workflow'; import { NodeApiError, NodeOperationError } from 'n8n-workflow'; /** @@ -10,7 +10,7 @@ import { NodeApiError, NodeOperationError } from 'n8n-workflow'; * */ export async function githubApiRequest( - this: IHookFunctions | IExecuteFunctions, + this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, method: string, endpoint: string, body: object, diff --git a/packages/nodes-base/nodes/Github/GithubTrigger.node.ts b/packages/nodes-base/nodes/Github/GithubTrigger.node.ts index d3e23f1de9..518f2d0d87 100644 --- a/packages/nodes-base/nodes/Github/GithubTrigger.node.ts +++ b/packages/nodes-base/nodes/Github/GithubTrigger.node.ts @@ -9,6 +9,7 @@ import type { import { NodeApiError, NodeOperationError } from 'n8n-workflow'; import { githubApiRequest } from './GenericFunctions'; +import { getRepositories, getUsers } from './SearchFunctions'; export class GithubTrigger implements INodeType { description: INodeTypeDescription = { @@ -73,20 +74,111 @@ export class GithubTrigger implements INodeType { { displayName: 'Repository Owner', name: 'owner', - type: 'string', - default: '', + type: 'resourceLocator', + default: { mode: 'list', value: '' }, required: true, - placeholder: 'n8n-io', - description: 'Owner of the repsitory', + modes: [ + { + displayName: 'Repository Owner', + name: 'list', + type: 'list', + placeholder: 'Select an owner...', + typeOptions: { + searchListMethod: 'getUsers', + searchable: true, + searchFilterRequired: true, + }, + }, + { + displayName: 'Link', + name: 'url', + type: 'string', + placeholder: 'e.g. https://github.com/n8n-io', + extractValue: { + type: 'regex', + regex: 'https:\\/\\/github.com\\/([-_0-9a-zA-Z]+)', + }, + validation: [ + { + type: 'regex', + properties: { + regex: 'https:\\/\\/github.com\\/([-_0-9a-zA-Z]+)(?:.*)', + errorMessage: 'Not a valid Github URL', + }, + }, + ], + }, + { + displayName: 'By Name', + name: 'name', + type: 'string', + placeholder: 'e.g. n8n-io', + validation: [ + { + type: 'regex', + properties: { + regex: '[-_a-zA-Z0-9]+', + errorMessage: 'Not a valid Github Owner Name', + }, + }, + ], + url: '=https://github.com/{{$value}}', + }, + ], }, { displayName: 'Repository Name', name: 'repository', - type: 'string', - default: '', + type: 'resourceLocator', + default: { mode: 'list', value: '' }, required: true, - placeholder: 'n8n', - description: 'The name of the repsitory', + modes: [ + { + displayName: 'Repository Name', + name: 'list', + type: 'list', + placeholder: 'Select an Repository...', + typeOptions: { + searchListMethod: 'getRepositories', + searchable: true, + }, + }, + { + displayName: 'Link', + name: 'url', + type: 'string', + placeholder: 'e.g. https://github.com/n8n-io/n8n', + extractValue: { + type: 'regex', + regex: 'https:\\/\\/github.com\\/(?:[-_0-9a-zA-Z]+)\\/([-_.0-9a-zA-Z]+)', + }, + validation: [ + { + type: 'regex', + properties: { + regex: 'https:\\/\\/github.com\\/(?:[-_0-9a-zA-Z]+)\\/([-_.0-9a-zA-Z]+)(?:.*)', + errorMessage: 'Not a valid Github Repository URL', + }, + }, + ], + }, + { + displayName: 'By Name', + name: 'name', + type: 'string', + placeholder: 'e.g. n8n', + validation: [ + { + type: 'regex', + properties: { + regex: '[-_.0-9a-zA-Z]+', + errorMessage: 'Not a valid Github Repository Name', + }, + }, + ], + url: '=https://github.com/{{$parameter["owner"]}}/{{$value}}', + }, + ], }, { displayName: 'Events', @@ -343,7 +435,6 @@ export class GithubTrigger implements INodeType { ], }; - // @ts-ignore (because of request) webhookMethods = { default: { async checkExists(this: IHookFunctions): Promise { @@ -355,8 +446,10 @@ export class GithubTrigger implements INodeType { } // Webhook got created before so check if it still exists - const owner = this.getNodeParameter('owner') as string; - const repository = this.getNodeParameter('repository') as string; + const owner = this.getNodeParameter('owner', '', { extractValue: true }) as string; + const repository = this.getNodeParameter('repository', '', { + extractValue: true, + }) as string; const endpoint = `/repos/${owner}/${repository}/hooks/${webhookData.webhookId}`; try { @@ -373,7 +466,6 @@ export class GithubTrigger implements INodeType { // Some error occured throw error; } - // If it did not error then the webhook exists return true; }, @@ -387,8 +479,10 @@ export class GithubTrigger implements INodeType { ); } - const owner = this.getNodeParameter('owner') as string; - const repository = this.getNodeParameter('repository') as string; + const owner = this.getNodeParameter('owner', '', { extractValue: true }) as string; + const repository = this.getNodeParameter('repository', '', { + extractValue: true, + }) as string; const events = this.getNodeParameter('events', []); const endpoint = `/repos/${owner}/${repository}/hooks`; @@ -455,8 +549,10 @@ export class GithubTrigger implements INodeType { const webhookData = this.getWorkflowStaticData('node'); if (webhookData.webhookId !== undefined) { - const owner = this.getNodeParameter('owner') as string; - const repository = this.getNodeParameter('repository') as string; + const owner = this.getNodeParameter('owner', '', { extractValue: true }) as string; + const repository = this.getNodeParameter('repository', '', { + extractValue: true, + }) as string; const endpoint = `/repos/${owner}/${repository}/hooks/${webhookData.webhookId}`; const body = {}; @@ -477,6 +573,13 @@ export class GithubTrigger implements INodeType { }, }; + methods = { + listSearch: { + getUsers, + getRepositories, + }, + }; + async webhook(this: IWebhookFunctions): Promise { const bodyData = this.getBodyData(); diff --git a/packages/nodes-base/nodes/Github/SearchFunctions.ts b/packages/nodes-base/nodes/Github/SearchFunctions.ts new file mode 100644 index 0000000000..79da3bd05b --- /dev/null +++ b/packages/nodes-base/nodes/Github/SearchFunctions.ts @@ -0,0 +1,87 @@ +import type { + ILoadOptionsFunctions, + INodeListSearchItems, + INodeListSearchResult, +} from 'n8n-workflow'; +import { githubApiRequest } from './GenericFunctions'; + +type UserSearchItem = { + login: string; + html_url: string; +}; + +type RepositorySearchItem = { + name: string; + html_url: string; +}; + +type UserSearchResponse = { + items: UserSearchItem[]; + total_count: number; +}; + +type RepositorySearchResponse = { + items: RepositorySearchItem[]; + total_count: number; +}; + +export async function getUsers( + this: ILoadOptionsFunctions, + filter?: string, + paginationToken?: string, +): Promise { + const page = paginationToken ? +paginationToken : 1; + const per_page = 100; + const responseData: UserSearchResponse = await githubApiRequest.call( + this, + 'GET', + '/search/users', + {}, + { q: filter, page, per_page }, + ); + + const results: INodeListSearchItems[] = responseData.items.map((item: UserSearchItem) => ({ + name: item.login, + value: item.login, + url: item.html_url, + })); + + const nextPaginationToken = page * per_page < responseData.total_count ? page + 1 : undefined; + return { results, paginationToken: nextPaginationToken }; +} + +export async function getRepositories( + this: ILoadOptionsFunctions, + filter?: string, + paginationToken?: string, +): Promise { + const owner = this.getCurrentNodeParameter('owner', { extractValue: true }); + const page = paginationToken ? +paginationToken : 1; + const per_page = 100; + const q = `${filter ?? ''} user:${owner} fork:true`; + let responseData: RepositorySearchResponse = { + items: [], + total_count: 0, + }; + + try { + responseData = await githubApiRequest.call( + this, + 'GET', + '/search/repositories', + {}, + { q, page, per_page }, + ); + } catch (_error) { + // will fail if the owner does not have any repositories + } + + const results: INodeListSearchItems[] = responseData.items.map((item: RepositorySearchItem) => ({ + name: item.name, + value: item.name, + url: item.html_url, + })); + + const nextPaginationToken = page * per_page < responseData.total_count ? page + 1 : undefined; + return { results, paginationToken: nextPaginationToken }; +}