From cd795a55d3b1b7f27f0fa397e7dea71bdbed49c0 Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Tue, 18 Oct 2022 00:01:42 +0200 Subject: [PATCH 01/13] feat: add parse-header-link helper --- .../backend/src/helpers/parse-header-link.ts | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 packages/backend/src/helpers/parse-header-link.ts diff --git a/packages/backend/src/helpers/parse-header-link.ts b/packages/backend/src/helpers/parse-header-link.ts new file mode 100644 index 00000000..61e99695 --- /dev/null +++ b/packages/backend/src/helpers/parse-header-link.ts @@ -0,0 +1,48 @@ +type TParameters = { + [key: string]: string; + rel?: string; +}; + +type TReference = { + uri: string; + parameters: TParameters; +}; + +type TRel = 'next' | 'prev' | 'first' | 'last'; + +type TParsedLinkHeader = { + next?: TReference; + prev?: TReference; + first?: TReference; + last?: TReference; +}; + +export default function parseLinkHeader(link: string): TParsedLinkHeader { + const parsed: TParsedLinkHeader = {}; + + if (!link) return parsed; + + const items = link.split(','); + + for (const item of items) { + const [rawUriReference, ...rawLinkParameters] = item.split(';') as [string, ...string[]]; + const trimmedUriReference = rawUriReference.trim(); + + const reference = trimmedUriReference.slice(1, -1); + const parameters: TParameters = {}; + + for (const rawParameter of rawLinkParameters) { + const trimmedRawParameter = rawParameter.trim(); + const [key, value] = trimmedRawParameter.split('='); + + parameters[key.trim()] = value.slice(1, -1); + } + + parsed[parameters.rel as TRel] = { + uri: reference, + parameters, + }; + } + + return parsed; +} \ No newline at end of file From 0b8b5aeebd9655daf446b219fa21fbce00ac2112 Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Tue, 18 Oct 2022 20:55:07 +0200 Subject: [PATCH 02/13] feat(github): add paginate-all utility --- .../src/apps/github/common/paginate-all.ts | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 packages/backend/src/apps/github/common/paginate-all.ts diff --git a/packages/backend/src/apps/github/common/paginate-all.ts b/packages/backend/src/apps/github/common/paginate-all.ts new file mode 100644 index 00000000..78a01de9 --- /dev/null +++ b/packages/backend/src/apps/github/common/paginate-all.ts @@ -0,0 +1,36 @@ +import { IGlobalVariable, IJSONObject } from "@automatisch/types"; +import type { AxiosResponse } from 'axios'; +import parseLinkHeader from '../../../helpers/parse-header-link'; + +type TResponse = { + data: IJSONObject[], + error?: IJSONObject, +} + +export default async function paginateAll($: IGlobalVariable, request: Promise) { + const response = await request; + const aggregatedResponse: TResponse = { + data: [...response.data], + }; + + let links = parseLinkHeader(response.headers.link); + + while (links.next) { + const nextPageResponse = await $.http.request({ + ...response.config, + url: links.next.uri, + }); + + if (nextPageResponse.integrationError) { + aggregatedResponse.error = nextPageResponse.integrationError; + + links = null; + } else { + aggregatedResponse.data.push(...nextPageResponse.data); + + links = parseLinkHeader(nextPageResponse.headers.link); + } + } + + return aggregatedResponse; +} From 51059b0f3963b164d390a698395d045c6dfb50fc Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Tue, 18 Oct 2022 20:55:34 +0200 Subject: [PATCH 03/13] refactor(github): export default in get-repo-owner-and-repo --- .../backend/src/apps/github/common/get-repo-owner-and-repo.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/apps/github/common/get-repo-owner-and-repo.ts b/packages/backend/src/apps/github/common/get-repo-owner-and-repo.ts index ccd89667..fcaec1f1 100644 --- a/packages/backend/src/apps/github/common/get-repo-owner-and-repo.ts +++ b/packages/backend/src/apps/github/common/get-repo-owner-and-repo.ts @@ -3,7 +3,7 @@ type TRepoOwnerAndRepo = { repo: string; } -export function getRepoOwnerAndRepo(repoFullName: string): TRepoOwnerAndRepo { +export default function getRepoOwnerAndRepo(repoFullName: string): TRepoOwnerAndRepo { const [repoOwner, repo] = repoFullName.split('/'); return { From 12e73d59d5020ce438a0349a7028748457379f22 Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Tue, 18 Oct 2022 20:55:51 +0200 Subject: [PATCH 04/13] feat(github): add list-repos dynamic data --- .../src/apps/github/data/list-repos/index.ts | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 packages/backend/src/apps/github/data/list-repos/index.ts diff --git a/packages/backend/src/apps/github/data/list-repos/index.ts b/packages/backend/src/apps/github/data/list-repos/index.ts new file mode 100644 index 00000000..5330f22d --- /dev/null +++ b/packages/backend/src/apps/github/data/list-repos/index.ts @@ -0,0 +1,21 @@ +import { IGlobalVariable } from '@automatisch/types'; +import paginateAll from '../../common/paginate-all'; + +export default { + name: 'List repos', + key: 'listRepos', + + async run($: IGlobalVariable) { + const firstPageRequest = $.http.get('/user/repos'); + const response = await paginateAll($, firstPageRequest); + + response.data = response.data.map((repo: { full_name: string }) => { + return { + value: repo.full_name, + name: repo.full_name, + }; + }); + + return response; + }, +}; From eaf3c1ecfd85bb08e51fe8861a4138e1e19d99a3 Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Tue, 18 Oct 2022 22:09:37 +0200 Subject: [PATCH 05/13] feat(github): add list-labels dynamic data --- .../src/apps/github/data/list-labels/index.ts | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 packages/backend/src/apps/github/data/list-labels/index.ts diff --git a/packages/backend/src/apps/github/data/list-labels/index.ts b/packages/backend/src/apps/github/data/list-labels/index.ts new file mode 100644 index 00000000..2f6974c5 --- /dev/null +++ b/packages/backend/src/apps/github/data/list-labels/index.ts @@ -0,0 +1,26 @@ +import { IGlobalVariable } from '@automatisch/types'; +import getRepoOwnerAndRepo from '../../common/get-repo-owner-and-repo'; +import paginateAll from '../../common/paginate-all'; + +export default { + name: 'List labels', + key: 'listLabels', + + async run($: IGlobalVariable) { + const { + repoOwner, + repo, + } = getRepoOwnerAndRepo($.step.parameters.repo as string); + const firstPageRequest = $.http.get(`/repos/${repoOwner}/${repo}/labels`); + const response = await paginateAll($, firstPageRequest); + + response.data = response.data.map((repo: { name: string }) => { + return { + value: repo.name, + name: repo.name, + }; + }); + + return response; + }, +}; From 1458003536484297376325d051b769a0254e04f7 Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Tue, 18 Oct 2022 22:09:53 +0200 Subject: [PATCH 06/13] feat(github): add new-issues trigger --- .../apps/github/triggers/new-issues/index.ts | 100 ++++++++++++++++++ .../github/triggers/new-issues/new-issues.ts | 64 +++++++++++ 2 files changed, 164 insertions(+) create mode 100644 packages/backend/src/apps/github/triggers/new-issues/index.ts create mode 100644 packages/backend/src/apps/github/triggers/new-issues/new-issues.ts diff --git a/packages/backend/src/apps/github/triggers/new-issues/index.ts b/packages/backend/src/apps/github/triggers/new-issues/index.ts new file mode 100644 index 00000000..e66dd4fc --- /dev/null +++ b/packages/backend/src/apps/github/triggers/new-issues/index.ts @@ -0,0 +1,100 @@ +import defineTrigger from '../../../../helpers/define-trigger'; +import newIssues from './new-issues'; + +export default defineTrigger({ + name: 'New issue', + key: 'newIssues', + pollInterval: 15, + description: 'Triggers when a new issue is created', + substeps: [ + { + key: 'chooseConnection', + name: 'Choose connection' + }, + { + key: 'chooseTrigger', + name: 'Set up a trigger', + arguments: [ + { + label: 'Repo', + key: 'repo', + type: 'dropdown', + required: false, + variables: false, + source: { + type: 'query', + name: 'getData', + arguments: [ + { + name: 'key', + value: 'listRepos' + } + ] + } + }, + { + label: 'Which types of issues should this trigger on?', + key: 'issueType', + type: 'dropdown', + description: 'Defaults to any issue you can see.', + required: true, + variables: false, + value: 'all', + options: [ + { + label: 'Any issue you can see', + value: 'all' + }, + { + label: 'Only issues assigned to you', + value: 'assigned' + }, + { + label: 'Only issues created by you', + value: 'created' + }, + { + label: `Only issues you're mentioned in`, + value: 'mentioned' + }, + { + label: `Only issues you're subscribed to`, + value: 'subscribed' + } + ] + }, + { + label: 'Label', + key: 'label', + type: 'dropdown', + description: 'Only trigger on issues when this label is added.', + required: false, + variables: false, + dependsOn: ['parameters.repo'], + source: { + type: 'query', + name: 'getData', + arguments: [ + { + name: 'key', + value: 'listLabels' + }, + { + name: 'parameters.repo', + value: '{parameters.repo}' + } + ] + } + } + ] + }, + { + key: 'testStep', + name: 'Test trigger' + } + ], + + async run($) { + return await newIssues($); + }, +}); diff --git a/packages/backend/src/apps/github/triggers/new-issues/new-issues.ts b/packages/backend/src/apps/github/triggers/new-issues/new-issues.ts new file mode 100644 index 00000000..712571f0 --- /dev/null +++ b/packages/backend/src/apps/github/triggers/new-issues/new-issues.ts @@ -0,0 +1,64 @@ +import { + IGlobalVariable, + ITriggerOutput, +} from '@automatisch/types'; +import getRepoOwnerAndRepo from '../../common/get-repo-owner-and-repo'; +import parseLinkHeader from '../../../../helpers/parse-header-link'; + +function getPathname($: IGlobalVariable) { + const { repoOwner, repo } = getRepoOwnerAndRepo($.step.parameters.repo as string); + + if (repoOwner && repo) { + return `/repos/${repoOwner}/${repo}/issues`; + } + + return '/issues'; +} + +const newIssues = async ($: IGlobalVariable) => { + const pathname = getPathname($); + const params = { + labels: $.step.parameters.label, + filter: 'all', + state: 'all', + sort: 'created', + direction: 'desc', + per_page: 100, + }; + + const issues: ITriggerOutput = { + data: [], + }; + + let links; + do { + const response = await $.http.get(pathname, { params }); + links = parseLinkHeader(response.headers.link); + + if (response.integrationError) { + issues.error = response.integrationError; + return issues; + } + + if (response.data.length) { + for (const issue of response.data) { + const issueId = issue.id.toString(); + + if (issueId <= $.flow.lastInternalId && !$.execution.testRun) return issues; + + const dataItem = { + raw: issue, + meta: { + internalId: issueId, + }, + }; + + issues.data.push(dataItem); + } + } + } while (links.next && !$.execution.testRun); + + return issues; +}; + +export default newIssues; From 3440b89b04c6a93f67e10567097f9c33609b2925 Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Wed, 19 Oct 2022 19:42:44 +0200 Subject: [PATCH 07/13] fix: use runtime parameters in get-data query --- packages/backend/src/graphql/queries/get-data.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/backend/src/graphql/queries/get-data.ts b/packages/backend/src/graphql/queries/get-data.ts index e8ea2acd..3a644623 100644 --- a/packages/backend/src/graphql/queries/get-data.ts +++ b/packages/backend/src/graphql/queries/get-data.ts @@ -29,6 +29,11 @@ const getData = async (_parent: unknown, params: Params, context: Context) => { const command = app.data.find((data: IData) => data.key === params.key); + for (const parameterKey in params.parameters) { + const parameterValue = params.parameters[parameterKey]; + $.step.parameters[parameterKey] = parameterValue; + } + const fetchedData = await command.run($); if (fetchedData.error) { From 4670e2fe0c0d3470ea9c9c951650f7ddeb77062c Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Wed, 19 Oct 2022 19:59:10 +0200 Subject: [PATCH 08/13] fix: reset field when its deps are not satisfied --- .../ControlledAutocomplete/index.tsx | 20 ++++++++++++++++++- .../web/src/components/InputCreator/index.tsx | 2 ++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/web/src/components/ControlledAutocomplete/index.tsx b/packages/web/src/components/ControlledAutocomplete/index.tsx index f7ab33f6..a79b2f7f 100644 --- a/packages/web/src/components/ControlledAutocomplete/index.tsx +++ b/packages/web/src/components/ControlledAutocomplete/index.tsx @@ -9,12 +9,13 @@ interface ControlledAutocompleteProps extends AutocompleteProps options.find(option => option.value === value) || null; function ControlledAutocomplete(props: ControlledAutocompleteProps): React.ReactElement { - const { control } = useFormContext(); + const { control, watch, setValue, resetField } = useFormContext(); const { required = false, @@ -25,9 +26,26 @@ function ControlledAutocomplete(props: ControlledAutocompleteProps): React.React onChange, description, options = [], + dependsOn = [], ...autocompleteProps } = props; + let dependsOnValues: unknown[] = []; + if (dependsOn?.length) { + dependsOnValues = watch(dependsOn); + } + + React.useEffect(() => { + const hasDependencies = dependsOnValues.length; + const allDepsSatisfied = dependsOnValues.every(Boolean); + + if (hasDependencies && !allDepsSatisfied) { + // Reset the field if any dependency is not satisfied + setValue(name, null); + resetField(name); + } + }, dependsOnValues); + return ( Date: Wed, 19 Oct 2022 19:59:58 +0200 Subject: [PATCH 09/13] fix(github): return empty in list labels without repo --- packages/backend/src/apps/github/data/list-labels/index.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/backend/src/apps/github/data/list-labels/index.ts b/packages/backend/src/apps/github/data/list-labels/index.ts index 2f6974c5..9cb71280 100644 --- a/packages/backend/src/apps/github/data/list-labels/index.ts +++ b/packages/backend/src/apps/github/data/list-labels/index.ts @@ -11,6 +11,9 @@ export default { repoOwner, repo, } = getRepoOwnerAndRepo($.step.parameters.repo as string); + + if (!repo) return { data: [] }; + const firstPageRequest = $.http.get(`/repos/${repoOwner}/${repo}/labels`); const response = await paginateAll($, firstPageRequest); From 859b3e2db86ab1574e9a604ae313ffff4f83f8ad Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Wed, 19 Oct 2022 20:31:02 +0200 Subject: [PATCH 10/13] fix(trigger): create empty execution step if needed --- packages/backend/src/services/trigger.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/services/trigger.ts b/packages/backend/src/services/trigger.ts index 5bf92185..265f2bb6 100644 --- a/packages/backend/src/services/trigger.ts +++ b/packages/backend/src/services/trigger.ts @@ -38,7 +38,7 @@ export const processTrigger = async (options: ProcessTriggerOptions) => { stepId: $.step.id, status: error ? 'failure' : 'success', dataIn: $.step.parameters, - dataOut: !error ? triggerDataItem.raw : null, + dataOut: !error ? triggerDataItem?.raw : null, errorDetails: error, }); From f3f4ea5b601d519f6697efcae305ddafd852c6fc Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Wed, 19 Oct 2022 20:31:27 +0200 Subject: [PATCH 11/13] fix(action): create empty execution step if needed --- packages/backend/src/services/action.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/services/action.ts b/packages/backend/src/services/action.ts index e05948cf..b4c3f66c 100644 --- a/packages/backend/src/services/action.ts +++ b/packages/backend/src/services/action.ts @@ -47,7 +47,7 @@ export const processAction = async (options: ProcessActionOptions) => { stepId: $.step.id, status: actionOutput.error ? 'failure' : 'success', dataIn: computedParameters, - dataOut: actionOutput.error ? null : actionOutput.data.raw, + dataOut: actionOutput.error ? null : actionOutput.data?.raw, errorDetails: actionOutput.error ? actionOutput.error : null, }); From 91ec19c7df82e5b9f97654eec80253407b277acc Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Wed, 19 Oct 2022 20:32:08 +0200 Subject: [PATCH 12/13] fix(useDynamicData): throw error when dependency is undefined --- packages/web/src/hooks/useDynamicData.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web/src/hooks/useDynamicData.ts b/packages/web/src/hooks/useDynamicData.ts index 15839bac..6d4d3ac2 100644 --- a/packages/web/src/hooks/useDynamicData.ts +++ b/packages/web/src/hooks/useDynamicData.ts @@ -20,7 +20,7 @@ function computeArguments(args: IFieldDropdownSource["arguments"], getValues: Us const sanitizedFieldPath = value.replace(/{|}/g, ''); const computedValue = getValues(sanitizedFieldPath); - if (!computedValue) throw new Error(`The ${sanitizedFieldPath} field is required.`); + if (computedValue === undefined) throw new Error(`The ${sanitizedFieldPath} field is required.`); set(result, name, computedValue); From 486e817a40a9cd33099c57582577e7ec68f89a79 Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Wed, 19 Oct 2022 20:32:42 +0200 Subject: [PATCH 13/13] fix(github): return empty without repo in getRepoOwnerAndRepo helper --- .../src/apps/github/common/get-repo-owner-and-repo.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/apps/github/common/get-repo-owner-and-repo.ts b/packages/backend/src/apps/github/common/get-repo-owner-and-repo.ts index fcaec1f1..25891c40 100644 --- a/packages/backend/src/apps/github/common/get-repo-owner-and-repo.ts +++ b/packages/backend/src/apps/github/common/get-repo-owner-and-repo.ts @@ -1,9 +1,11 @@ type TRepoOwnerAndRepo = { - repoOwner: string; - repo: string; + repoOwner?: string; + repo?: string; } export default function getRepoOwnerAndRepo(repoFullName: string): TRepoOwnerAndRepo { + if (!repoFullName) return {}; + const [repoOwner, repo] = repoFullName.split('/'); return {