style: auto format whole project

This commit is contained in:
Ali BARIN
2022-11-05 23:57:33 +01:00
parent e338770e57
commit 475f24f661
199 changed files with 2421 additions and 1839 deletions

View File

@@ -1,11 +1,11 @@
version: "3.9"
version: '3.9'
services:
main:
build:
context: ../images/wait-for-postgres
network: host
ports:
- "3000:3000"
- '3000:3000'
depends_on:
- postgres
- redis
@@ -36,12 +36,12 @@ services:
volumes:
- automatisch_storage:/automatisch/storage
postgres:
image: "postgres:14.5"
image: 'postgres:14.5'
environment:
POSTGRES_HOST_AUTH_METHOD: trust
POSTGRES_DB: automatisch
POSTGRES_USER: automatisch_user
redis:
image: "redis:7.0.4"
image: 'redis:7.0.4'
volumes:
automatisch_storage:

View File

@@ -1,7 +1,5 @@
{
"packages": [
"packages/*"
],
"packages": ["packages/*"],
"version": "0.1.5",
"npmClient": "yarn",
"useWorkspaces": true,

View File

@@ -4,6 +4,6 @@ const client = new Client({
host: 'localhost',
user: 'postgres',
port: 5432,
})
});
export default client;

View File

@@ -4,7 +4,10 @@ import client from './client';
import User from '../../src/models/user';
import '../../src/config/orm';
export async function createUser(email = 'user@automatisch.io', password = 'sample') {
export async function createUser(
email = 'user@automatisch.io',
password = 'sample'
) {
const UNIQUE_VIOLATION_CODE = '23505';
const userParams = {
email,
@@ -29,14 +32,17 @@ export async function createUser(email = 'user@automatisch.io', password = 'samp
}
}
export const createDatabaseAndUser = async (database = appConfig.postgresDatabase, user = appConfig.postgresUsername) => {
export const createDatabaseAndUser = async (
database = appConfig.postgresDatabase,
user = appConfig.postgresUsername
) => {
await client.connect();
await createDatabase(database);
await createDatabaseUser(user);
await grantPrivileges(database, user);
await client.end();
}
};
export const createDatabase = async (database = appConfig.postgresDatabase) => {
const DUPLICATE_DB_CODE = '42P04';
@@ -51,7 +57,7 @@ export const createDatabase = async (database = appConfig.postgresDatabase) => {
logger.info(`Database: ${database} already exists!`);
}
}
};
export const createDatabaseUser = async (user = appConfig.postgresUsername) => {
const DUPLICATE_OBJECT_CODE = '42710';
@@ -68,25 +74,25 @@ export const createDatabaseUser = async (user = appConfig.postgresUsername) => {
logger.info(`Database User: ${user} already exists!`);
}
}
};
export const grantPrivileges = async (
database = appConfig.postgresDatabase, user = appConfig.postgresUsername
database = appConfig.postgresDatabase,
user = appConfig.postgresUsername
) => {
await client.query(
`GRANT ALL PRIVILEGES ON DATABASE ${database} TO ${user};`
);
logger.info(
`${user} has granted all privileges on ${database}!`
);
}
logger.info(`${user} has granted all privileges on ${database}!`);
};
export const dropDatabase = async () => {
if (appConfig.appEnv != 'development' && appConfig.appEnv != 'test') {
const errorMessage = 'Drop database command can be used only with development or test environments!'
const errorMessage =
'Drop database command can be used only with development or test environments!';
logger.error(errorMessage)
logger.error(errorMessage);
return;
}
@@ -94,13 +100,15 @@ export const dropDatabase = async () => {
await dropDatabaseAndUser();
await client.end();
}
};
export const dropDatabaseAndUser = async(database = appConfig.postgresDatabase, user = appConfig.postgresUsername) => {
export const dropDatabaseAndUser = async (
database = appConfig.postgresDatabase,
user = appConfig.postgresUsername
) => {
await client.query(`DROP DATABASE IF EXISTS ${database}`);
logger.info(`Database: ${database} removed!`);
await client.query(`DROP USER IF EXISTS ${user}`);
logger.info(`Database User: ${user} removed!`);
}
};

View File

@@ -10,7 +10,7 @@ const knexConfig = {
user: appConfig.postgresUsername,
password: appConfig.postgresPassword,
database: appConfig.postgresDatabase,
ssl: appConfig.postgresEnableSsl
ssl: appConfig.postgresEnableSsl,
},
pool: { min: 0, max: 20 },
migrations: {
@@ -20,7 +20,7 @@ const knexConfig = {
},
seeds: {
directory: __dirname + '/src/db/seeds',
}
}
},
};
export default knexConfig;

View File

@@ -1,5 +1,3 @@
import sendMessageToChannel from "./send-message-to-channel";
import sendMessageToChannel from './send-message-to-channel';
export default [
sendMessageToChannel
];
export default [sendMessageToChannel];

View File

@@ -17,12 +17,12 @@ export default async function createAuthData($: IGlobalVariable) {
scope: scopes.join(' '),
});
const url = `${$.app.apiBaseUrl}/oauth2/authorize?${searchParams.toString()}`;
const url = `${
$.app.apiBaseUrl
}/oauth2/authorize?${searchParams.toString()}`;
await $.auth.set({ url });
} catch (error) {
throw new Error(
`Error occured while verifying credentials: ${error}`
);
throw new Error(`Error occured while verifying credentials: ${error}`);
}
}

View File

@@ -12,9 +12,10 @@ export default {
readOnly: true,
value: '{WEB_APP_URL}/app/discord/connections/add',
placeholder: null,
description: 'When asked to input an OAuth callback or redirect URL in Discord OAuth, enter the URL above.',
description:
'When asked to input an OAuth callback or redirect URL in Discord OAuth, enter the URL above.',
docUrl: 'https://automatisch.io/docs/discord#oauth-redirect-url',
clickToCopy: true
clickToCopy: true,
},
{
key: 'consumerKey',
@@ -26,7 +27,7 @@ export default {
placeholder: null,
description: null,
docUrl: 'https://automatisch.io/docs/discord#consumer-key',
clickToCopy: false
clickToCopy: false,
},
{
key: 'consumerSecret',
@@ -38,7 +39,7 @@ export default {
placeholder: null,
description: null,
docUrl: 'https://automatisch.io/docs/discord#consumer-secret',
clickToCopy: false
clickToCopy: false,
},
{
key: 'botToken',
@@ -50,8 +51,8 @@ export default {
placeholder: null,
description: null,
docUrl: 'https://automatisch.io/docs/discord#bot-token',
clickToCopy: false
}
clickToCopy: false,
},
],
authenticationSteps: [
{
@@ -60,7 +61,7 @@ export default {
arguments: [
{
name: 'key',
value: '{key}'
value: '{key}',
},
{
name: 'formattedData',
@@ -68,19 +69,19 @@ export default {
properties: [
{
name: 'consumerKey',
value: '{fields.consumerKey}'
value: '{fields.consumerKey}',
},
{
name: 'consumerSecret',
value: '{fields.consumerSecret}'
value: '{fields.consumerSecret}',
},
{
name: 'botToken',
value: '{fields.botToken}'
}
]
}
]
value: '{fields.botToken}',
},
],
},
],
},
{
type: 'mutation' as const,
@@ -88,9 +89,9 @@ export default {
arguments: [
{
name: 'id',
value: '{createConnection.id}'
}
]
value: '{createConnection.id}',
},
],
},
{
type: 'openWithPopup' as const,
@@ -98,9 +99,9 @@ export default {
arguments: [
{
name: 'url',
value: '{createAuthData.url}'
}
]
value: '{createAuthData.url}',
},
],
},
{
type: 'mutation' as const,
@@ -108,7 +109,7 @@ export default {
arguments: [
{
name: 'id',
value: '{createConnection.id}'
value: '{createConnection.id}',
},
{
name: 'formattedData',
@@ -116,11 +117,11 @@ export default {
properties: [
{
name: 'oauthVerifier',
value: '{openAuthPopup.code}'
}
]
}
]
value: '{openAuthPopup.code}',
},
],
},
],
},
{
type: 'mutation' as const,
@@ -128,10 +129,10 @@ export default {
arguments: [
{
name: 'id',
value: '{createConnection.id}'
}
]
}
value: '{createConnection.id}',
},
],
},
],
createAuthData,

View File

@@ -28,10 +28,7 @@ const verifyCredentials = async ($: IGlobalVariable) => {
expires_in: expiresIn,
scope: scope,
token_type: tokenType,
guild: {
id: guildId,
name: guildName,
}
guild: { id: guildId, name: guildName },
} = verifiedCredentials;
await $.auth.set({
@@ -40,7 +37,7 @@ const verifyCredentials = async ($: IGlobalVariable) => {
expiresIn,
scope,
tokenType,
})
});
const user = await getCurrentUser($);

View File

@@ -1,5 +1,3 @@
import listChannels from "./list-channels";
import listChannels from './list-channels';
export default [
listChannels,
];
export default [listChannels];

View File

@@ -13,7 +13,9 @@ export default {
error: null,
};
const response = await $.http.get(`/guilds/${$.auth.data.guildId}/channels`);
const response = await $.http.get(
`/guilds/${$.auth.data.guildId}/channels`
);
channels.data = response.data
.filter((channel: IJSONObject) => {

View File

@@ -12,9 +12,10 @@ export default {
readOnly: true,
value: '{WEB_APP_URL}/app/flickr/connections/add',
placeholder: null,
description: 'When asked to input an OAuth callback or redirect URL in Flickr OAuth, enter the URL above.',
description:
'When asked to input an OAuth callback or redirect URL in Flickr OAuth, enter the URL above.',
docUrl: 'https://automatisch.io/docs/flickr#oauth-redirect-url',
clickToCopy: true
clickToCopy: true,
},
{
key: 'consumerKey',
@@ -26,7 +27,7 @@ export default {
placeholder: null,
description: null,
docUrl: 'https://automatisch.io/docs/flickr#consumer-key',
clickToCopy: false
clickToCopy: false,
},
{
key: 'consumerSecret',
@@ -38,8 +39,8 @@ export default {
placeholder: null,
description: null,
docUrl: 'https://automatisch.io/docs/flickr#consumer-secret',
clickToCopy: false
}
clickToCopy: false,
},
],
authenticationSteps: [
{
@@ -48,7 +49,7 @@ export default {
arguments: [
{
name: 'key',
value: '{key}'
value: '{key}',
},
{
name: 'formattedData',
@@ -56,15 +57,15 @@ export default {
properties: [
{
name: 'consumerKey',
value: '{fields.consumerKey}'
value: '{fields.consumerKey}',
},
{
name: 'consumerSecret',
value: '{fields.consumerSecret}'
}
]
}
]
value: '{fields.consumerSecret}',
},
],
},
],
},
{
type: 'mutation' as const,
@@ -72,9 +73,9 @@ export default {
arguments: [
{
name: 'id',
value: '{createConnection.id}'
}
]
value: '{createConnection.id}',
},
],
},
{
type: 'openWithPopup' as const,
@@ -82,9 +83,9 @@ export default {
arguments: [
{
name: 'url',
value: '{createAuthData.url}'
}
]
value: '{createAuthData.url}',
},
],
},
{
type: 'mutation' as const,
@@ -92,7 +93,7 @@ export default {
arguments: [
{
name: 'id',
value: '{createConnection.id}'
value: '{createConnection.id}',
},
{
name: 'formattedData',
@@ -100,11 +101,11 @@ export default {
properties: [
{
name: 'oauthVerifier',
value: '{openAuthPopup.oauth_verifier}'
}
]
}
]
value: '{openAuthPopup.oauth_verifier}',
},
],
},
],
},
{
type: 'mutation' as const,
@@ -112,10 +113,10 @@ export default {
arguments: [
{
name: 'id',
value: '{createConnection.id}'
}
]
}
value: '{createConnection.id}',
},
],
},
],
createAuthData,

View File

@@ -6,7 +6,7 @@ const isStillVerified = async ($: IGlobalVariable) => {
method: 'flickr.test.login',
format: 'json',
nojsoncallback: 1,
}
};
const response = await $.http.get('/rest', { params });
return !!response.data.user.id;
} catch (error) {

View File

@@ -10,7 +10,7 @@ type TPhotoset = {
title: {
_content: string;
};
}
};
export default {
name: 'List albums',
@@ -25,7 +25,7 @@ export default {
format: 'json',
nojsoncallback: 1,
};
let response = await $.http.get('/rest', { params, });
let response = await $.http.get('/rest', { params });
const aggregatedResponse: TResponse = {
data: [...response.data.photosets.photoset],
@@ -35,19 +35,21 @@ export default {
response = await $.http.get('/rest', {
params: {
...params,
page: response.data.photosets.page
}
page: response.data.photosets.page,
},
});
aggregatedResponse.data.push(...response.data.photosets.photoset);
}
aggregatedResponse.data = aggregatedResponse.data.map((photoset: TPhotoset) => {
aggregatedResponse.data = aggregatedResponse.data.map(
(photoset: TPhotoset) => {
return {
value: photoset.id,
name: photoset.title._content,
} as IJSONObject;
});
}
);
return aggregatedResponse;
},

View File

@@ -3,9 +3,4 @@ import newFavoritePhotos from './new-favorite-photos';
import newPhotos from './new-photos';
import newPhotosInAlbums from './new-photos-in-album';
export default [
newAlbums,
newFavoritePhotos,
newPhotos,
newPhotosInAlbums,
];
export default [newAlbums, newFavoritePhotos, newPhotos, newPhotosInAlbums];

View File

@@ -13,7 +13,9 @@ export default async function createAuthData($: IGlobalVariable) {
scope: scopes.join(','),
});
const url = `${$.app.baseUrl}/login/oauth/authorize?${searchParams.toString()}`;
const url = `${
$.app.baseUrl
}/login/oauth/authorize?${searchParams.toString()}`;
await $.auth.set({
url,

View File

@@ -12,9 +12,10 @@ export default {
readOnly: true,
value: '{WEB_APP_URL}/app/github/connections/add',
placeholder: null,
description: 'When asked to input an OAuth callback or redirect URL in Github OAuth, enter the URL above.',
description:
'When asked to input an OAuth callback or redirect URL in Github OAuth, enter the URL above.',
docUrl: 'https://automatisch.io/docs/github#oauth-redirect-url',
clickToCopy: true
clickToCopy: true,
},
{
key: 'consumerKey',
@@ -26,7 +27,7 @@ export default {
placeholder: null,
description: null,
docUrl: 'https://automatisch.io/docs/github#client-id',
clickToCopy: false
clickToCopy: false,
},
{
key: 'consumerSecret',
@@ -38,8 +39,8 @@ export default {
placeholder: null,
description: null,
docUrl: 'https://automatisch.io/docs/github#client-secret',
clickToCopy: false
}
clickToCopy: false,
},
],
authenticationSteps: [
{
@@ -48,7 +49,7 @@ export default {
arguments: [
{
name: 'key',
value: '{key}'
value: '{key}',
},
{
name: 'formattedData',
@@ -56,15 +57,15 @@ export default {
properties: [
{
name: 'consumerKey',
value: '{fields.consumerKey}'
value: '{fields.consumerKey}',
},
{
name: 'consumerSecret',
value: '{fields.consumerSecret}'
}
]
}
]
value: '{fields.consumerSecret}',
},
],
},
],
},
{
type: 'mutation' as const,
@@ -72,9 +73,9 @@ export default {
arguments: [
{
name: 'id',
value: '{createConnection.id}'
}
]
value: '{createConnection.id}',
},
],
},
{
type: 'openWithPopup' as const,
@@ -82,9 +83,9 @@ export default {
arguments: [
{
name: 'url',
value: '{createAuthData.url}'
}
]
value: '{createAuthData.url}',
},
],
},
{
type: 'mutation' as const,
@@ -92,7 +93,7 @@ export default {
arguments: [
{
name: 'id',
value: '{createConnection.id}'
value: '{createConnection.id}',
},
{
name: 'formattedData',
@@ -100,11 +101,11 @@ export default {
properties: [
{
name: 'oauthVerifier',
value: '{openAuthPopup.code}'
}
]
}
]
value: '{openAuthPopup.code}',
},
],
},
],
},
{
type: 'mutation' as const,
@@ -112,10 +113,10 @@ export default {
arguments: [
{
name: 'id',
value: '{createConnection.id}'
}
]
}
value: '{createConnection.id}',
},
],
},
],
createAuthData,

View File

@@ -12,9 +12,10 @@ const verifyCredentials = async ($: IGlobalVariable) => {
},
{
headers: {
Accept: 'application/json'
Accept: 'application/json',
},
}
});
);
const data = response.data;

View File

@@ -1,11 +1,11 @@
import { TBeforeRequest } from "@automatisch/types";
import { TBeforeRequest } from '@automatisch/types';
const addAuthHeader: TBeforeRequest = ($, requestConfig) => {
if (requestConfig.headers && $.auth.data?.accessToken) {
requestConfig.headers.Authorization = `Bearer ${$.auth.data.accessToken}`
requestConfig.headers.Authorization = `Bearer ${$.auth.data.accessToken}`;
}
return requestConfig;
}
};
export default addAuthHeader;

View File

@@ -1,15 +1,17 @@
type TRepoOwnerAndRepo = {
repoOwner?: string;
repo?: string;
}
};
export default function getRepoOwnerAndRepo(repoFullName: string): TRepoOwnerAndRepo {
export default function getRepoOwnerAndRepo(
repoFullName: string
): TRepoOwnerAndRepo {
if (!repoFullName) return {};
const [repoOwner, repo] = repoFullName.split('/');
return {
repoOwner,
repo
repo,
};
}

View File

@@ -1,7 +1,4 @@
import listLabels from './list-labels';
import listRepos from './list-repos';
export default [
listLabels,
listRepos,
];
export default [listLabels, listRepos];

View File

@@ -7,10 +7,9 @@ export default {
key: 'listLabels',
async run($: IGlobalVariable) {
const {
repoOwner,
repo,
} = getRepoOwnerAndRepo($.step.parameters.repo as string);
const { repoOwner, repo } = getRepoOwnerAndRepo(
$.step.parameters.repo as string
);
if (!repo) return { data: [] };

View File

@@ -3,9 +3,4 @@ import newPullRequests from './new-pull-requests';
import newStargazers from './new-stargazers';
import newWatchers from './new-watchers';
export default [
newIssues,
newPullRequests,
newStargazers,
newWatchers,
];
export default [newIssues, newPullRequests, newStargazers, newWatchers];

View File

@@ -10,15 +10,13 @@ export default async function createAuthData($: IGlobalVariable) {
const searchParams = qs.stringify({
client_id: $.auth.data.consumerKey as string,
redirect_uri: redirectUri,
response_type: 'code'
})
response_type: 'code',
});
await $.auth.set({
url: `${$.auth.data.oauth2Url}/authorize?${searchParams}`,
});
} catch (error) {
throw new Error(
`Error occured while verifying credentials: ${error}`
);
throw new Error(`Error occured while verifying credentials: ${error}`);
}
}

View File

@@ -34,8 +34,8 @@ export default {
{
label: 'sandbox',
value: 'https://test.salesforce.com/services/oauth2',
}
]
},
],
},
{
key: 'consumerKey',
@@ -76,7 +76,7 @@ export default {
properties: [
{
name: 'oauth2Url',
value: '{fields.oauth2Url}'
value: '{fields.oauth2Url}',
},
{
name: 'consumerKey',

View File

@@ -13,10 +13,10 @@ const verifyCredentials = async ($: IGlobalVariable) => {
grant_type: 'authorization_code',
client_id: $.auth.data.consumerKey as string,
client_secret: $.auth.data.consumerSecret as string,
redirect_uri: redirectUri
redirect_uri: redirectUri,
});
const { data } = await $.http.post(
`${$.auth.data.oauth2Url}/token?${searchParams}`,
`${$.auth.data.oauth2Url}/token?${searchParams}`
);
await $.auth.set({

View File

@@ -1,7 +1,4 @@
import listObjects from './list-objects';
import listFields from './list-fields';
export default [
listObjects,
listFields,
];
export default [listObjects, listFields];

View File

@@ -2,19 +2,21 @@ import { IGlobalVariable } from '@automatisch/types';
type TResponse = {
sobjects: TObject[];
}
};
type TObject = {
name: string;
label: string;
}
};
export default {
name: 'List objects',
key: 'listObjects',
async run($: IGlobalVariable) {
const response = await $.http.get<TResponse>('/services/data/v56.0/sobjects');
const response = await $.http.get<TResponse>(
'/services/data/v56.0/sobjects'
);
const objects = response.data.sobjects.map((object) => {
return {

View File

@@ -1,5 +1,3 @@
import updatedFieldInRecords from "./updated-field-in-records";
import updatedFieldInRecords from './updated-field-in-records';
export default [
updatedFieldInRecords
];
export default [updatedFieldInRecords];

View File

@@ -23,7 +23,7 @@ const updatedFieldInRecord = async ($: IGlobalVariable): Promise<void> => {
const options = {
params: {
q: getQuery(object, limit, offset),
}
},
};
response = await $.http.get('/services/data/v56.0/query', options);
@@ -34,7 +34,7 @@ const updatedFieldInRecord = async ($: IGlobalVariable): Promise<void> => {
raw: record,
meta: {
internalId: `${record.Id}-${record[field]}`,
}
},
});
}

View File

@@ -3,7 +3,8 @@ const cronTimes = {
everyHourExcludingWeekends: '0 * * * 1-5',
everyDayAt: (hour: number) => `0 ${hour} * * *`,
everyDayExcludingWeekendsAt: (hour: number) => `0 ${hour} * * 1-5`,
everyWeekOnAndAt: (weekday: number, hour: number) => `0 ${hour} * * ${weekday}`,
everyWeekOnAndAt: (weekday: number, hour: number) =>
`0 ${hour} * * ${weekday}`,
everyMonthOnAndAt: (day: number, hour: number) => `0 ${hour} ${day} * *`,
};

View File

@@ -4,7 +4,9 @@ import cronParser from 'cron-parser';
export default function getNextCronDateTime(cronString: string) {
const cronDate = cronParser.parseExpression(cronString);
const matchingNextCronDateTime = cronDate.next();
const matchingNextDateTime = DateTime.fromJSDate(matchingNextCronDateTime.toDate());
const matchingNextDateTime = DateTime.fromJSDate(
matchingNextCronDateTime.toDate()
);
return matchingNextDateTime;
};
}

View File

@@ -3,9 +3,4 @@ import everyDay from './every-day';
import everyWeek from './every-week';
import everyMonth from './every-month';
export default [
everyHour,
everyDay,
everyWeek,
everyMonth,
];
export default [everyHour, everyDay, everyWeek, everyMonth];

View File

@@ -1,7 +1,4 @@
import findMessage from './find-message';
import sendMessageToChannel from './send-a-message-to-channel';
export default [
findMessage,
sendMessageToChannel,
];
export default [findMessage, sendMessageToChannel];

View File

@@ -38,7 +38,8 @@ export default defineAction({
type: 'dropdown' as const,
required: false,
value: false,
description: 'If you choose no, this message will appear to come from you. Direct messages are always sent by bots.',
description:
'If you choose no, this message will appear to come from you. Direct messages are always sent by bots.',
variables: false,
options: [
{
@@ -48,8 +49,8 @@ export default defineAction({
{
label: 'No',
value: false,
}
]
},
],
},
{
label: 'Bot name',
@@ -57,7 +58,8 @@ export default defineAction({
type: 'string' as const,
required: true,
value: 'Automatisch',
description: 'Specify the bot name which appears as a bold username above the message inside Slack. Defaults to Automatisch.',
description:
'Specify the bot name which appears as a bold username above the message inside Slack. Defaults to Automatisch.',
variables: true,
},
{
@@ -65,7 +67,8 @@ export default defineAction({
key: 'botIcon',
type: 'string' as const,
required: false,
description: 'Either an image url or an emoji available to your team (surrounded by :). For example, https://example.com/icon_256.png or :robot_face:',
description:
'Either an image url or an emoji available to your team (surrounded by :). For example, https://example.com/icon_256.png or :robot_face:',
variables: true,
},
],

View File

@@ -7,7 +7,7 @@ type TData = {
username?: string;
icon_url?: string;
icon_emoji?: string;
}
};
const postMessage = async ($: IGlobalVariable) => {
const { parameters } = $.step;
@@ -37,11 +37,9 @@ const postMessage = async ($: IGlobalVariable) => {
sendAsBot,
};
const response = await $.http.post(
'/chat.postMessage',
data,
{ additionalProperties: customConfig },
);
const response = await $.http.post('/chat.postMessage', data, {
additionalProperties: customConfig,
});
if (response.data.ok === false) {
throw new Error(JSON.stringify(response.data));

View File

@@ -59,4 +59,4 @@ export default async function createAuthData($: IGlobalVariable) {
await $.auth.set({
url,
});
};
}

View File

@@ -22,14 +22,9 @@ const verifyCredentials = async ($: IGlobalVariable) => {
const {
bot_user_id: botId,
authed_user: {
id: userId,
access_token: userAccessToken,
},
authed_user: { id: userId, access_token: userAccessToken },
access_token: botAccessToken,
team: {
name: teamName,
}
team: { name: teamName },
} = response.data;
await $.auth.set({
@@ -44,7 +39,7 @@ const verifyCredentials = async ($: IGlobalVariable) => {
const currentUser = await getCurrentUser($);
await $.auth.set({
screenName: `${currentUser.real_name} @ ${teamName}`
screenName: `${currentUser.real_name} @ ${teamName}`,
});
};

View File

@@ -3,9 +3,9 @@ import { TBeforeRequest } from '@automatisch/types';
const addAuthHeader: TBeforeRequest = ($, requestConfig) => {
const authData = $.auth.data;
if (
requestConfig.headers
&& authData?.userAccessToken
&& authData?.botAccessToken
requestConfig.headers &&
authData?.userAccessToken &&
authData?.botAccessToken
) {
if (requestConfig.additionalProperties?.sendAsBot) {
requestConfig.headers.Authorization = `Bearer ${authData.botAccessToken}`;
@@ -14,7 +14,8 @@ const addAuthHeader: TBeforeRequest = ($, requestConfig) => {
}
}
requestConfig.headers['Content-Type'] = requestConfig.headers['Content-Type'] || 'application/json; charset=utf-8';
requestConfig.headers['Content-Type'] =
requestConfig.headers['Content-Type'] || 'application/json; charset=utf-8';
return requestConfig;
};

View File

@@ -3,7 +3,7 @@ import { IGlobalVariable, IJSONObject } from '@automatisch/types';
const getCurrentUser = async ($: IGlobalVariable): Promise<IJSONObject> => {
const params = {
user: $.auth.data.userId as string,
}
};
const response = await $.http.get('/users.info', { params });
const currentUser = response.data.user;

View File

@@ -3,9 +3,4 @@ import newFollowerOfMe from './new-follower-of-me';
import searchTweets from './search-tweets';
import userTweets from './user-tweets';
export default [
myTweets,
newFollowerOfMe,
searchTweets,
userTweets,
];
export default [myTweets, newFollowerOfMe, searchTweets, userTweets];

View File

@@ -1,10 +1,10 @@
import appConfig from './app'
import appConfig from './app';
const corsOptions = {
origin: appConfig.webAppUrl,
methods: 'POST',
credentials: true,
optionsSuccessStatus: 200,
}
};
export default corsOptions;

View File

@@ -1,4 +1,4 @@
import { Knex } from "knex";
import { Knex } from 'knex';
export async function up(knex: Knex): Promise<void> {
return knex.schema.createTable('users', (table) => {
@@ -8,10 +8,8 @@ export async function up(knex: Knex): Promise<void> {
table.timestamps(true, true);
});
}
export async function down(knex: Knex): Promise<void> {
return knex.schema.dropTable('users');
}

View File

@@ -1,8 +1,8 @@
import { Knex } from "knex";
import { Knex } from 'knex';
export async function up(knex: Knex): Promise<void> {
return knex.schema.createTable('credentials', (table) => {
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'))
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'));
table.string('key').notNullable();
table.string('display_name').notNullable();
table.text('data').notNullable();
@@ -11,7 +11,7 @@ export async function up(knex: Knex): Promise<void> {
table.timestamps(true, true);
});
};
}
export async function down(knex: Knex): Promise<void> {
return knex.schema.dropTable('credentials');

View File

@@ -1,4 +1,4 @@
import { Knex } from "knex";
import { Knex } from 'knex';
export async function up(knex: Knex): Promise<void> {
return knex.schema.table('credentials', (table) => {

View File

@@ -1,4 +1,4 @@
import { Knex } from "knex";
import { Knex } from 'knex';
export async function up(knex: Knex): Promise<void> {
return knex.schema.renameTable('credentials', 'connections');

View File

@@ -2,7 +2,7 @@ import { Knex } from 'knex';
export async function up(knex: Knex): Promise<void> {
return knex.schema.createTable('steps', (table) => {
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'))
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'));
table.string('key').notNullable();
table.string('app_key').notNullable();
table.string('type').notNullable();

View File

@@ -1,14 +1,14 @@
import { Knex } from "knex";
import { Knex } from 'knex';
export async function up(knex: Knex): Promise<void> {
return knex.schema.createTable('flows', (table) => {
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'))
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'));
table.string('name');
table.uuid('user_id').references('id').inTable('users');
table.timestamps(true, true);
});
};
}
export async function down(knex: Knex): Promise<void> {
return knex.schema.dropTable('flows');

View File

@@ -1,15 +1,11 @@
import { Knex } from "knex";
import { Knex } from 'knex';
export async function up(knex: Knex): Promise<void> {
return knex.schema.table('steps', (table) => {
table
.uuid('flow_id')
.references('id')
.inTable('flows');
table.uuid('flow_id').references('id').inTable('flows');
});
}
export async function down(knex: Knex): Promise<void> {
return knex.schema.table('steps', (table) => {
table.dropColumn('flow_id');

View File

@@ -1,16 +1,15 @@
import { Knex } from "knex";
import { Knex } from 'knex';
export async function up(knex: Knex): Promise<void> {
return knex.schema.alterTable('steps', table => {
return knex.schema.alterTable('steps', (table) => {
table.string('key').nullable().alter();
table.string('app_key').nullable().alter();
})
});
}
export async function down(knex: Knex): Promise<void> {
return knex.schema.alterTable('steps', table => {
return knex.schema.alterTable('steps', (table) => {
table.string('key').notNullable().alter();
table.string('app_key').notNullable().alter();
})
});
}

View File

@@ -1,8 +1,8 @@
import { Knex } from "knex";
import { Knex } from 'knex';
export async function up(knex: Knex): Promise<void> {
return knex.schema.table('flows', (table) => {
table.boolean('active').defaultTo(false)
table.boolean('active').defaultTo(false);
});
}

View File

@@ -2,7 +2,7 @@ import { Knex } from 'knex';
export async function up(knex: Knex): Promise<void> {
return knex.schema.createTable('executions', (table) => {
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'))
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'));
table.uuid('flow_id').references('id').inTable('flows');
table.boolean('test_run').notNullable().defaultTo(false);

View File

@@ -2,7 +2,7 @@ import { Knex } from 'knex';
export async function up(knex: Knex): Promise<void> {
return knex.schema.createTable('execution_steps', (table) => {
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'))
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'));
table.uuid('execution_id').references('id').inTable('executions');
table.uuid('step_id').references('id').inTable('steps');
table.string('status');

View File

@@ -1,4 +1,4 @@
import { Knex } from "knex";
import { Knex } from 'knex';
async function addDeletedColumn(knex: Knex, tableName: string) {
return await knex.schema.table(tableName, (table) => {

View File

@@ -32,13 +32,13 @@ const createFlow = async (
type: 'trigger',
position: 1,
appKey,
connectionId
connectionId,
});
await Step.query().insert({
flowId: flow.id,
type: 'action',
position: 2
position: 2,
});
return flow;

View File

@@ -18,7 +18,9 @@ const resetConnection = async (
})
.throwIfNotFound();
if (!connection.formattedData) { return null; }
if (!connection.formattedData) {
return null;
}
connection = await connection.$query().patchAndFetch({
formattedData: { screenName: connection.formattedData.screenName },

View File

@@ -1,6 +1,10 @@
import Context from '../../types/express/context';
const getCurrentUser = async (_parent: unknown, _params: unknown, context: Context) => {
const getCurrentUser = async (
_parent: unknown,
_params: unknown,
context: Context
) => {
return context.currentUser;
};

View File

@@ -13,8 +13,8 @@ const getExecution = async (
.$relatedQuery('executions')
.withGraphFetched({
flow: {
steps: true
}
steps: true,
},
})
.findById(params.executionId)
.throwIfNotFound();

View File

@@ -13,12 +13,12 @@ const getFlows = async (_parent: unknown, params: Params, context: Context) => {
const flowsQuery = context.currentUser
.$relatedQuery('flows')
.joinRelated({
steps: true
steps: true,
})
.withGraphFetched({
steps: {
connection: true
}
connection: true,
},
})
.where((builder) => {
if (params.connectionId) {

View File

@@ -3,7 +3,7 @@ import appConfig from '../../config/app';
const healthcheck = () => {
return {
version: appConfig.version,
}
};
};
export default healthcheck;

View File

@@ -1,4 +1,8 @@
import { IApp, IAuthenticationStep, IAuthenticationStepField } from '@automatisch/types';
import {
IApp,
IAuthenticationStep,
IAuthenticationStepField,
} from '@automatisch/types';
import cloneDeep from 'lodash/cloneDeep';
const connectionIdArgument = {
@@ -9,16 +13,11 @@ const connectionIdArgument = {
const resetConnectionStep = {
type: 'mutation' as const,
name: 'resetConnection',
arguments: [
connectionIdArgument,
],
arguments: [connectionIdArgument],
};
function replaceCreateConnection(string: string) {
return string.replace(
'{createConnection.id}',
'{connection.id}'
);
return string.replace('{createConnection.id}', '{connection.id}');
}
function removeAppKeyArgument(args: IAuthenticationStepField[]) {
@@ -36,7 +35,7 @@ function addConnectionId(step: IAuthenticationStep) {
return {
name: property.name,
value: replaceCreateConnection(property.value),
}
};
});
}
@@ -60,7 +59,7 @@ function replaceCreateConnectionsWithUpdate(steps: IAuthenticationStep[]) {
}
return step;
})
});
}
function addReconnectionSteps(app: IApp): IApp {
@@ -68,12 +67,11 @@ function addReconnectionSteps(app: IApp): IApp {
if (hasReconnectionSteps) return app;
const updatedSteps = replaceCreateConnectionsWithUpdate(app.auth.authenticationSteps);
const updatedSteps = replaceCreateConnectionsWithUpdate(
app.auth.authenticationSteps
);
app.auth.reconnectionSteps = [
resetConnectionStep,
...updatedSteps,
]
app.auth.reconnectionSteps = [resetConnectionStep, ...updatedSteps];
return app;
}

View File

@@ -16,7 +16,7 @@ const appInfoConverter = (rawAppData: IApp) => {
};
}
return field
return field;
});
}

View File

@@ -3,15 +3,15 @@ import logger from './logger';
type Error = {
message: string;
}
};
const errorHandler = (err: Error, req: Request, res: Response): void => {
if (err.message === 'Not Found') {
res.status(404).end()
res.status(404).end();
} else {
logger.error(err.message)
res.status(500).end()
}
logger.error(err.message);
res.status(500).end();
}
};
export default errorHandler;

View File

@@ -75,7 +75,10 @@ const globalVariable = async (
},
},
pushTriggerItem: (triggerItem: ITriggerItem) => {
if (isAlreadyProcessed(triggerItem.meta.internalId) && !$.execution.testRun) {
if (
isAlreadyProcessed(triggerItem.meta.internalId) &&
!$.execution.testRun
) {
// early exit as we do not want to process duplicate items in actual executions
throw new EarlyExitError();
}

View File

@@ -10,7 +10,7 @@ const levels = {
};
const level = () => {
return appConfig.appEnv === 'development' ? 'debug' : 'info'
return appConfig.appEnv === 'development' ? 'debug' : 'info';
};
const colors = {
@@ -27,8 +27,8 @@ const format = winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss:ms' }),
winston.format.colorize({ all: true }),
winston.format.printf(
(info) => `${info.timestamp} [${info.level}]: ${info.message}`,
),
(info) => `${info.timestamp} [${info.level}]: ${info.message}`
)
);
const transports = [

View File

@@ -3,13 +3,14 @@ import { Request } from 'express';
import logger from './logger';
const stream: StreamOptions = {
write: (message) => logger.http(message.substring(0, message.lastIndexOf("\n")))
write: (message) =>
logger.http(message.substring(0, message.lastIndexOf('\n'))),
};
const registerGraphQLToken = () => {
morgan.token("graphql-query", (req: Request) => {
morgan.token('graphql-query', (req: Request) => {
if (req.body.query) {
return `GraphQL ${req.body.query}`
return `GraphQL ${req.body.query}`;
}
});
};
@@ -17,7 +18,7 @@ const registerGraphQLToken = () => {
registerGraphQLToken();
const morganMiddleware = morgan(
":method :url :status :res[content-length] - :response-time ms\n:graphql-query",
':method :url :status :res[content-length] - :response-time ms\n:graphql-query',
{ stream }
);

View File

@@ -25,7 +25,10 @@ export default function parseLinkHeader(link: string): TParsedLinkHeader {
const items = link.split(',');
for (const item of items) {
const [rawUriReference, ...rawLinkParameters] = item.split(';') as [string, ...string[]];
const [rawUriReference, ...rawLinkParameters] = item.split(';') as [
string,
...string[]
];
const trimmedUriReference = rawUriReference.trim();
const reference = trimmedUriReference.slice(1, -1);

View File

@@ -36,7 +36,10 @@ class Base extends Model {
this.updatedAt = new Date().toISOString();
}
async $beforeUpdate(opts: ModelOptions, queryContext: QueryContext): Promise<void> {
async $beforeUpdate(
opts: ModelOptions,
queryContext: QueryContext
): Promise<void> {
this.updatedAt = new Date().toISOString();
await super.$beforeUpdate(opts, queryContext);

View File

@@ -1,20 +1,34 @@
import { Model, Page, PartialModelObject, ForClassMethod, AnyQueryBuilder } from "objection";
import {
Model,
Page,
PartialModelObject,
ForClassMethod,
AnyQueryBuilder,
} from 'objection';
const DELETED_COLUMN_NAME = 'deleted_at';
const buildQueryBuidlerForClass = (): ForClassMethod => {
return (modelClass) => {
const qb: AnyQueryBuilder = Model.QueryBuilder.forClass.call(ExtendedQueryBuilder, modelClass);
const qb: AnyQueryBuilder = Model.QueryBuilder.forClass.call(
ExtendedQueryBuilder,
modelClass
);
qb.onBuild((builder) => {
if (!builder.context().withSoftDeleted) {
builder.whereNull(`${qb.modelClass().tableName}.${DELETED_COLUMN_NAME}`);
builder.whereNull(
`${qb.modelClass().tableName}.${DELETED_COLUMN_NAME}`
);
}
});
return qb;
};
};
class ExtendedQueryBuilder<M extends Model, R = M[]> extends Model.QueryBuilder<M, R> {
class ExtendedQueryBuilder<M extends Model, R = M[]> extends Model.QueryBuilder<
M,
R
> {
ArrayQueryBuilderType!: ExtendedQueryBuilder<M, M[]>;
SingleQueryBuilderType!: ExtendedQueryBuilder<M, M>;
MaybeSingleQueryBuilderType!: ExtendedQueryBuilder<M, M | undefined>;
@@ -25,7 +39,7 @@ class ExtendedQueryBuilder<M extends Model, R = M[]> extends Model.QueryBuilder<
delete() {
return this.patch({
[DELETED_COLUMN_NAME]: (new Date()).toISOString(),
[DELETED_COLUMN_NAME]: new Date().toISOString(),
} as unknown as PartialModelObject<M>);
}

View File

@@ -2,13 +2,13 @@ import FieldType from './field';
import AuthenticationStepType from './authentication-step';
type AppInfo = {
name: string,
key: string,
iconUrl: string,
docUrl: string,
primaryColor: string,
fields: FieldType[],
authenticationSteps?: AuthenticationStepType[]
}
name: string;
key: string;
iconUrl: string;
docUrl: string;
primaryColor: string;
fields: FieldType[];
authenticationSteps?: AuthenticationStepType[];
};
export default AppInfo;

View File

@@ -1,10 +1,10 @@
type AuthenticationStepField = {
name: string,
value: string | null,
name: string;
value: string | null;
fields?: {
name: string,
value: string | null
}[]
}
name: string;
value: string | null;
}[];
};
export default AuthenticationStepField;

View File

@@ -1,10 +1,10 @@
import type { IAuthenticationStepField } from '@automatisch/types';
type AuthenticationStep = {
step: number,
type: string,
name: string,
step: number;
type: string;
name: string;
fields: IAuthenticationStepField[];
}
};
export default AuthenticationStep;

View File

@@ -1,14 +1,14 @@
type Field = {
key: string,
label: string,
type: string,
required: boolean,
readOnly: boolean,
value: string,
placeholder: string | null,
description: string,
docUrl: string,
clickToCopy: boolean
}
key: string;
label: string;
type: string;
required: boolean;
readOnly: boolean;
value: string;
placeholder: string | null;
description: string;
docUrl: string;
clickToCopy: boolean;
};
export default Field;

View File

@@ -2,6 +2,6 @@ import test from 'ava';
const fn = () => 'foo';
test('fn() returns foo', t => {
test('fn() returns foo', (t) => {
t.is(fn(), 'foo');
});

View File

@@ -1,16 +1,16 @@
version: 2
updates:
- package-ecosystem: "npm"
- package-ecosystem: 'npm'
versioning-strategy: increase
directory: "/"
directory: '/'
schedule:
interval: "monthly"
interval: 'monthly'
labels:
- "dependencies"
- 'dependencies'
open-pull-requests-limit: 100
pull-request-branch-name:
separator: "-"
separator: '-'
ignore:
- dependency-name: "fs-extra"
- dependency-name: "*"
update-types: ["version-update:semver-major"]
- dependency-name: 'fs-extra'
- dependency-name: '*'
update-types: ['version-update:semver-major']

View File

@@ -11,7 +11,7 @@ export default class StartWorker extends Command {
char: 'e',
}),
'env-file': Flags.string(),
}
};
async prepareEnvVars(): Promise<void> {
const { flags } = await this.parse(StartWorker);

View File

@@ -11,7 +11,7 @@ export default class Start extends Command {
char: 'e',
}),
'env-file': Flags.string(),
}
};
get isProduction() {
return process.env.APP_ENV === 'production';
@@ -46,7 +46,7 @@ export default class Start extends Command {
await utils.createDatabaseAndUser(
process.env.POSTGRES_DATABASE,
process.env.POSTGRES_USERNAME,
process.env.POSTGRES_USERNAME
);
}

View File

@@ -1 +1 @@
export {run} from '@oclif/core'
export { run } from '@oclif/core';

View File

@@ -1,4 +1,4 @@
const { defineConfig } = require("cypress");
const { defineConfig } = require('cypress');
const TO_BE_PROVIDED = 'HAS_TO_BE_PROVIDED_IN_cypress.env.json';
@@ -6,12 +6,12 @@ module.exports = defineConfig({
e2e: {
baseUrl: 'http://localhost:3001',
env: {
login_email: "user@automatisch.io",
login_password: "sample",
login_email: 'user@automatisch.io',
login_password: 'sample',
slack_user_token: TO_BE_PROVIDED,
},
specPattern: 'cypress/e2e/**/*.{js,jsx,ts,tsx}',
viewportWidth: 1280,
viewportHeight: 768
viewportHeight: 768,
},
});

View File

@@ -23,24 +23,17 @@ describe('Connections page', () => {
context('can add connection', () => {
it('has a button to open add connection dialog', () => {
cy
.og('add-connection-button')
.scrollIntoView()
.should('be.visible');
cy.og('add-connection-button').scrollIntoView().should('be.visible');
});
it('add connection button takes user to add connection page', () => {
cy
.og('add-connection-button')
.click({ force: true });
cy.og('add-connection-button').click({ force: true });
cy.location('pathname').should('equal', '/app/slack/connections/add');
});
it('shows add connection dialog to create a new connection', () => {
cy
.get('input[name="accessToken"]')
.type(Cypress.env('slack_user_token'));
cy.get('input[name="accessToken"]').type(Cypress.env('slack_user_token'));
cy.og('create-connection-button').click();

View File

@@ -27,10 +27,7 @@ describe('Flow editor page', () => {
});
it('choose an event', () => {
cy
.og('choose-event-autocomplete')
.should('be.visible')
.click();
cy.og('choose-event-autocomplete').should('be.visible').click();
cy.get('li[role="option"]:contains("Every hour")').click();
});
@@ -42,13 +39,12 @@ describe('Flow editor page', () => {
it('collapses the substep', () => {
cy.og('choose-app-autocomplete').should('not.be.visible');
cy.og('choose-event-autocomplete').should('not.be.visible');
})
});
});
context('set up a trigger', () => {
it('choose "yes" in "trigger on weekends?"', () => {
cy
.og('parameters.triggersOnWeekend-autocomplete')
cy.og('parameters.triggersOnWeekend-autocomplete')
.should('be.visible')
.click();
@@ -60,7 +56,9 @@ describe('Flow editor page', () => {
});
it('collapses the substep', () => {
cy.og('parameters.triggersOnWeekend-autocomplete').should('not.exist');
cy.og('parameters.triggersOnWeekend-autocomplete').should(
'not.exist'
);
});
});
@@ -88,12 +86,11 @@ describe('Flow editor page', () => {
});
it('choose an event', () => {
cy
.og('choose-event-autocomplete')
.should('be.visible')
.click();
cy.og('choose-event-autocomplete').should('be.visible').click();
cy.get('li[role="option"]:contains("Send a message to channel")').click();
cy.get(
'li[role="option"]:contains("Send a message to channel")'
).click();
});
it('continue to next step', () => {
@@ -130,14 +127,16 @@ describe('Flow editor page', () => {
});
it('arrange message text', () => {
cy
.og('power-input', ' [contenteditable]')
cy.og('power-input', ' [contenteditable]')
.click()
.type(`Hello from e2e tests! Here is the first suggested variable's value; `);
.type(
`Hello from e2e tests! Here is the first suggested variable's value; `
);
cy
.og('power-input-suggestion-group').first()
.og('power-input-suggestion-item').first()
cy.og('power-input-suggestion-group')
.first()
.og('power-input-suggestion-item')
.first()
.click();
cy.clickOutside();
@@ -150,9 +149,7 @@ describe('Flow editor page', () => {
});
it('collapses the substep', () => {
cy
.og('power-input', ' [contenteditable]')
.should('not.exist');
cy.og('power-input', ' [contenteditable]').should('not.exist');
});
});
@@ -176,10 +173,7 @@ describe('Flow editor page', () => {
it('publish flow', () => {
cy.og('unpublish-flow-button').should('not.exist');
cy
.og('publish-flow-button')
.should('be.visible')
.click();
cy.og('publish-flow-button').should('be.visible').click();
cy.og('publish-flow-button').should('not.exist');
});
@@ -191,27 +185,19 @@ describe('Flow editor page', () => {
});
it('unpublish from snackbar', () => {
cy
.og('unpublish-flow-from-snackbar')
.click();
cy.og('unpublish-flow-from-snackbar').click();
cy.og('flow-cannot-edit-info-snackbar').should('not.exist');
})
});
it('publish once again', () => {
cy
.og('publish-flow-button')
.should('be.visible')
.click();
cy.og('publish-flow-button').should('be.visible').click();
cy.og('publish-flow-button').should('not.exist');
});
it('unpublish from layout top bar', () => {
cy
.og('unpublish-flow-button')
.should('be.visible')
.click();
cy.og('unpublish-flow-button').should('be.visible').click();
cy.og('unpublish-flow-button').should('not.exist');

View File

@@ -1,10 +1,14 @@
Cypress.Commands.add('og', { prevSubject: 'optional' }, (subject, selector, suffix = '') => {
Cypress.Commands.add(
'og',
{ prevSubject: 'optional' },
(subject, selector, suffix = '') => {
if (subject) {
return cy.wrap(subject).get(`[data-test="${selector}"]${suffix}`);
}
return cy.get(`[data-test="${selector}"]${suffix}`);
});
}
);
Cypress.Commands.add('login', () => {
cy.visit('/login');
@@ -12,12 +16,10 @@ Cypress.Commands.add('login', () => {
cy.og('email-text-field').type(Cypress.env('login_email'));
cy.og('password-text-field').type(Cypress.env('login_password'));
cy
.intercept('/graphql')
.as('graphqlCalls');
cy
.intercept('https://notifications.automatisch.io/notifications.json')
.as('notificationsCall');
cy.intercept('/graphql').as('graphqlCalls');
cy.intercept('https://notifications.automatisch.io/notifications.json').as(
'notificationsCall'
);
cy.og('login-button').click();
cy.wait(['@graphqlCalls', '@notificationsCall']);
@@ -30,14 +32,11 @@ Cypress.Commands.add('logout', () => {
});
Cypress.Commands.add('ss', (name, opts = {}) => {
return cy.screenshot(
name,
{
return cy.screenshot(name, {
overwrite: true,
capture: 'viewport',
...opts,
}
);
});
});
Cypress.Commands.add('clickOutside', () => {

View File

@@ -14,7 +14,7 @@
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands'
import './commands';
// Alternatively you can use CommonJS syntax:
// require('./commands')

View File

@@ -25,9 +25,12 @@
-->
<title>Automatisch</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap" rel="stylesheet">
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap"
rel="stylesheet"
/>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>

View File

@@ -12,21 +12,18 @@ import useFormatMessage from 'hooks/useFormatMessage';
type AccountDropdownMenuProps = {
open: boolean;
onClose: () => void;
anchorEl: MenuProps["anchorEl"];
anchorEl: MenuProps['anchorEl'];
id: string;
}
};
function AccountDropdownMenu(props: AccountDropdownMenuProps): React.ReactElement {
function AccountDropdownMenu(
props: AccountDropdownMenuProps
): React.ReactElement {
const formatMessage = useFormatMessage();
const authentication = useAuthentication();
const navigate = useNavigate();
const {
open,
onClose,
anchorEl,
id
} = props
const { open, onClose, anchorEl, id } = props;
const logout = async () => {
authentication.updateToken('');
@@ -53,17 +50,11 @@ function AccountDropdownMenu(props: AccountDropdownMenuProps): React.ReactElemen
open={open}
onClose={onClose}
>
<MenuItem
component={Link}
to={URLS.SETTINGS_DASHBOARD}
>
<MenuItem component={Link} to={URLS.SETTINGS_DASHBOARD}>
{formatMessage('accountDropdownMenu.settings')}
</MenuItem>
<MenuItem
onClick={logout}
data-test="logout-item"
>
<MenuItem onClick={logout} data-test="logout-item">
{formatMessage('accountDropdownMenu.logout')}
</MenuItem>
</Menu>

View File

@@ -14,8 +14,11 @@ import InputCreator from 'components/InputCreator';
import type { IApp, IField } from '@automatisch/types';
import { Form } from './style';
const generateDocsLink = (link: string) => (str: string) => (
<a href={link} target="_blank">{str}</a>
const generateDocsLink = (link: string) => (str: string) =>
(
<a href={link} target="_blank">
{str}
</a>
);
type AddAppConnectionProps = {
@@ -26,25 +29,33 @@ type AddAppConnectionProps = {
type Response = {
[key: string]: any;
}
};
export default function AddAppConnection(props: AddAppConnectionProps): React.ReactElement {
export default function AddAppConnection(
props: AddAppConnectionProps
): React.ReactElement {
const { application, connectionId, onClose } = props;
const { name, authDocUrl, key, auth } = application;
const formatMessage = useFormatMessage();
const [errorMessage, setErrorMessage] = React.useState<string | null>(null);
const [inProgress, setInProgress] = React.useState(false);
const hasConnection = Boolean(connectionId);
const steps = hasConnection ? auth?.reconnectionSteps : auth?.authenticationSteps;
const steps = hasConnection
? auth?.reconnectionSteps
: auth?.authenticationSteps;
React.useEffect(() => {
if (window.opener) {
window.opener.postMessage({ source: 'automatisch', payload: window.location.search });
window.opener.postMessage({
source: 'automatisch',
payload: window.location.search,
});
window.close();
}
}, []);
const submitHandler: SubmitHandler<FieldValues> = React.useCallback(async (data) => {
const submitHandler: SubmitHandler<FieldValues> = React.useCallback(
async (data) => {
if (!steps) return;
setInProgress(true);
@@ -53,7 +64,7 @@ export default function AddAppConnection(props: AddAppConnectionProps): React.Re
const response: Response = {
key,
connection: {
id: connectionId
id: connectionId,
},
fields: data,
};
@@ -84,21 +95,24 @@ export default function AddAppConnection(props: AddAppConnectionProps): React.Re
}
setInProgress(false);
}, [connectionId, key, steps, onClose]);
},
[connectionId, key, steps, onClose]
);
return (
<Dialog open={true} onClose={onClose} data-test="add-app-connection-dialog">
<DialogTitle>{hasConnection ? formatMessage('app.reconnectConnection') : formatMessage('app.addConnection')}</DialogTitle>
<DialogTitle>
{hasConnection
? formatMessage('app.reconnectConnection')
: formatMessage('app.addConnection')}
</DialogTitle>
{authDocUrl && (
<Alert severity="info" sx={{ fontWeight: 300 }}>
{formatMessage(
'addAppConnection.callToDocs',
{
{formatMessage('addAppConnection.callToDocs', {
appName: name,
docsLink: generateDocsLink(authDocUrl)
}
)}
docsLink: generateDocsLink(authDocUrl),
})}
</Alert>
)}
@@ -111,7 +125,9 @@ export default function AddAppConnection(props: AddAppConnectionProps): React.Re
<DialogContent>
<DialogContentText tabIndex={-1} component="div">
<Form onSubmit={submitHandler}>
{auth?.fields?.map((field: IField) => (<InputCreator key={field.key} schema={field} />))}
{auth?.fields?.map((field: IField) => (
<InputCreator key={field.key} schema={field} />
))}
<LoadingButton
type="submit"
@@ -128,4 +144,4 @@ export default function AddAppConnection(props: AddAppConnectionProps): React.Re
</DialogContent>
</Dialog>
);
};
}

View File

@@ -13,9 +13,12 @@ const ApolloProvider = (props: ApolloProviderProps): React.ReactElement => {
const { enqueueSnackbar } = useSnackbar();
const authentication = useAuthentication();
const onError = React.useCallback((message) => {
const onError = React.useCallback(
(message) => {
enqueueSnackbar(message, { variant: 'error' });
}, [enqueueSnackbar]);
},
[enqueueSnackbar]
);
const client = React.useMemo(() => {
return mutateAndGetClient({
@@ -24,9 +27,7 @@ const ApolloProvider = (props: ApolloProviderProps): React.ReactElement => {
});
}, [onError, authentication]);
return (
<BaseApolloProvider client={client} {...props} />
);
return <BaseApolloProvider client={client} {...props} />;
};
export default ApolloProvider;

View File

@@ -20,23 +20,21 @@ type AppBarProps = {
drawerOpen: boolean;
onDrawerOpen: () => void;
onDrawerClose: () => void;
maxWidth?: ContainerProps["maxWidth"];
maxWidth?: ContainerProps['maxWidth'];
};
const accountMenuId = 'account-menu';
export default function AppBar(props: AppBarProps): React.ReactElement {
const {
drawerOpen,
onDrawerOpen,
onDrawerClose,
maxWidth = false,
} = props;
const { drawerOpen, onDrawerOpen, onDrawerClose, maxWidth = false } = props;
const theme = useTheme();
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('md'), { noSsr: true });
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('md'), {
noSsr: true,
});
const [accountMenuAnchorElement, setAccountMenuAnchorElement] = React.useState<null | HTMLElement>(null);
const [accountMenuAnchorElement, setAccountMenuAnchorElement] =
React.useState<null | HTMLElement>(null);
const isMenuOpen = Boolean(accountMenuAnchorElement);
@@ -65,11 +63,7 @@ export default function AppBar(props: AppBarProps): React.ReactElement {
<div style={{ flexGrow: 1 }}>
<Link to={URLS.DASHBOARD}>
<Typography
variant="h6"
component="h1"
noWrap
>
<Typography variant="h6" component="h1" noWrap>
<FormattedMessage id="brandText" />
</Typography>
</Link>

View File

@@ -19,17 +19,22 @@ type ContextMenuProps = {
anchorEl: PopoverProps['anchorEl'];
};
export default function ContextMenu(props: ContextMenuProps): React.ReactElement {
export default function ContextMenu(
props: ContextMenuProps
): React.ReactElement {
const { appKey, connectionId, onClose, onMenuItemClick, anchorEl } = props;
const formatMessage = useFormatMessage();
const createActionHandler = React.useCallback((action: Action) => {
const createActionHandler = React.useCallback(
(action: Action) => {
return function clickHandler(event: React.MouseEvent) {
onMenuItemClick(event, action);
onClose();
};
}, [onMenuItemClick, onClose]);
},
[onMenuItemClick, onClose]
);
return (
<Menu
@@ -63,4 +68,4 @@ export default function ContextMenu(props: ContextMenuProps): React.ReactElement
</MenuItem>
</Menu>
);
};
}

View File

@@ -20,13 +20,11 @@ import { CardContent, Typography } from './style';
type AppConnectionRowProps = {
connection: IConnection;
}
};
const countTranslation = (value: React.ReactNode) => (
<>
<Typography variant="body1">
{value}
</Typography>
<Typography variant="body1">{value}</Typography>
<br />
</>
);
@@ -34,15 +32,21 @@ const countTranslation = (value: React.ReactNode) => (
function AppConnectionRow(props: AppConnectionRowProps): React.ReactElement {
const { enqueueSnackbar } = useSnackbar();
const [verificationVisible, setVerificationVisible] = React.useState(false);
const [testConnection, { called: testCalled, loading: testLoading }] = useLazyQuery(TEST_CONNECTION, {
const [testConnection, { called: testCalled, loading: testLoading }] =
useLazyQuery(TEST_CONNECTION, {
fetchPolicy: 'network-only',
onCompleted: () => { setTimeout(() => setVerificationVisible(false), 3000); },
onError: () => { setTimeout(() => setVerificationVisible(false), 3000); },
onCompleted: () => {
setTimeout(() => setVerificationVisible(false), 3000);
},
onError: () => {
setTimeout(() => setVerificationVisible(false), 3000);
},
});
const [deleteConnection] = useMutation(DELETE_CONNECTION);
const formatMessage = useFormatMessage();
const { id, key, formattedData, verified, createdAt, flowCount } = props.connection;
const { id, key, formattedData, verified, createdAt, flowCount } =
props.connection;
const contextButtonRef = React.useRef<SVGSVGElement | null>(null);
const [anchorEl, setAnchorEl] = React.useState<SVGSVGElement | null>(null);
@@ -52,7 +56,8 @@ function AppConnectionRow(props: AppConnectionRowProps): React.ReactElement {
};
const onContextMenuClick = () => setAnchorEl(contextButtonRef.current);
const onContextMenuAction = React.useCallback(async (event, action: { [key: string]: string }) => {
const onContextMenuAction = React.useCallback(
async (event, action: { [key: string]: string }) => {
if (action.type === 'delete') {
await deleteConnection({
variables: { input: { id } },
@@ -65,34 +70,38 @@ function AppConnectionRow(props: AppConnectionRowProps): React.ReactElement {
cache.evict({
id: connectionCacheId,
});
}
},
});
enqueueSnackbar(formatMessage('connection.deletedMessage'), { variant: 'success' });
enqueueSnackbar(formatMessage('connection.deletedMessage'), {
variant: 'success',
});
} else if (action.type === 'test') {
setVerificationVisible(true);
testConnection({ variables: { id } });
}
}, [deleteConnection, id, testConnection, formatMessage, enqueueSnackbar]);
},
[deleteConnection, id, testConnection, formatMessage, enqueueSnackbar]
);
const relativeCreatedAt = DateTime.fromMillis(parseInt(createdAt, 10)).toRelative();
const relativeCreatedAt = DateTime.fromMillis(
parseInt(createdAt, 10)
).toRelative();
return (
<>
<Card sx={{ my: 2 }} data-test="app-connection-row">
<CardActionArea onClick={onContextMenuClick}>
<CardContent>
<Stack
justifyContent="center"
alignItems="flex-start"
spacing={1}
>
<Stack justifyContent="center" alignItems="flex-start" spacing={1}>
<Typography variant="h6" sx={{ textAlign: 'left' }}>
{formattedData?.screenName}
</Typography>
<Typography variant="caption">
{formatMessage('connection.addedAt', { datetime: relativeCreatedAt })}
{formatMessage('connection.addedAt', {
datetime: relativeCreatedAt,
})}
</Typography>
</Stack>
@@ -101,27 +110,42 @@ function AppConnectionRow(props: AppConnectionRowProps): React.ReactElement {
{verificationVisible && testCalled && testLoading && (
<>
<CircularProgress size={16} />
<Typography variant="caption">{formatMessage('connection.testing')}</Typography>
<Typography variant="caption">
{formatMessage('connection.testing')}
</Typography>
</>
)}
{verificationVisible && testCalled && !testLoading && verified && (
<>
<CheckCircleIcon fontSize="small" color="success" />
<Typography variant="caption">{formatMessage('connection.testSuccessful')}</Typography>
<Typography variant="caption">
{formatMessage('connection.testSuccessful')}
</Typography>
</>
)}
{verificationVisible && testCalled && !testLoading && !verified && (
{verificationVisible &&
testCalled &&
!testLoading &&
!verified && (
<>
<ErrorIcon fontSize="small" color="error" />
<Typography variant="caption">{formatMessage('connection.testFailed')}</Typography>
<Typography variant="caption">
{formatMessage('connection.testFailed')}
</Typography>
</>
)}
</Stack>
</Box>
<Box sx={{ px: 2 }}>
<Typography variant="caption" color="textSecondary" sx={{ display: ['none', 'inline-block'] }}>
{formatMessage('connection.flowCount', { count: countTranslation(flowCount) })}
<Typography
variant="caption"
color="textSecondary"
sx={{ display: ['none', 'inline-block'] }}
>
{formatMessage('connection.flowCount', {
count: countTranslation(flowCount),
})}
</Typography>
</Box>
@@ -132,13 +156,15 @@ function AppConnectionRow(props: AppConnectionRowProps): React.ReactElement {
</CardActionArea>
</Card>
{anchorEl && <ConnectionContextMenu
{anchorEl && (
<ConnectionContextMenu
appKey={key}
connectionId={id}
onClose={handleClose}
onMenuItemClick={onContextMenuAction}
anchorEl={anchorEl}
/>}
/>
)}
</>
);
}

View File

@@ -10,7 +10,6 @@ export const CardContent = styled(MuiCardContent)(({ theme }) => ({
alignItems: 'center',
}));
export const Typography = styled(MuiTypography)(() => ({
textAlign: 'center',
display: 'inline-block',

View File

@@ -10,12 +10,16 @@ import * as URLS from 'config/urls';
type AppConnectionsProps = {
appKey: string;
}
};
export default function AppConnections(props: AppConnectionsProps): React.ReactElement {
export default function AppConnections(
props: AppConnectionsProps
): React.ReactElement {
const { appKey } = props;
const formatMessage = useFormatMessage();
const { data } = useQuery(GET_APP_CONNECTIONS, { variables: { key: appKey } });
const { data } = useQuery(GET_APP_CONNECTIONS, {
variables: { key: appKey },
});
const appConnections: IConnection[] = data?.getApp?.connections || [];
const hasConnections = appConnections?.length;
@@ -35,5 +39,5 @@ export default function AppConnections(props: AppConnectionsProps): React.ReactE
<AppConnectionRow key={appConnection.id} connection={appConnection} />
))}
</>
)
};
);
}

View File

@@ -12,7 +12,7 @@ import type { IFlow } from '@automatisch/types';
type AppFlowsProps = {
appKey: string;
}
};
const FLOW_PER_PAGE = 10;
@@ -27,11 +27,13 @@ export default function AppFlows(props: AppFlowsProps): React.ReactElement {
const [searchParams, setSearchParams] = useSearchParams();
const connectionId = searchParams.get('connectionId') || undefined;
const page = parseInt(searchParams.get('page') || '', 10) || 1;
const { data } = useQuery(GET_FLOWS, { variables: {
const { data } = useQuery(GET_FLOWS, {
variables: {
appKey,
connectionId,
...getLimitAndOffset(page)
}});
...getLimitAndOffset(page),
},
});
const getFlows = data?.getFlows || {};
const { pageInfo, edges } = getFlows;
@@ -53,7 +55,8 @@ export default function AppFlows(props: AppFlowsProps): React.ReactElement {
<AppFlowRow key={appFlow.id} flow={appFlow} />
))}
{pageInfo && pageInfo.totalPages > 1 && <Pagination
{pageInfo && pageInfo.totalPages > 1 && (
<Pagination
sx={{ display: 'flex', justifyContent: 'center', mt: 3 }}
page={pageInfo?.currentPage}
count={pageInfo?.totalPages}
@@ -65,7 +68,8 @@ export default function AppFlows(props: AppFlowsProps): React.ReactElement {
{...item}
/>
)}
/>}
/>
)}
</>
)
};
);
}

View File

@@ -13,15 +13,10 @@ const inlineImgStyle: React.CSSProperties = {
objectFit: 'contain',
};
export default function AppIcon(props: AppIconProps & AvatarProps): React.ReactElement {
const {
name,
url,
color,
sx = {},
variant = "square",
...restProps
} = props;
export default function AppIcon(
props: AppIconProps & AvatarProps
): React.ReactElement {
const { name, url, color, sx = {}, variant = 'square', ...restProps } = props;
const initialLetter = name?.[0];
@@ -37,4 +32,4 @@ export default function AppIcon(props: AppIconProps & AvatarProps): React.ReactE
{...restProps}
/>
);
};
}

View File

@@ -14,20 +14,19 @@ import { CardContent, Typography } from './style';
type AppRowProps = {
application: IApp;
}
};
const countTranslation = (value: React.ReactNode) => (
<>
<Typography variant="body1">
{value}
</Typography>
<Typography variant="body1">{value}</Typography>
<br />
</>
);
function AppRow(props: AppRowProps): React.ReactElement {
const formatMessage = useFormatMessage();
const { name, primaryColor, iconUrl, connectionCount, flowCount } = props.application;
const { name, primaryColor, iconUrl, connectionCount, flowCount } =
props.application;
return (
<Link to={URLS.APP(name.toLowerCase())} data-test="app-row">
@@ -39,25 +38,37 @@ function AppRow(props: AppRowProps): React.ReactElement {
</Box>
<Box>
<Typography variant="h6">
{name}
<Typography variant="h6">{name}</Typography>
</Box>
<Box sx={{ px: 2 }}>
<Typography
variant="caption"
color="textSecondary"
sx={{ display: ['none', 'inline-block'] }}
>
{formatMessage('app.connectionCount', {
count: countTranslation(connectionCount),
})}
</Typography>
</Box>
<Box sx={{ px: 2 }}>
<Typography variant="caption" color="textSecondary" sx={{ display: ['none', 'inline-block'] }}>
{formatMessage('app.connectionCount', { count: countTranslation(connectionCount) })}
</Typography>
</Box>
<Box sx={{ px: 2 }}>
<Typography variant="caption" color="textSecondary" sx={{ display: ['none', 'inline-block'] }}>
{formatMessage('app.flowCount', { count: countTranslation(flowCount) })}
<Typography
variant="caption"
color="textSecondary"
sx={{ display: ['none', 'inline-block'] }}
>
{formatMessage('app.flowCount', {
count: countTranslation(flowCount),
})}
</Typography>
</Box>
<Box>
<ArrowForwardIosIcon sx={{ color: (theme) => theme.palette.primary.main }} />
<ArrowForwardIosIcon
sx={{ color: (theme) => theme.palette.primary.main }}
/>
</Box>
</CardContent>
</CardActionArea>

View File

@@ -10,7 +10,6 @@ export const CardContent = styled(MuiCardContent)(({ theme }) => ({
alignItems: 'center',
}));
export const Typography = styled(MuiTypography)(() => ({
'&.MuiTypography-h6': {
textTransform: 'capitalize',
@@ -22,5 +21,5 @@ export const Typography = styled(MuiTypography)(() => ({
export const DesktopOnlyBreakline = styled('br')(({ theme }) => ({
[theme.breakpoints.down('sm')]: {
display: 'none',
}
},
}));

View File

@@ -12,7 +12,13 @@ import useFormatMessage from 'hooks/useFormatMessage';
import { EditorContext } from 'contexts/Editor';
import { GET_APPS } from 'graphql/queries/get-apps';
import FlowSubstepTitle from 'components/FlowSubstepTitle';
import type { IApp, IStep, ISubstep, ITrigger, IAction } from '@automatisch/types';
import type {
IApp,
IStep,
ISubstep,
ITrigger,
IAction,
} from '@automatisch/types';
type ChooseAppAndEventSubstepProps = {
substep: ISubstep;
@@ -24,7 +30,10 @@ type ChooseAppAndEventSubstepProps = {
step: IStep;
};
const optionGenerator = (app: { name: string, key: string, }): { label: string; value: string } => ({
const optionGenerator = (app: {
name: string;
key: string;
}): { label: string; value: string } => ({
label: app.name as string,
value: app.key as string,
});
@@ -61,13 +70,13 @@ function ChooseAppAndEventSubstep(
() => apps?.map((app) => optionGenerator(app)),
[apps]
);
const actionsOrTriggers: Array<ITrigger | IAction> = (isTrigger ? app?.triggers : app?.actions) || [];
const actionsOrTriggers: Array<ITrigger | IAction> =
(isTrigger ? app?.triggers : app?.actions) || [];
const actionOptions = React.useMemo(
() => actionsOrTriggers.map((trigger) => optionGenerator(trigger)),
[app?.key]
);
const selectedActionOrTrigger =
actionsOrTriggers.find(
const selectedActionOrTrigger = actionsOrTriggers.find(
(actionOrTrigger: IAction | ITrigger) => actionOrTrigger.key === step?.key
);

View File

@@ -16,7 +16,7 @@ import { TEST_CONNECTION } from 'graphql/queries/test-connection';
type ChooseConnectionSubstepProps = {
application: IApp;
substep: ISubstep,
substep: ISubstep;
expanded?: boolean;
onExpand: () => void;
onCollapse: () => void;
@@ -27,14 +27,19 @@ type ChooseConnectionSubstepProps = {
const ADD_CONNECTION_VALUE = 'ADD_CONNECTION';
const optionGenerator = (connection: IConnection): { label: string; value: string; } => ({
label: connection?.formattedData?.screenName as string ?? 'Unnamed',
const optionGenerator = (
connection: IConnection
): { label: string; value: string } => ({
label: (connection?.formattedData?.screenName as string) ?? 'Unnamed',
value: connection?.id as string,
});
const getOption = (options: Record<string, unknown>[], connectionId?: string) => options.find(connection => connection.value === connectionId) || null;
const getOption = (options: Record<string, unknown>[], connectionId?: string) =>
options.find((connection) => connection.value === connectionId) || null;
function ChooseConnectionSubstep(props: ChooseConnectionSubstepProps): React.ReactElement {
function ChooseConnectionSubstep(
props: ChooseConnectionSubstepProps
): React.ReactElement {
const {
substep,
expanded = false,
@@ -45,36 +50,30 @@ function ChooseConnectionSubstep(props: ChooseConnectionSubstepProps): React.Rea
onChange,
application,
} = props;
const {
connection,
appKey,
} = step;
const { connection, appKey } = step;
const formatMessage = useFormatMessage();
const editorContext = React.useContext(EditorContext);
const [showAddConnectionDialog, setShowAddConnectionDialog] = React.useState(false);
const { data, loading, refetch } = useQuery(GET_APP_CONNECTIONS, { variables: { key: appKey }});
const [showAddConnectionDialog, setShowAddConnectionDialog] =
React.useState(false);
const { data, loading, refetch } = useQuery(GET_APP_CONNECTIONS, {
variables: { key: appKey },
});
// TODO: show detailed error when connection test/verification fails
const [
testConnection,
{
loading: testResultLoading,
refetch: retestConnection
}
] = useLazyQuery(
TEST_CONNECTION,
{
{ loading: testResultLoading, refetch: retestConnection },
] = useLazyQuery(TEST_CONNECTION, {
variables: {
id: connection?.id,
}
}
);
},
});
React.useEffect(() => {
if (connection?.id) {
testConnection({
variables: {
id: connection.id,
}
},
});
}
// intentionally no dependencies for initial test
@@ -82,21 +81,23 @@ function ChooseConnectionSubstep(props: ChooseConnectionSubstepProps): React.Rea
const connectionOptions = React.useMemo(() => {
const appWithConnections = data?.getApp as IApp;
const options = appWithConnections
?.connections
?.map((connection) => optionGenerator(connection)) || [];
const options =
appWithConnections?.connections?.map((connection) =>
optionGenerator(connection)
) || [];
options.push({
label: formatMessage('chooseConnectionSubstep.addNewConnection'),
value: ADD_CONNECTION_VALUE
})
value: ADD_CONNECTION_VALUE,
});
return options;
}, [data, formatMessage]);
const { name } = substep;
const handleAddConnectionClose = React.useCallback(async (response) => {
const handleAddConnectionClose = React.useCallback(
async (response) => {
setShowAddConnectionDialog(false);
const connectionId = response?.createConnection.id;
@@ -113,9 +114,12 @@ function ChooseConnectionSubstep(props: ChooseConnectionSubstepProps): React.Rea
},
});
}
}, [onChange, refetch, step]);
},
[onChange, refetch, step]
);
const handleChange = React.useCallback((event: React.SyntheticEvent, selectedOption: unknown) => {
const handleChange = React.useCallback(
(event: React.SyntheticEvent, selectedOption: unknown) => {
if (typeof selectedOption === 'object') {
// TODO: try to simplify type casting below.
const typedSelectedOption = selectedOption as { value: string };
@@ -138,7 +142,9 @@ function ChooseConnectionSubstep(props: ChooseConnectionSubstepProps): React.Rea
});
}
}
}, [step, onChange]);
},
[step, onChange]
);
React.useEffect(() => {
if (step.connection?.id) {
@@ -146,7 +152,7 @@ function ChooseConnectionSubstep(props: ChooseConnectionSubstepProps): React.Rea
id: step.connection.id,
});
}
}, [step.connection?.id, retestConnection])
}, [step.connection?.id, retestConnection]);
const onToggle = expanded ? onCollapse : onExpand;
@@ -159,7 +165,14 @@ function ChooseConnectionSubstep(props: ChooseConnectionSubstepProps): React.Rea
valid={testResultLoading ? null : connection?.verified}
/>
<Collapse in={expanded} timeout="auto" unmountOnExit>
<ListItem sx={{ pt: 2, pb: 3, flexDirection: 'column', alignItems: 'flex-start' }}>
<ListItem
sx={{
pt: 2,
pb: 3,
flexDirection: 'column',
alignItems: 'flex-start',
}}
>
<Autocomplete
fullWidth
disablePortal
@@ -169,7 +182,9 @@ function ChooseConnectionSubstep(props: ChooseConnectionSubstepProps): React.Rea
renderInput={(params) => (
<TextField
{...params}
label={formatMessage('chooseConnectionSubstep.chooseConnection')}
label={formatMessage(
'chooseConnectionSubstep.chooseConnection'
)}
/>
)}
value={getOption(connectionOptions, connection?.id)}
@@ -183,17 +198,24 @@ function ChooseConnectionSubstep(props: ChooseConnectionSubstepProps): React.Rea
variant="contained"
onClick={onSubmit}
sx={{ mt: 2 }}
disabled={testResultLoading || !connection?.verified || editorContext.readOnly}data-test="flow-substep-continue-button"
disabled={
testResultLoading ||
!connection?.verified ||
editorContext.readOnly
}
data-test="flow-substep-continue-button"
>
{formatMessage('chooseConnectionSubstep.continue')}
</Button>
</ListItem>
</Collapse>
{application && showAddConnectionDialog && <AddAppConnection
{application && showAddConnectionDialog && (
<AddAppConnection
onClose={handleAddConnectionClose}
application={application}
/>}
/>
)}
</React.Fragment>
);
}

View File

@@ -10,7 +10,9 @@ import { IconButton } from './style';
export default function ConditionalIconButton(props: any): React.ReactElement {
const { icon, ...buttonProps } = props;
const theme = useTheme();
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('md'), { noSsr: true });
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('md'), {
noSsr: true,
});
if (matchSmallScreens) {
return (
@@ -22,10 +24,8 @@ export default function ConditionalIconButton(props: any): React.ReactElement {
>
{icon}
</IconButton>
)
}
return (
<Button {...(buttonProps as ButtonProps)} />
);
}
return <Button {...(buttonProps as ButtonProps)} />;
}

View File

@@ -2,11 +2,9 @@ import * as React from 'react';
import MuiContainer, { ContainerProps } from '@mui/material/Container';
export default function Container(props: ContainerProps): React.ReactElement {
return (
<MuiContainer {...props} />
);
};
return <MuiContainer {...props} />;
}
Container.defaultProps = {
maxWidth: 'lg'
maxWidth: 'lg',
};

Some files were not shown because too many files have changed in this diff Show More