Compare commits

..

42 Commits

Author SHA1 Message Date
Rıdvan Akca
98274c3d71 feat(airbrake): add airbrake integration 2024-02-13 12:57:10 +03:00
morihoos
8c936a91be fix(csp): remove illegal characters in directive names (#1585) 2024-02-08 17:41:33 +01:00
Ali BARIN
24451892ff feat: add custom additional drawer link (#1586) 2024-02-08 16:33:12 +01:00
morihoos
6bba2c82fe feat(config): add ability to override apiUrl in environment variables (#1581) 2024-02-07 16:17:03 +01:00
Ali BARIN
3320dc6bc4 Merge pull request #1582 from automatisch/toggle-favicon-and-notifications-page
feat: put favicon and notifications page behind feature flags
2024-02-07 14:46:59 +01:00
Ali BARIN
9d42fd9293 test(queries/get-config): incorporate feature flags
cover disableNotificationsPage and disableFavicon feature flags
2024-02-07 13:11:57 +00:00
Ali BARIN
e6b806616f feat: add DISABLE_FAVICON feature flag 2024-02-07 11:51:17 +00:00
Ali BARIN
6ec5872391 feat: add DISABLE_NOTIFICATIONS_PAGE feature flag 2024-02-07 11:47:44 +00:00
Ali BARIN
a26cf932a1 chore(devcontainer): upgrade node to 20 2024-02-07 11:46:12 +00:00
Ali BARIN
38a3e3ab9f Merge pull request #1570 from automatisch/prevent-public-registrations
fix: prevent registration on non-cloud
2024-01-30 14:11:05 +01:00
Ali BARIN
32b17c1418 fix: prevent registration on non-cloud 2024-01-30 13:03:52 +00:00
Ömer Faruk Aydın
44aa6a1579 Merge pull request #1569 from automatisch/compile-email
fix: Adjust dirname for compile email helper
2024-01-29 13:05:58 +01:00
Faruk AYDIN
2369aacd2a fix: Adjust dirname for compile email helper 2024-01-29 12:48:21 +01:00
Ömer Faruk Aydın
7dafc6364b Merge pull request #1552 from automatisch/dependabot/npm_and_yarn/vite-3.2.8
chore(deps): Bump vite from 3.2.7 to 3.2.8
2024-01-29 12:32:10 +01:00
Ömer Faruk Aydın
3d25fa0aeb Merge branch 'main' into dependabot/npm_and_yarn/vite-3.2.8 2024-01-29 11:14:49 +01:00
Ali BARIN
0297b0f296 Merge pull request #1559 from automatisch/base64-to-text
feat(formatter): add base64 to string action
2024-01-26 10:56:54 +01:00
Rıdvan Akca
4c7d09c3d8 feat(formatter): add base64 to string action 2024-01-26 12:41:05 +03:00
Ali BARIN
48a74826e8 Merge pull request #1557 from automatisch/text-to-base64
feat(formatter): add string to base64 action
2024-01-25 16:56:15 +01:00
Rıdvan Akca
ef34068ac4 feat(formatter): add string to base64 action 2024-01-25 18:52:44 +03:00
dependabot[bot]
3987a8db77 chore(deps): Bump vite from 3.2.7 to 3.2.8
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 3.2.7 to 3.2.8.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v3.2.8/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v3.2.8/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-19 22:40:35 +00:00
Ömer Faruk Aydın
953c5a5b5b Merge pull request #1540 from automatisch/authentication-tests
feat: Add tests for authentication helper
2024-01-15 16:10:36 +01:00
Ömer Faruk Aydın
4313265c00 Merge pull request #1541 from automatisch/remove-cli-from
chore: Remove cli package from linter and build scripts
2024-01-15 16:10:24 +01:00
Faruk AYDIN
9405f267ba chore: Remove build:watch script 2024-01-15 16:06:28 +01:00
Faruk AYDIN
1d29238199 chore: Remove cli package from linter and build scripts 2024-01-15 15:33:21 +01:00
Faruk AYDIN
c5bf66f462 chore: Remove redundant import of authentication rule 2024-01-15 15:31:20 +01:00
Faruk AYDIN
e6180bdfaa chore: No need to export authentication options 2024-01-15 15:31:00 +01:00
Faruk AYDIN
55c391afc8 chore: Remove authentication cases from individual tests 2024-01-15 15:27:30 +01:00
Faruk AYDIN
782fa67320 feat: Add tests for authentication helper 2024-01-15 15:21:04 +01:00
Ömer Faruk Aydın
1e3ab75bb7 Merge pull request #1538 from automatisch/remove-types-package
chore: Remove types package
2024-01-15 13:57:01 +01:00
Faruk AYDIN
5f6dd12a73 chore: Remove types package from web dependencies 2024-01-15 13:41:54 +01:00
Faruk AYDIN
d18c06d2c4 chore: Remove types package 2024-01-15 13:37:58 +01:00
Ömer Faruk Aydın
baf99a9cfe Merge pull request #1537 from automatisch/web/types
chore: Use types from the web package
2024-01-15 13:37:43 +01:00
Faruk AYDIN
159931a6ea chore: Use types from the web package 2024-01-15 13:30:48 +01:00
Ömer Faruk Aydın
7831f2925b Merge pull request #1536 from automatisch/docs/remove-typescript
Use JS for the documentation examples
2024-01-15 13:17:48 +01:00
Faruk AYDIN
8fcb7840de docs: Convert code examples to JS 2024-01-15 13:13:48 +01:00
Faruk AYDIN
9ece9461dc docs: Convert all file imports to ES modules 2024-01-15 13:10:26 +01:00
Faruk AYDIN
b304acaaba docs: Use .js file extension 2024-01-15 13:02:06 +01:00
Faruk AYDIN
5a1960609a docs: Use JS highlighter instead of TS 2024-01-15 12:56:43 +01:00
Faruk AYDIN
476aa6e3aa docs: Remove imports of automatisch types 2024-01-15 12:55:55 +01:00
Ömer Faruk Aydın
aa76007fd0 Merge pull request #1533 from automatisch/remove-cli
chore: Remove cli package
2024-01-14 21:06:46 +01:00
Faruk AYDIN
17a8813c4b chore: Remove build step from CI for cli 2024-01-14 20:13:23 +01:00
Faruk AYDIN
fe79fc9003 chore: Remove cli package 2024-01-14 20:10:23 +01:00
149 changed files with 2000 additions and 19812 deletions

View File

@@ -8,7 +8,7 @@
"version": "latest"
},
"ghcr.io/devcontainers/features/node:1": {
"version": 16
"version": 20
},
"ghcr.io/devcontainers/features/common-utils:1": {
"username": "vscode",

View File

@@ -83,20 +83,3 @@ jobs:
env:
CI: false
- run: echo "🍏 This job's status is ${{ job.status }}."
build-cli:
runs-on: ubuntu-latest
steps:
- run: echo "🎉 The job was automatically triggered by a ${{ github.event_name }} event."
- run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by GitHub!"
- run: echo "🔎 The name of your branch is ${{ github.ref }} and your repository is ${{ github.repository }}."
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '18'
cache: 'yarn'
cache-dependency-path: yarn.lock
- run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner."
- run: echo "🖥️ The workflow is now ready to test your code on the runner."
- run: yarn --frozen-lockfile && yarn lerna bootstrap
- run: cd packages/cli && yarn build
- run: echo "🍏 This job's status is ${{ job.status }}."

View File

@@ -6,8 +6,7 @@
"start": "lerna run --stream --parallel --scope=@*/{web,backend} dev",
"start:web": "lerna run --stream --scope=@*/web dev",
"start:backend": "lerna run --stream --scope=@*/backend dev",
"lint": "lerna run --no-bail --stream --parallel --scope=@*/{web,backend,cli} lint",
"build:watch": "lerna run --no-bail --stream --parallel --scope=@*/{web,backend,cli} build:watch",
"lint": "lerna run --no-bail --stream --parallel --scope=@*/{web,backend} lint",
"build:docs": "cd ./packages/docs && yarn install && yarn build"
},
"workspaces": {
@@ -18,7 +17,6 @@
"**/babel-loader",
"**/webpack",
"**/@automatisch/web",
"**/@automatisch/types",
"**/ajv"
]
},

View File

@@ -1,3 +1,3 @@
import { createUser } from './utils.js';
await createUser();
createUser();

View File

@@ -0,0 +1 @@
<svg height="255" preserveAspectRatio="xMidYMid" viewBox="0 0 256 255" width="256" xmlns="http://www.w3.org/2000/svg"><path d="m128.636514 155.746615v-155.23361889h-3.522242v.06873152l-124.60824865 64.03287157v60.8642488h.00597665v3.234366h-.00597665v60.868233l124.60824865 64.747082h3.842989v-98.581914z" fill="#ff8e4a"/><path d="m129.941416 254.328529 125.568498-64.747082v-124.9668478l-125.887253-64.10160309h-2.243237v253.81055289h2.243237" fill="#f48746"/><path d="m109.097837 87.2551595h36.19561v59.2077195h-36.19561z" fill="#ff8e4a"/><path d="m66.1735097 188.397074h14.8639378c9.4102412 0 12.6087471-2.238257 15.6189883-9.988981l8.2796572-21.353587h45.159596l8.280653 21.353587c3.011238 7.750724 6.396016 9.988981 15.805261 9.988981h14.677665v-19.114335h-3.011237c-3.19751 0-4.704622-.689307-5.831222-3.790194l-39.516638-99.3658524h-25.779299l-39.703907 99.3658524c-1.1285915 3.100887-2.632716 3.790194-5.833214 3.790194h-3.0102413zm44.4075333-49.939922 11.478163-30.655253c2.445448-6.714771 5.269417-18.2556889 5.269417-18.2556889h.375533s2.822972 11.5409179 5.269416 18.2556889l11.478163 30.655253z" fill="#fff"/><path d="m231.204856 150.082739v-51.8086223c.235082 4.5233303 2.970397 16.8432063 24.305058 27.8512063v11.653479zm0-53.1623343v1.353712c-.029883-.5926848-.01793-1.0479066 0-1.353712zm.041837-.4392841s-.022911.1534008-.041837.4392841v-.4392841z" fill="#d4763c"/><path d="m231.155051 94.3016342c-.013946.9931207.05877 1.8945993.049805 2.0460078-.01793.2480312-2.220327 16.094132 24.305058 29.777681v-60.863253c-23.325883 12.0349884-24.449494 25.7414475-24.354863 29.0395642" fill="#ff8e4a"/></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,44 @@
import verifyCredentials from './verify-credentials.js';
import isStillVerified from './is-still-verified.js';
export default {
fields: [
{
key: 'screenName',
label: 'Screen Name',
type: 'string',
required: true,
readOnly: false,
value: null,
placeholder: null,
description:
'Screen name of your connection to be used on Automatisch UI.',
clickToCopy: false,
},
{
key: 'instanceUrl',
label: 'Instance URL',
type: 'string',
required: true,
readOnly: false,
value: null,
placeholder: null,
description: 'Your subdomain as https://{yoursubdomain}.airbrake.io',
clickToCopy: false,
},
{
key: 'authToken',
label: 'Auth Token',
type: 'string',
required: true,
readOnly: false,
value: null,
placeholder: null,
description: 'Airbrake Auth Token of your account.',
clickToCopy: false,
},
],
verifyCredentials,
isStillVerified,
};

View File

@@ -0,0 +1,8 @@
import verifyCredentials from './verify-credentials.js';
const isStillVerified = async ($) => {
await verifyCredentials($);
return true;
};
export default isStillVerified;

View File

@@ -0,0 +1,14 @@
const verifyCredentials = async ($) => {
await $.http.get(`/api/v4/projects?key=${$.auth.data.authToken}`, {
additionalProperties: {
skipAddingAuthToken: true,
},
});
await $.auth.set({
screenName: $.auth.data.screenName,
authToken: $.auth.data.authToken,
});
};
export default verifyCredentials;

View File

@@ -0,0 +1,10 @@
const addAuthToken = ($, requestConfig) => {
if (requestConfig.additionalProperties?.skipAddingAuthToken)
return requestConfig;
requestConfig.url = requestConfig.url + `?key=${$.auth.data.authToken}`;
return requestConfig;
};
export default addAuthToken;

View File

@@ -0,0 +1,11 @@
const setBaseUrl = ($, requestConfig) => {
const subdomain = $.auth.data.instanceUrl;
if (subdomain) {
requestConfig.baseURL = `https://${subdomain}.airbrake.io`;
}
return requestConfig;
};
export default setBaseUrl;

View File

@@ -0,0 +1,17 @@
import defineApp from '../../helpers/define-app.js';
import setBaseUrl from './common/set-base-url.js';
import auth from './auth/index.js';
import addAuthToken from './common/add-auth-token.js';
export default defineApp({
name: 'Airbrake',
key: 'airbrake',
iconUrl: '{BASE_URL}/apps/airbrake/assets/favicon.svg',
authDocUrl: 'https://automatisch.io/docs/apps/airbrake/connection',
supportsConnections: true,
baseUrl: 'https://www.airbrake.io',
apiBaseUrl: '',
primaryColor: 'f58c54',
beforeRequest: [setBaseUrl, addAuthToken],
auth,
});

View File

@@ -1,5 +1,6 @@
import defineAction from '../../../../helpers/define-action.js';
import base64ToString from './transformers/base64-to-string.js';
import capitalize from './transformers/capitalize.js';
import extractEmailAddress from './transformers/extract-email-address.js';
import extractNumber from './transformers/extract-number.js';
@@ -8,10 +9,12 @@ import lowercase from './transformers/lowercase.js';
import markdownToHtml from './transformers/markdown-to-html.js';
import pluralize from './transformers/pluralize.js';
import replace from './transformers/replace.js';
import stringToBase64 from './transformers/string-to-base64.js';
import trimWhitespace from './transformers/trim-whitespace.js';
import useDefaultValue from './transformers/use-default-value.js';
const transformers = {
base64ToString,
capitalize,
extractEmailAddress,
extractNumber,
@@ -20,6 +23,7 @@ const transformers = {
markdownToHtml,
pluralize,
replace,
stringToBase64,
trimWhitespace,
useDefaultValue,
};
@@ -37,6 +41,7 @@ export default defineAction({
required: true,
variables: true,
options: [
{ label: 'Base64 to String', value: 'base64ToString' },
{ label: 'Capitalize', value: 'capitalize' },
{ label: 'Convert HTML to Markdown', value: 'htmlToMarkdown' },
{ label: 'Convert Markdown to HTML', value: 'markdownToHtml' },
@@ -45,6 +50,7 @@ export default defineAction({
{ label: 'Lowercase', value: 'lowercase' },
{ label: 'Pluralize', value: 'pluralize' },
{ label: 'Replace', value: 'replace' },
{ label: 'String to Base64', value: 'stringToBase64' },
{ label: 'Trim Whitespace', value: 'trimWhitespace' },
{ label: 'Use Default Value', value: 'useDefaultValue' },
],

View File

@@ -0,0 +1,8 @@
const base64ToString = ($) => {
const input = $.step.parameters.input;
const decodedString = Buffer.from(input, 'base64').toString('utf8');
return decodedString;
};
export default base64ToString;

View File

@@ -0,0 +1,8 @@
const stringtoBase64 = ($) => {
const input = $.step.parameters.input;
const base64String = Buffer.from(input).toString('base64');
return base64String;
};
export default stringtoBase64;

View File

@@ -1,3 +1,4 @@
import base64ToString from './text/base64-to-string.js';
import capitalize from './text/capitalize.js';
import extractEmailAddress from './text/extract-email-address.js';
import extractNumber from './text/extract-number.js';
@@ -6,6 +7,7 @@ import lowercase from './text/lowercase.js';
import markdownToHtml from './text/markdown-to-html.js';
import pluralize from './text/pluralize.js';
import replace from './text/replace.js';
import stringToBase64 from './text/string-to-base64.js';
import trimWhitespace from './text/trim-whitespace.js';
import useDefaultValue from './text/use-default-value.js';
import performMathOperation from './numbers/perform-math-operation.js';
@@ -15,6 +17,7 @@ import formatPhoneNumber from './numbers/format-phone-number.js';
import formatDateTime from './date-time/format-date-time.js';
const options = {
base64ToString,
capitalize,
extractEmailAddress,
extractNumber,
@@ -23,6 +26,7 @@ const options = {
markdownToHtml,
pluralize,
replace,
stringToBase64,
trimWhitespace,
useDefaultValue,
performMathOperation,

View File

@@ -0,0 +1,12 @@
const base64ToString = [
{
label: 'Input',
key: 'input',
type: 'string',
required: true,
description: 'Text that will be converted from Base64 to string.',
variables: true,
},
];
export default base64ToString;

View File

@@ -0,0 +1,12 @@
const stringToBase64 = [
{
label: 'Input',
key: 'input',
type: 'string',
required: true,
description: 'Text that will be converted to Base64.',
variables: true,
},
];
export default stringToBase64;

View File

@@ -18,7 +18,9 @@ const port = process.env.PORT || '3000';
const serveWebAppSeparately =
process.env.SERVE_WEB_APP_SEPARATELY === 'true' ? true : false;
let apiUrl = new URL(`${protocol}://${host}:${port}`).toString();
let apiUrl = new URL(
process.env.API_URL || `${protocol}://${host}:${port}`
).toString();
apiUrl = apiUrl.substring(0, apiUrl.length - 1);
// use apiUrl by default, which has less priority over the following cases
@@ -88,6 +90,10 @@ const appConfig = {
licenseKey: process.env.LICENSE_KEY,
sentryDsn: process.env.SENTRY_DSN,
CI: process.env.CI === 'true',
disableNotificationsPage: process.env.DISABLE_NOTIFICATIONS_PAGE === 'true',
disableFavicon: process.env.DISABLE_FAVICON === 'true',
additionalDrawerLink: process.env.ADDITIONAL_DRAWER_LINK,
additionalDrawerLinkText: process.env.ADDITIONAL_DRAWER_LINK_TEXT,
};
if (!appConfig.encryptionKey) {

View File

@@ -1,7 +1,10 @@
import appConfig from '../../config/app.js';
import User from '../../models/user.js';
import Role from '../../models/role.js';
const registerUser = async (_parent, params) => {
if (!appConfig.isCloud) return;
const { fullName, email, password } = params.input;
const existingUser = await User.query().findOne({

View File

@@ -1,9 +1,17 @@
import appConfig from '../../config/app.js';
import { hasValidLicense } from '../../helpers/license.ee.js';
import Config from '../../models/config.js';
const getConfig = async (_parent, params) => {
if (!(await hasValidLicense())) return {};
const defaultConfig = {
disableNotificationsPage: appConfig.disableNotificationsPage,
disableFavicon: appConfig.disableFavicon,
additionalDrawerLink: appConfig.additionalDrawerLink,
additionalDrawerLinkText: appConfig.additionalDrawerLinkText,
};
const configQuery = Config.query();
if (Array.isArray(params.keys)) {
@@ -18,7 +26,7 @@ const getConfig = async (_parent, params) => {
computedConfig[key] = value?.data;
return computedConfig;
}, {});
}, defaultConfig);
};
export default getConfig;

View File

@@ -2,6 +2,7 @@ import { vi, describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import app from '../../app';
import { createConfig } from '../../../test/factories/config';
import appConfig from '../../config/app';
import * as license from '../../helpers/license.ee';
describe('graphQL getConfig query', () => {
@@ -56,6 +57,10 @@ describe('graphQL getConfig query', () => {
[configOne.key]: configOne.value.data,
[configTwo.key]: configTwo.value.data,
[configThree.key]: configThree.value.data,
disableNotificationsPage: false,
disableFavicon: false,
additionalDrawerLink: undefined,
additionalDrawerLinkText: undefined,
},
},
};
@@ -82,6 +87,48 @@ describe('graphQL getConfig query', () => {
getConfig: {
[configOne.key]: configOne.value.data,
[configTwo.key]: configTwo.value.data,
disableNotificationsPage: false,
disableFavicon: false,
additionalDrawerLink: undefined,
additionalDrawerLinkText: undefined,
},
},
};
expect(response.body).toEqual(expectedResponsePayload);
});
});
describe('and with different defaults', () => {
beforeEach(async () => {
vi.spyOn(appConfig, 'disableNotificationsPage', 'get').mockReturnValue(
true
);
vi.spyOn(appConfig, 'disableFavicon', 'get').mockReturnValue(true);
vi.spyOn(appConfig, 'additionalDrawerLink', 'get').mockReturnValue(
'https://automatisch.io'
);
vi.spyOn(appConfig, 'additionalDrawerLinkText', 'get').mockReturnValue(
'Automatisch'
);
});
it('should return custom config', async () => {
const response = await request(app)
.post('/graphql')
.send({ query })
.expect(200);
const expectedResponsePayload = {
data: {
getConfig: {
[configOne.key]: configOne.value.data,
[configTwo.key]: configTwo.value.data,
[configThree.key]: configThree.value.data,
disableNotificationsPage: true,
disableFavicon: true,
additionalDrawerLink: 'https://automatisch.io',
additionalDrawerLinkText: 'Automatisch',
},
},
};

View File

@@ -6,31 +6,6 @@ import { createRole } from '../../../test/factories/role';
import { createUser } from '../../../test/factories/user';
describe('graphQL getCurrentUser query', () => {
describe('with unauthenticated user', () => {
it('should throw not authorized error', async () => {
const invalidUserToken = 'invalid-token';
const query = `
query {
getCurrentUser {
id
email
}
}
`;
const response = await request(app)
.post('/graphql')
.set('Authorization', invalidUserToken)
.send({ query })
.expect(200);
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toEqual('Not Authorised!');
});
});
describe('with authenticated user', () => {
let role, currentUser, token, requestObject;
beforeEach(async () => {
@@ -101,5 +76,4 @@ describe('graphQL getCurrentUser query', () => {
'Cannot query field "password" on type "User".'
);
});
});
});

View File

@@ -40,23 +40,7 @@ describe('graphQL getExecutions query', () => {
}
`;
const invalidToken = 'invalid-token';
describe('with unauthenticated user', () => {
it('should throw not authorized error', async () => {
const response = await request(app)
.post('/graphql')
.set('Authorization', invalidToken)
.send({ query })
.expect(200);
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toEqual('Not Authorised!');
});
});
describe('with authenticated user', () => {
describe('and without permissions', () => {
describe('and without correct permissions', () => {
it('should throw not authorized error', async () => {
const userWithoutPermissions = await createUser();
const token = createAuthTokenByUserId(userWithoutPermissions.id);
@@ -485,5 +469,4 @@ describe('graphQL getExecutions query', () => {
});
});
});
});
});

View File

@@ -40,23 +40,6 @@ describe('graphQL getFlow query', () => {
`;
};
describe('with unauthenticated user', () => {
it('should throw not authorized error', async () => {
const invalidToken = 'invalid-token';
const flow = await createFlow();
const response = await request(app)
.post('/graphql')
.set('Authorization', invalidToken)
.send({ query: query(flow.id) })
.expect(200);
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toEqual('Not Authorised!');
});
});
describe('with authenticated user', () => {
describe('and without permissions', () => {
it('should throw not authorized error', async () => {
const userWithoutPermissions = await createUser();
@@ -145,9 +128,7 @@ describe('graphQL getFlow query', () => {
{
appKey: actionStep.appKey,
connection: {
createdAt: actionConnection.createdAt
.getTime()
.toString(),
createdAt: actionConnection.createdAt.getTime().toString(),
id: actionConnection.id,
verified: actionConnection.verified,
},
@@ -234,9 +215,7 @@ describe('graphQL getFlow query', () => {
{
appKey: actionStep.appKey,
connection: {
createdAt: actionConnection.createdAt
.getTime()
.toString(),
createdAt: actionConnection.createdAt.getTime().toString(),
id: actionConnection.id,
verified: actionConnection.verified,
},
@@ -258,5 +237,4 @@ describe('graphQL getFlow query', () => {
});
});
});
});
});

View File

@@ -17,7 +17,6 @@ describe('graphQL getRole query', () => {
userWithoutPermissions,
tokenWithPermissions,
tokenWithoutPermissions,
invalidToken,
permissionOne,
permissionTwo;
@@ -74,24 +73,8 @@ describe('graphQL getRole query', () => {
tokenWithoutPermissions = createAuthTokenByUserId(
userWithoutPermissions.id
);
invalidToken = 'invalid-token';
});
describe('with unauthenticated user', () => {
it('should throw not authorized error', async () => {
const response = await request(app)
.post('/graphql')
.set('Authorization', invalidToken)
.send({ query: queryWithValidRole })
.expect(200);
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toEqual('Not Authorised!');
});
});
describe('with authenticated user', () => {
describe('and with valid license', () => {
beforeEach(async () => {
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
@@ -178,5 +161,4 @@ describe('graphQL getRole query', () => {
});
});
});
});
});

View File

@@ -15,8 +15,7 @@ describe('graphQL getRoles query', () => {
userWithPermissions,
userWithoutPermissions,
tokenWithPermissions,
tokenWithoutPermissions,
invalidToken;
tokenWithoutPermissions;
beforeEach(async () => {
currentUserRole = await createRole({ name: 'Current user role' });
@@ -53,24 +52,8 @@ describe('graphQL getRoles query', () => {
tokenWithoutPermissions = createAuthTokenByUserId(
userWithoutPermissions.id
);
invalidToken = 'invalid-token';
});
describe('with unauthenticated user', () => {
it('should throw not authorized error', async () => {
const response = await request(app)
.post('/graphql')
.set('Authorization', invalidToken)
.send({ query })
.expect(200);
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toEqual('Not Authorised!');
});
});
describe('with authenticated user', () => {
describe('and with valid license', () => {
beforeEach(async () => {
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
@@ -148,5 +131,4 @@ describe('graphQL getRoles query', () => {
});
});
});
});
});

View File

@@ -16,22 +16,6 @@ describe('graphQL getTrialStatus query', () => {
}
`;
const invalidToken = 'invalid-token';
describe('with unauthenticated user', () => {
it('should throw not authorized error', async () => {
const response = await request(app)
.post('/graphql')
.set('Authorization', invalidToken)
.send({ query })
.expect(200);
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toEqual('Not Authorised!');
});
});
describe('with authenticated user', () => {
let user, userToken;
beforeEach(async () => {
@@ -113,5 +97,4 @@ describe('graphQL getTrialStatus query', () => {
});
});
});
});
});

View File

@@ -8,31 +8,6 @@ import { createPermission } from '../../../test/factories/permission';
import { createUser } from '../../../test/factories/user';
describe('graphQL getUser query', () => {
describe('with unauthenticated user', () => {
it('should throw not authorized error', async () => {
const invalidUserId = '123123123';
const query = `
query {
getUser(id: "${invalidUserId}") {
id
email
}
}
`;
const response = await request(app)
.post('/graphql')
.set('Authorization', 'invalid-token')
.send({ query })
.expect(200);
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toEqual('Not Authorised!');
});
});
describe('with authenticated user', () => {
describe('and without permissions', () => {
it('should throw not authorized error', async () => {
const userWithoutPermissions = await createUser();
@@ -84,9 +59,7 @@ describe('graphQL getUser query', () => {
});
token = createAuthTokenByUserId(currentUser.id);
requestObject = request(app)
.post('/graphql')
.set('Authorization', token);
requestObject = request(app).post('/graphql').set('Authorization', token);
});
it('should return user data for a valid user id', async () => {
@@ -170,5 +143,4 @@ describe('graphQL getUser query', () => {
expect(response.body.errors[0].message).toEqual('NotFoundError');
});
});
});
});

View File

@@ -30,20 +30,6 @@ describe('graphQL getUsers query', () => {
}
`;
describe('with unauthenticated user', () => {
it('should throw not authorized error', async () => {
const response = await request(app)
.post('/graphql')
.set('Authorization', 'invalid-token')
.send({ query })
.expect(200);
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toEqual('Not Authorised!');
});
});
describe('with authenticated user', () => {
describe('and without permissions', () => {
it('should throw not authorized error', async () => {
const userWithoutPermissions = await createUser();
@@ -86,9 +72,7 @@ describe('graphQL getUsers query', () => {
});
token = createAuthTokenByUserId(currentUser.id);
requestObject = request(app)
.post('/graphql')
.set('Authorization', token);
requestObject = request(app).post('/graphql').set('Authorization', token);
});
it('should return users data', async () => {
@@ -161,5 +145,4 @@ describe('graphQL getUsers query', () => {
);
});
});
});
});

View File

@@ -3,7 +3,7 @@ import jwt from 'jsonwebtoken';
import appConfig from '../config/app.js';
import User from '../models/user.js';
const isAuthenticated = rule()(async (_parent, _args, req) => {
export const isAuthenticated = async (_parent, _args, req) => {
const token = req.headers['authorization'];
if (token == null) return false;
@@ -26,12 +26,13 @@ const isAuthenticated = rule()(async (_parent, _args, req) => {
} catch (error) {
return false;
}
});
};
const authentication = shield(
{
const isAuthenticatedRule = rule()(isAuthenticated);
export const authenticationRules = {
Query: {
'*': isAuthenticated,
'*': isAuthenticatedRule,
getAutomatischInfo: allow,
getConfig: allow,
getNotifications: allow,
@@ -39,16 +40,18 @@ const authentication = shield(
listSamlAuthProviders: allow,
},
Mutation: {
'*': isAuthenticated,
'*': isAuthenticatedRule,
forgotPassword: allow,
login: allow,
registerUser: allow,
resetPassword: allow,
},
},
{
};
const authenticationOptions = {
allowExternalErrors: true,
}
);
};
const authentication = shield(authenticationRules, authenticationOptions);
export default authentication;

View File

@@ -0,0 +1,78 @@
import { describe, it, expect, vi } from 'vitest';
import { allow } from 'graphql-shield';
import jwt from 'jsonwebtoken';
import User from '../models/user.js';
import { isAuthenticated, authenticationRules } from './authentication.js';
vi.mock('jsonwebtoken');
vi.mock('../models/user.js');
describe('isAuthenticated', () => {
it('should return false if no token is provided', async () => {
const req = { headers: {} };
expect(await isAuthenticated(null, null, req)).toBe(false);
});
it('should return false if token is invalid', async () => {
jwt.verify.mockImplementation(() => {
throw new Error('invalid token');
});
const req = { headers: { authorization: 'invalidToken' } };
expect(await isAuthenticated(null, null, req)).toBe(false);
});
it('should return true if token is valid', async () => {
jwt.verify.mockReturnValue({ userId: '123' });
User.query.mockReturnValue({
findById: vi.fn().mockReturnValue({
leftJoinRelated: vi.fn().mockReturnThis(),
withGraphFetched: vi
.fn()
.mockResolvedValue({ id: '123', role: {}, permissions: {} }),
}),
});
const req = { headers: { authorization: 'validToken' } };
expect(await isAuthenticated(null, null, req)).toBe(true);
});
});
describe('authentication rules', () => {
const getQueryAndMutationNames = (rules) => {
const queries = Object.keys(rules.Query || {});
const mutations = Object.keys(rules.Mutation || {});
return { queries, mutations };
};
const { queries, mutations } = getQueryAndMutationNames(authenticationRules);
describe('for queries', () => {
queries.forEach((query) => {
it(`should apply correct rule for query: ${query}`, () => {
const ruleApplied = authenticationRules.Query[query];
if (query === '*') {
expect(ruleApplied.func).toBe(isAuthenticated);
} else {
expect(ruleApplied).toEqual(allow);
}
});
});
});
describe('for mutations', () => {
mutations.forEach((mutation) => {
it(`should apply correct rule for mutation: ${mutation}`, () => {
const ruleApplied = authenticationRules.Mutation[mutation];
if (mutation === '*') {
expect(ruleApplied.func).toBe(isAuthenticated);
} else {
expect(ruleApplied).toBe(allow);
}
});
});
});
});

View File

@@ -1,6 +1,9 @@
import * as path from 'path';
import * as fs from 'fs';
import * as handlebars from 'handlebars';
import path from 'path';
import fs from 'fs';
import handlebars from 'handlebars';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const compileEmail = (emailPath, replacements = {}) => {
const filePath = path.join(__dirname, `../views/emails/${emailPath}.ee.hbs`);

View File

@@ -15,7 +15,7 @@ const webUIHandler = async (app) => {
app.use(express.static(webBuildPath));
app.get('*', (_req, res) => {
res.set('Content-Security-Policy', 'frame-ancestors: none;');
res.set('Content-Security-Policy', 'frame-ancestors \'none\';');
res.set('X-Frame-Options', 'DENY');
res.sendFile(indexHtml);

View File

@@ -1,11 +0,0 @@
root = true
[*]
indent_style = space
indent_size = 2
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false

View File

@@ -1 +0,0 @@
/dist

View File

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

View File

@@ -1,9 +0,0 @@
*-debug.log
*-error.log
/.nyc_output
/dist
/lib
/package-lock.json
/tmp
node_modules
oclif.manifest.json

View File

@@ -1,4 +0,0 @@
# `@automatisch/cli`
The open source Zapier alternative. Build workflow automation without spending
time and money.

View File

@@ -1,5 +0,0 @@
#!/usr/bin/env node
const oclif = require('@oclif/core')
oclif.run().then(require('@oclif/core/flush')).catch(require('@oclif/core/handle'))

View File

@@ -1,3 +0,0 @@
@echo off
node "%~dp0\automatisch" %*

View File

@@ -1,73 +0,0 @@
{
"name": "@automatisch/cli",
"version": "0.10.0",
"license": "See LICENSE file",
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.",
"contributors": [
{
"name": "automatisch contributors",
"url": "https://github.com/automatisch/automatisch/graphs/contributors"
}
],
"homepage": "https://github.com/automatisch/automatisch#readme",
"main": "dist/index.js",
"bin": {
"automatisch": "./bin/automatisch"
},
"files": [
"/bin",
"/dist",
"oclif.manifest.json"
],
"repository": {
"type": "git",
"url": "git+https://github.com/automatisch/automatisch.git"
},
"scripts": {
"build": "shx rm -rf dist && tsc -b",
"build:watch": "nodemon --watch 'src/**/*.ts' --exec 'shx rm -rf dist && tsc -b' --ext 'ts'",
"lint": "eslint . --ext .js --ignore-path ../../.eslintignore",
"postpack": "shx rm -f oclif.manifest.json",
"posttest": "yarn lint",
"prepack": "yarn build && oclif manifest && oclif readme",
"version": "oclif readme && git add README.md"
},
"dependencies": {
"@automatisch/backend": "^0.10.0",
"@oclif/core": "^1",
"@oclif/plugin-help": "^5",
"@oclif/plugin-plugins": "^2.0.1",
"dotenv": "^10.0.0"
},
"devDependencies": {
"@oclif/test": "^2",
"@types/node": "^16.9.4",
"eslint-config-oclif": "^4",
"eslint-config-oclif-typescript": "^1.0.2",
"globby": "^11",
"oclif": "^2",
"shx": "^0.3.3",
"ts-node": "^10.2.1",
"tslib": "^2.3.1",
"typescript": "^4.6.3"
},
"oclif": {
"bin": "automatisch",
"dirname": "automatisch",
"commands": "./dist/commands",
"plugins": [
"@oclif/plugin-help",
"@oclif/plugin-plugins"
]
},
"engines": {
"node": ">=12.0.0"
},
"bugs": {
"url": "https://github.com/automatisch/automatisch/issues"
},
"types": "dist/index.d.ts",
"publishConfig": {
"access": "public"
}
}

View File

@@ -1,50 +0,0 @@
import { readFileSync } from 'fs';
import { Command, Flags } from '@oclif/core';
import * as dotenv from 'dotenv';
import process from 'process';
export default class StartWorker extends Command {
static description = 'Run automatisch worker';
static flags = {
env: Flags.string({
multiple: true,
char: 'e',
}),
'env-file': Flags.string(),
};
async prepareEnvVars() {
const { flags } = await this.parse(StartWorker);
if (flags['env-file']) {
const envFile = readFileSync(flags['env-file'], 'utf8');
const envConfig = dotenv.parse(envFile);
for (const key in envConfig) {
const value = envConfig[key];
process.env[key] = value;
}
}
if (flags.env) {
for (const env of flags.env) {
const [key, value] = env.split('=');
process.env[key] = value;
}
}
// must serve until more customization is introduced
delete process.env.SERVE_WEB_APP_SEPARATELY;
}
async runWorker() {
await import('@automatisch/backend/worker');
}
async run() {
await this.prepareEnvVars();
await this.runWorker();
}
}

View File

@@ -1,96 +0,0 @@
import { readFileSync } from 'fs';
import { Command, Flags } from '@oclif/core';
import * as dotenv from 'dotenv';
import process from 'process';
export default class Start extends Command {
static description = 'Run automatisch';
static flags = {
env: Flags.string({
multiple: true,
char: 'e',
}),
'env-file': Flags.string(),
};
get isProduction() {
return process.env.APP_ENV === 'production';
}
async prepareEnvVars() {
const { flags } = await this.parse(Start);
if (flags['env-file']) {
const envFile = readFileSync(flags['env-file'], 'utf8');
const envConfig = dotenv.parse(envFile);
for (const key in envConfig) {
const value = envConfig[key];
process.env[key] = value;
}
}
if (flags.env) {
for (const env of flags.env) {
const [key, value] = env.split('=');
process.env[key] = value;
}
}
// must serve until more customization is introduced
delete process.env.SERVE_WEB_APP_SEPARATELY;
}
async createDatabaseAndUser() {
const utils = await import('@automatisch/backend/database-utils');
await utils.createDatabaseAndUser(
process.env.POSTGRES_DATABASE,
process.env.POSTGRES_USERNAME
);
}
async runMigrationsIfNeeded() {
const { logger } = await import('@automatisch/backend/logger');
const database = await import('@automatisch/backend/database');
const migrator = database.client.migrate;
const [, pendingMigrations] = await migrator.list();
const pendingMigrationsCount = pendingMigrations.length;
const needsToMigrate = pendingMigrationsCount > 0;
if (needsToMigrate) {
logger.info(`Processing ${pendingMigrationsCount} migrations.`);
await migrator.latest();
logger.info(`Completed ${pendingMigrationsCount} migrations.`);
} else {
logger.info('No migrations needed.');
}
}
async seedUser() {
const utils = await import('@automatisch/backend/database-utils');
await utils.createUser();
}
async runApp() {
await import('@automatisch/backend/server');
}
async run() {
await this.prepareEnvVars();
if (!this.isProduction) {
await this.createDatabaseAndUser();
}
await this.runMigrationsIfNeeded();
await this.seedUser();
await this.runApp();
}
}

View File

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

View File

@@ -1,19 +0,0 @@
{
"compilerOptions": {
"declaration": true,
"allowJs": true,
"esModuleInterop": true,
"importHelpers": true,
"lib": ["es2021"],
"module": "commonjs",
"moduleResolution": "node",
"noImplicitAny": false,
"outDir": "dist",
"rootDir": "src",
"skipLibCheck": true,
"strict": true,
"target": "es2021",
"typeRoots": ["node_modules/@types", "./src/types"]
},
"include": ["src/**/*"]
}

File diff suppressed because it is too large Load Diff

View File

@@ -32,6 +32,12 @@ export default defineConfig({
],
sidebar: {
'/apps/': [
{
text: 'Airbrake',
collapsible: true,
collapsed: true,
items: [{ text: 'Connection', link: '/apps/airbrake/connection' }],
},
{
text: 'Carbone',
collapsible: true,
@@ -305,7 +311,7 @@ export default defineConfig({
collapsed: true,
items: [
{ text: 'Actions', link: '/apps/removebg/actions' },
{ text: 'Connection', link: '/apps/removebg/connection' }
{ text: 'Connection', link: '/apps/removebg/connection' },
],
},
{

View File

@@ -15,7 +15,7 @@ Please be careful with the `ENCRYPTION_KEY` and `WEBHOOK_SECRET_KEY` environment
:::
| Variable Name | Type | Default Value | Description |
| --------------------------- | ------- | ------------------ | ---------------------------------------------------------------------------------------------------- |
| ---------------------------- | ------- | ------------------ | ----------------------------------------------------------------------------------- |
| `HOST` | string | `localhost` | HTTP Host |
| `PROTOCOL` | string | `http` | HTTP Protocol |
| `PORT` | string | `3000` | HTTP Port |
@@ -42,3 +42,5 @@ Please be careful with the `ENCRYPTION_KEY` and `WEBHOOK_SECRET_KEY` environment
| `ENABLE_BULLMQ_DASHBOARD` | boolean | `false` | Enable BullMQ Dashboard |
| `BULLMQ_DASHBOARD_USERNAME` | string | | Username to login BullMQ Dashboard |
| `BULLMQ_DASHBOARD_PASSWORD` | string | | Password to login BullMQ Dashboard |
| `DISABLE_NOTIFICATIONS_PAGE` | boolean | `false` | Enable/Disable notifications page |
| `DISABLE_FAVICON` | boolean | `false` | Enable/Disable favicon |

View File

@@ -1,7 +1,7 @@
# Telemetry
:::info
We want to be very transparent about the data we collect and how we use it. Therefore, we have abstracted all of the code we use with our telemetry system into a single, easily accessible place. You can check the code [here](https://github.com/automatisch/automatisch/blob/main/packages/backend/src/helpers/telemetry/index.ts) and let us know if you have any suggestions for changes.
We want to be very transparent about the data we collect and how we use it. Therefore, we have abstracted all of the code we use with our telemetry system into a single, easily accessible place. You can check the code [here](https://github.com/automatisch/automatisch/blob/main/packages/backend/src/helpers/telemetry/index.js) and let us know if you have any suggestions for changes.
:::
Automatisch comes with a built-in telemetry system that collects anonymous usage data. This data is used to help us improve the product and to make sure we are focusing on the right features. While we're doing it, we don't collect any personal information. You can also disable the telemetry system by setting the `TELEMETRY_ENABLED` environment variable. See the [environment variables](/advanced/configuration#environment-variables) section for more information.

View File

@@ -0,0 +1,13 @@
# Airbrake
:::info
This page explains the steps you need to follow to set up the Airbrake
connection in Automatisch. If any of the steps are outdated, please let us know!
:::
1. Login to your Airbrake account: [https://www.airbrake.io/](https://www.airbrake.io/).
2. Go to your profile & notifications page.
3. Copy `Auth Token` from the page to the `Auth Token` field on Automatisch.
4. Fill the instance URL field with your subdomain. (https://{yoursubdomain}.airbrake.io)
5. Write any screen name to be displayed in Automatisch.
6. Now, you can start using the Airbrake connection with Automatisch.

View File

@@ -16,13 +16,13 @@ The build integrations section is best understood when read from beginning to en
## Add actions to the app.
Open the `thecatapi/index.ts` file and add the highlighted lines for actions.
Open the `thecatapi/index.js` file and add the highlighted lines for actions.
```typescript{4,17}
import defineApp from '../../helpers/define-app';
import auth from './auth';
import triggers from './triggers';
import actions from './actions';
```javascript{4,17}
import defineApp from '../../helpers/define-app.js';
import auth from './auth/index.js';
import triggers from './triggers/index.js';
import actions from './actions/index.js';
export default defineApp({
name: 'The cat API',
@@ -41,24 +41,24 @@ export default defineApp({
## Define actions
Create the `actions/index.ts` file inside of the `thecatapi` folder.
Create the `actions/index.js` file inside of the `thecatapi` folder.
```typescript
import markCatImageAsFavorite from './mark-cat-image-as-favorite';
```javascript
import markCatImageAsFavorite from './mark-cat-image-as-favorite/index.js';
export default [markCatImageAsFavorite];
```
:::tip
If you add new actions, you need to add them to the actions/index.ts file and export all actions as an array.
If you add new actions, you need to add them to the actions/index.js file and export all actions as an array.
:::
## Add metadata
Create the `actions/mark-cat-image-as-favorite/index.ts` file inside the `thecatapi` folder.
Create the `actions/mark-cat-image-as-favorite/index.js` file inside the `thecatapi` folder.
```typescript
import defineAction from '../../../../helpers/define-action';
```javascript
import defineAction from '../../../../helpers/define-action.js';
export default defineAction({
name: 'Mark the cat image as favorite',
@@ -68,7 +68,7 @@ export default defineAction({
{
label: 'Image ID',
key: 'imageId',
type: 'string' as const,
type: 'string',
required: true,
description: 'The ID of the cat image you want to mark as favorite.',
variables: true,
@@ -91,10 +91,10 @@ Let's briefly explain what we defined here.
## Implement the action
Open the `actions/mark-cat-image-as-favorite.ts` file and add the highlighted lines.
Open the `actions/mark-cat-image-as-favorite.js` file and add the highlighted lines.
```typescript{7-20}
import defineAction from '../../../../helpers/define-action';
```javascript{7-20}
import defineAction from '../../../../helpers/define-action.js';
export default defineAction({
// ...
@@ -104,7 +104,7 @@ export default defineAction({
const imageId = $.step.parameters.imageId;
const headers = {
'x-api-key': $.auth.data.apiKey as string,
'x-api-key': $.auth.data.apiKey,
};
const response = await $.http.post(

View File

@@ -27,17 +27,17 @@ cd packages/backend/src/apps
mkdir thecatapi
```
We need to create an `index.ts` file inside of the `thecatapi` folder.
We need to create an `index.js` file inside of the `thecatapi` folder.
```bash
cd thecatapi
touch index.ts
touch index.js
```
Then let's define the app inside of the `index.ts` file as follows:
Then let's define the app inside of the `index.js` file as follows:
```typescript
import defineApp from '../../helpers/define-app';
```javascript
import defineApp from '../../helpers/define-app.js';
export default defineApp({
name: 'The cat API',

View File

@@ -24,11 +24,11 @@ You can find detailed documentation of the cat API [here](https://docs.thecatapi
## Add auth to the app
Open the `thecatapi/index.ts` file and add the highlighted lines for authentication.
Open the `thecatapi/index.js` file and add the highlighted lines for authentication.
```typescript{2,13}
import defineApp from '../../helpers/define-app';
import auth from './auth';
```javascript{2,13}
import defineApp from '../../helpers/define-app.js';
import auth from './auth/index.js';
export default defineApp({
name: 'The cat API',
@@ -45,22 +45,22 @@ export default defineApp({
## Define auth fields
Let's create the `auth/index.ts` file inside of the `thecatapi` folder.
Let's create the `auth/index.js` file inside of the `thecatapi` folder.
```bash
mkdir auth
touch auth/index.ts
touch auth/index.js
```
Then let's start with defining fields the auth inside of the `auth/index.ts` file as follows:
Then let's start with defining fields the auth inside of the `auth/index.js` file as follows:
```typescript
```javascript
export default {
fields: [
{
key: 'screenName',
label: 'Screen Name',
type: 'string' as const,
type: 'string',
required: true,
readOnly: false,
value: null,
@@ -72,7 +72,7 @@ export default {
{
key: 'apiKey',
label: 'API Key',
type: 'string' as const,
type: 'string',
required: true,
readOnly: false,
value: null,
@@ -101,10 +101,10 @@ If the third-party service you use provides both an API key and OAuth for the au
So until now, we integrated auth folder with the app definition and defined the auth fields. Now we need to verify the credentials that the user entered. We will do that by defining the `verifyCredentials` method.
Start with adding the `verifyCredentials` method to the `auth/index.ts` file.
Start with adding the `verifyCredentials` method to the `auth/index.js` file.
```typescript{1,8}
import verifyCredentials from './verify-credentials';
```javascript{1,8}
import verifyCredentials from './verify-credentials.js';
export default {
fields: [
@@ -115,12 +115,10 @@ export default {
};
```
Let's create the `verify-credentials.ts` file inside the `auth` folder.
Let's create the `verify-credentials.js` file inside the `auth` folder.
```typescript
import { IGlobalVariable } from '@automatisch/types';
const verifyCredentials = async ($: IGlobalVariable) => {
```javascript
const verifyCredentials = async ($) => {
// TODO: Implement verification of the credentials
};
@@ -129,12 +127,10 @@ export default verifyCredentials;
We generally use the `users/me` endpoint or any other endpoint that we can validate the API key or any other credentials that the user provides. For our example, we don't have a specific API endpoint to check whether the credentials are correct or not. So we will randomly pick one of the API endpoints, which will be the `GET /v1/images/search` endpoint. We will send a request to this endpoint with the API key. If the API key is correct, we will get a response from the API. If the API key is incorrect, we will get an error response from the API.
Let's implement the authentication logic that we mentioned above in the `verify-credentials.ts` file.
Let's implement the authentication logic that we mentioned above in the `verify-credentials.js` file.
```typescript
import { IGlobalVariable } from '@automatisch/types';
const verifyCredentials = async ($: IGlobalVariable) => {
```javascript
const verifyCredentials = async ($) => {
await $.http.get('/v1/images/search');
await $.auth.set({
@@ -155,11 +151,11 @@ You must always provide a `screenName` field to auth data in the `verifyCredenti
We have implemented the `verifyCredentials` method. Now we need to check whether the credentials are still valid or not for the test connection functionality in Automatisch. We will do that by defining the `isStillVerified` method.
Start with adding the `isStillVerified` method to the `auth/index.ts` file.
Start with adding the `isStillVerified` method to the `auth/index.js` file.
```typescript{2,10}
import verifyCredentials from './verify-credentials';
import isStillVerified from './is-still-verified';
```javascript{2,10}
import verifyCredentials from './verify-credentials.js';
import isStillVerified from './is-still-verified.js';
export default {
fields: [
@@ -171,13 +167,12 @@ export default {
};
```
Let's create the `is-still-verified.ts` file inside the `auth` folder.
Let's create the `is-still-verified.js` file inside the `auth` folder.
```typescript
import { IGlobalVariable } from '@automatisch/types';
import verifyCredentials from './verify-credentials';
```javascript
import verifyCredentials from './verify-credentials.js';
const isStillVerified = async ($: IGlobalVariable) => {
const isStillVerified = async ($) => {
await verifyCredentials($);
return true;
};

View File

@@ -18,35 +18,35 @@ The build integrations section is best understood when read from beginning to en
### 3-legged OAuth
- [Discord](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/discord/auth/index.ts)
- [Flickr](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/flickr/auth/index.ts)
- [Github](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/github/auth/index.ts)
- [Salesforce](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/salesforce/auth/index.ts)
- [Slack](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/slack/auth/index.ts)
- [Twitter](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/twitter/auth/index.ts)
- [Discord](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/discord/auth/index.js)
- [Flickr](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/flickr/auth/index.js)
- [Github](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/github/auth/index.js)
- [Salesforce](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/salesforce/auth/index.js)
- [Slack](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/slack/auth/index.js)
- [Twitter](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/twitter/auth/index.js)
### OAuth with the refresh token
- [Salesforce](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/salesforce/auth/index.ts)
- [Salesforce](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/salesforce/auth/index.js)
### API key
- [DeepL](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/deepl/auth/index.ts)
- [Twilio](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/twilio/auth/index.ts)
- [SignalWire](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/signalwire/auth/index.ts)
- [SMTP](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/smtp/auth/index.ts)
- [DeepL](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/deepl/auth/index.js)
- [Twilio](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/twilio/auth/index.js)
- [SignalWire](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/signalwire/auth/index.js)
- [SMTP](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/smtp/auth/index.js)
### Without authentication
- [RSS](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/rss/index.ts)
- [Scheduler](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/scheduler/index.ts)
- [RSS](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/rss/index.js)
- [Scheduler](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/scheduler/index.js)
## Triggers
### Polling-based triggers
- [Search tweets - Twitter](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/twitter/triggers/search-tweets/index.ts)
- [New issues - Github](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/github/triggers/new-issues/index.ts)
- [Search tweets - Twitter](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/twitter/triggers/search-tweets/index.js)
- [New issues - Github](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/github/triggers/new-issues/index.js)
### Webhook-based triggers
@@ -54,27 +54,27 @@ The build integrations section is best understood when read from beginning to en
If you are developing a webhook-based trigger, you need to ensure that the webhook is publicly accessible. You can use [ngrok](https://ngrok.com) for this purpose and override the webhook URL by setting the **WEBHOOK_URL** environment variable.
:::
- [New entry - Typeform](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/typeform/triggers/new-entry/index.ts)
- [New entry - Typeform](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/typeform/triggers/new-entry/index.js)
### Pagination with descending order
- [Search tweets - Twitter](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/twitter/triggers/search-tweets/index.ts)
- [New issues - Github](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/github/triggers/new-issues/index.ts)
- [Receive SMS - Twilio](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/twilio/triggers/receive-sms/index.ts)
- [Receive SMS - SignalWire](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/signalwire/triggers/receive-sms/index.ts)
- [New photos - Flickr](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/flickr/triggers/new-photos/index.ts)
- [Search tweets - Twitter](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/twitter/triggers/search-tweets/index.js)
- [New issues - Github](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/github/triggers/new-issues/index.js)
- [Receive SMS - Twilio](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/twilio/triggers/receive-sms/index.js)
- [Receive SMS - SignalWire](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/signalwire/triggers/receive-sms/index.js)
- [New photos - Flickr](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/flickr/triggers/new-photos/index.js)
### Pagination with ascending order
- [New stargazers - Github](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/github/triggers/new-stargazers/index.ts)
- [New watchers - Github](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/github/triggers/new-watchers/index.ts)
- [New stargazers - Github](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/github/triggers/new-stargazers/index.js)
- [New watchers - Github](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/github/triggers/new-watchers/index.js)
## Actions
- [Send a message to channel - Slack](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/slack/actions/send-a-message-to-channel/index.ts)
- [Send SMS - Twilio](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/twilio/actions/send-sms/index.ts)
- [Send a message to channel - Discord](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/discord/actions/send-message-to-channel/index.ts)
- [Create issue - Github](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/github/actions/create-issue/index.ts)
- [Send an email - SMTP](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/smtp/actions/send-email/index.ts)
- [Create tweet - Twitter](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/twitter/actions/create-tweet/index.ts)
- [Translate text - DeepL](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/deepl/actions/translate-text/index.ts)
- [Send a message to channel - Slack](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/slack/actions/send-a-message-to-channel/index.js)
- [Send SMS - Twilio](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/twilio/actions/send-sms/index.js)
- [Send a message to channel - Discord](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/discord/actions/send-message-to-channel/index.js)
- [Create issue - Github](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/github/actions/create-issue/index.js)
- [Send an email - SMTP](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/smtp/actions/send-email/index.js)
- [Create tweet - Twitter](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/twitter/actions/create-tweet/index.js)
- [Translate text - DeepL](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/deepl/actions/translate-text/index.js)

View File

@@ -35,13 +35,13 @@ Here, you can see the folder structure of an example app. We will briefly walk t
├── auth
├── common
├── dynamic-data
├── index.ts
├── index.js
└── triggers
```
## App
The `index.ts` file is the entry point of the app. It contains the definition of the app and the app's metadata. It also includes the list of triggers, actions, and data sources that the app provides. So, whatever you build inside the app, you need to associate it within the `index.ts` file.
The `index.js` file is the entry point of the app. It contains the definition of the app and the app's metadata. It also includes the list of triggers, actions, and data sources that the app provides. So, whatever you build inside the app, you need to associate it within the `index.js` file.
## Auth

View File

@@ -16,11 +16,11 @@ The build integrations section is best understood when read from beginning to en
Before handling authentication and building a trigger and an action, it's better to explain the `global variable` concept in Automatisch. Automatisch provides you the global variable that you need to use with authentication, triggers, actions, and basically all the stuff you will build for the integration.
The global variable is represented as `$` variable in the codebase, and it's a JSON object that contains the following properties:
The global variable is represented as `$` variable in the codebase, and it's a JS object that contains the following properties:
## $.auth.set
```typescript
```javascript
await $.auth.set({
key: 'value',
});
@@ -30,7 +30,7 @@ It's used to set the authentication data, and you can use this method with multi
## $.auth.data
```typescript
```javascript
$.auth.data; // { key: 'value' }
```
@@ -38,7 +38,7 @@ It's used to retrieve the authentication data that we set with `$.auth.set()`. T
## $.app.baseUrl
```typescript
```javascript
$.app.baseUrl; // https://thecatapi.com
```
@@ -46,7 +46,7 @@ It's used to retrieve the base URL of the app that we defined previously. In our
## $.app.apiBaseUrl
```typescript
```javascript
$.app.apiBaseUrl; // https://api.thecatapi.com
```
@@ -54,7 +54,7 @@ It's used to retrieve the API base URL of the app that we defined previously. In
## $.app.auth.fields
```typescript
```javascript
$.app.auth.fields;
```
@@ -64,7 +64,7 @@ It's used to retrieve the fields that we defined in the `auth` section of the ap
It's an HTTP client to be used for making HTTP requests. It's a wrapper around the [axios](https://axios-http.com) library. We use this property when we need to make HTTP requests to the third-party service. The `apiBaseUrl` field we set up in the app will be used as the base URL for the HTTP requests. For example, to search the cat images, we can use the following code:
```typescript
```javascript
await $.http.get('/v1/images/search?order=DESC', {
headers: {
'x-api-key': $.auth.data.apiKey,
@@ -76,15 +76,15 @@ Keep in mind that the HTTP client handles the error with the status code that fa
## $.step.parameters
```typescript
```javascript
$.step.parameters; // { key: 'value' }
```
It refers to the parameters that are set by users in the UI. We use this property when we need to get the parameters for corresponding triggers and actions. For example [Send a message to channel](https://github.com/automatisch/automatisch/blob/main/packages/backend/src/apps/slack/actions/send-a-message-to-channel/post-message.ts) action from Slack integration, we have a step parameter called `message` that we need to use in the action. We can use `$.step.parameters.message` to get the value of the message to send a message to the Slack channel.
It refers to the parameters that are set by users in the UI. We use this property when we need to get the parameters for corresponding triggers and actions. For example [Send a message to channel](https://github.com/automatisch/automatisch/blob/main/packages/backend/src/apps/slack/actions/send-a-message-to-channel/post-message.js) action from Slack integration, we have a step parameter called `message` that we need to use in the action. We can use `$.step.parameters.message` to get the value of the message to send a message to the Slack channel.
## $.pushTriggerItem
```typescript
```javascript
$.pushTriggerItem({
raw: resourceData,
meta: {
@@ -97,7 +97,7 @@ It's used to push trigger data to be processed by Automatisch. It must reflect t
## $.setActionItem
```typescript
```javascript
$.setActionItem({
raw: resourceData,
});

View File

@@ -20,12 +20,12 @@ We used a polling-based HTTP trigger in our example but if you need to use a web
## Add triggers to the app
Open the `thecatapi/index.ts` file and add the highlighted lines for triggers.
Open the `thecatapi/index.js` file and add the highlighted lines for triggers.
```typescript{3,15}
import defineApp from '../../helpers/define-app';
import auth from './auth';
import triggers from './triggers';
```javascript{3,15}
import defineApp from '../../helpers/define-app.js';
import auth from './auth/index.js';
import triggers from './triggers/index.js';
export default defineApp({
name: 'The cat API',
@@ -43,24 +43,24 @@ export default defineApp({
## Define triggers
Create the `triggers/index.ts` file inside of the `thecatapi` folder.
Create the `triggers/index.js` file inside of the `thecatapi` folder.
```typescript
import searchCatImages from './search-cat-images';
```javascript
import searchCatImages from './search-cat-images/index.js';
export default [searchCatImages];
```
:::tip
If you add new triggers, you need to add them to the `triggers/index.ts` file and export all triggers as an array. The order of triggers in this array will be reflected in the Automatisch user interface.
If you add new triggers, you need to add them to the `triggers/index.js` file and export all triggers as an array. The order of triggers in this array will be reflected in the Automatisch user interface.
:::
## Add metadata
Create the `triggers/search-cat-images/index.ts` file inside of the `thecatapi` folder.
Create the `triggers/search-cat-images/index.js` file inside of the `thecatapi` folder.
```typescript
import defineTrigger from '../../../../helpers/define-trigger';
```javascript
import defineTrigger from '../../../../helpers/define-trigger.js';
export default defineTrigger({
name: 'Search cat images',
@@ -93,9 +93,8 @@ Let's briefly explain what we defined here.
Implement the `run` function by adding highlighted lines.
```typescript{1,7-30}
import { IJSONObject } from '@automatisch/types';
import defineTrigger from '../../../../helpers/define-trigger';
```javascript{1,7-30}
import defineTrigger from '../../../../helpers/define-trigger.js';
export default defineTrigger({
// ...
@@ -104,18 +103,18 @@ export default defineTrigger({
let response;
const headers = {
'x-api-key': $.auth.data.apiKey as string,
'x-api-key': $.auth.data.apiKey,
};
do {
let requestPath = `/v1/images/search?page=${page}&limit=10&order=DESC`;
response = await $.http.get(requestPath, { headers });
response.data.forEach((image: IJSONObject) => {
response.data.forEach((image) => {
const dataItem = {
raw: image,
meta: {
internalId: image.id as string
internalId: image.id
},
};

View File

@@ -35,7 +35,7 @@ yarn db:create
```
:::warning
`yarn db:create` commands expect that you have the `postgres` superuser. If not, you can create a superuser called `postgres` manually or you can create the database manually by checking PostgreSQL-related default values from the [app config](https://github.com/automatisch/automatisch/blob/main/packages/backend/src/config/app.ts).
`yarn db:create` commands expect that you have the `postgres` superuser. If not, you can create a superuser called `postgres` manually or you can create the database manually by checking PostgreSQL-related default values from the [app config](https://github.com/automatisch/automatisch/blob/main/packages/backend/src/config/app.js).
:::
Run the database migrations in the backend folder.

View File

@@ -0,0 +1 @@
<svg height="255" preserveAspectRatio="xMidYMid" viewBox="0 0 256 255" width="256" xmlns="http://www.w3.org/2000/svg"><path d="m128.636514 155.746615v-155.23361889h-3.522242v.06873152l-124.60824865 64.03287157v60.8642488h.00597665v3.234366h-.00597665v60.868233l124.60824865 64.747082h3.842989v-98.581914z" fill="#ff8e4a"/><path d="m129.941416 254.328529 125.568498-64.747082v-124.9668478l-125.887253-64.10160309h-2.243237v253.81055289h2.243237" fill="#f48746"/><path d="m109.097837 87.2551595h36.19561v59.2077195h-36.19561z" fill="#ff8e4a"/><path d="m66.1735097 188.397074h14.8639378c9.4102412 0 12.6087471-2.238257 15.6189883-9.988981l8.2796572-21.353587h45.159596l8.280653 21.353587c3.011238 7.750724 6.396016 9.988981 15.805261 9.988981h14.677665v-19.114335h-3.011237c-3.19751 0-4.704622-.689307-5.831222-3.790194l-39.516638-99.3658524h-25.779299l-39.703907 99.3658524c-1.1285915 3.100887-2.632716 3.790194-5.833214 3.790194h-3.0102413zm44.4075333-49.939922 11.478163-30.655253c2.445448-6.714771 5.269417-18.2556889 5.269417-18.2556889h.375533s2.822972 11.5409179 5.269416 18.2556889l11.478163 30.655253z" fill="#fff"/><path d="m231.204856 150.082739v-51.8086223c.235082 4.5233303 2.970397 16.8432063 24.305058 27.8512063v11.653479zm0-53.1623343v1.353712c-.029883-.5926848-.01793-1.0479066 0-1.353712zm.041837-.4392841s-.022911.1534008-.041837.4392841v-.4392841z" fill="#d4763c"/><path d="m231.155051 94.3016342c-.013946.9931207.05877 1.8945993.049805 2.0460078-.01793.2480312-2.220327 16.094132 24.305058 29.777681v-60.863253c-23.325883 12.0349884-24.449494 25.7414475-24.354863 29.0395642" fill="#ff8e4a"/></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -1,4 +0,0 @@
# `@automatisch/types`
The open source Zapier alternative. Build workflow automation without spending
time and money.

View File

@@ -1,20 +0,0 @@
{
"name": "@automatisch/types",
"version": "0.10.0",
"license": "See LICENSE file",
"description": "Type definitions for automatisch",
"homepage": "https://github.com/automatisch/automatisch",
"types": "./index.d.ts",
"scripts": {},
"typeScriptVersion": "4.1",
"repository": {
"type": "git",
"url": "git+https://github.com/automatisch/automatisch.git"
},
"bugs": {
"url": "https://github.com/automatisch/automatisch/issues"
},
"publishConfig": {
"access": "public"
}
}

View File

@@ -5,7 +5,6 @@
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.",
"dependencies": {
"@apollo/client": "^3.6.9",
"@automatisch/types": "^0.10.0",
"@casl/ability": "^6.5.0",
"@casl/react": "^3.1.0",
"@emotion/react": "^11.4.1",

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -2,7 +2,6 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#0059F7" />
<meta

View File

@@ -2,13 +2,6 @@
"short_name": "automatisch",
"name": "automatisch",
"description": "Build workflow automation without spending time and money. No code is required.",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",

View File

@@ -1,4 +1,4 @@
import type { IApp, IField, IJSONObject } from '@automatisch/types';
import type { IApp, IField, IJSONObject } from 'types';
import LoadingButton from '@mui/lab/LoadingButton';
import Alert from '@mui/material/Alert';
import Dialog from '@mui/material/Dialog';

View File

@@ -19,7 +19,7 @@ import InputLabel from '@mui/material/InputLabel';
import OutlinedInput from '@mui/material/OutlinedInput';
import FormControl from '@mui/material/FormControl';
import Box from '@mui/material/Box';
import type { IApp } from '@automatisch/types';
import type { IApp } from 'types';
import * as URLS from 'config/urls';
import AppIcon from 'components/AppIcon';

View File

@@ -1,5 +1,5 @@
import React from 'react';
import type { IField } from '@automatisch/types';
import type { IField } from 'types';
import LoadingButton from '@mui/lab/LoadingButton';
import Alert from '@mui/material/Alert';
import Dialog from '@mui/material/Dialog';

View File

@@ -1,5 +1,5 @@
import React, { useCallback, useMemo } from 'react';
import type { IApp } from '@automatisch/types';
import type { IApp } from 'types';
import { FieldValues, SubmitHandler } from 'react-hook-form';
import { useMutation } from '@apollo/client';
import { CREATE_APP_CONFIG } from 'graphql/mutations/create-app-config';

View File

@@ -1,6 +1,6 @@
import React, { useCallback, useMemo } from 'react';
import { useParams } from 'react-router-dom';
import type { IApp } from '@automatisch/types';
import type { IApp } from 'types';
import { FieldValues, SubmitHandler } from 'react-hook-form';
import { useMutation } from '@apollo/client';
import { UPDATE_APP_AUTH_CLIENT } from 'graphql/mutations/update-app-auth-client';

View File

@@ -15,6 +15,7 @@ import { SvgIconComponent } from '@mui/icons-material';
import AppBar from 'components/AppBar';
import Drawer from 'components/Drawer';
import * as URLS from 'config/urls';
import useFormatMessage from 'hooks/useFormatMessage';
import useCurrentUserAbility from 'hooks/useCurrentUserAbility';
type SettingsLayoutProps = {
@@ -86,19 +87,11 @@ function createDrawerLinks({
return items;
}
const drawerBottomLinks = [
{
Icon: ArrowBackIosNewIcon,
primary: 'adminSettingsDrawer.goBack',
to: '/',
dataTest: 'go-back-drawer-link',
},
];
export default function SettingsLayout({
children,
}: SettingsLayoutProps): React.ReactElement {
const theme = useTheme();
const formatMessage = useFormatMessage();
const currentUserAbility = useCurrentUserAbility();
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('lg'));
const [isDrawerOpen, setDrawerOpen] = React.useState(!matchSmallScreens);
@@ -116,6 +109,15 @@ export default function SettingsLayout({
canUpdateApp: currentUserAbility.can('update', 'App'),
});
const drawerBottomLinks = [
{
Icon: ArrowBackIosNewIcon,
primary: formatMessage('adminSettingsDrawer.goBack'),
to: '/',
dataTest: 'go-back-drawer-link',
},
];
return (
<>
<AppBar

View File

@@ -3,7 +3,7 @@ import { Link } from 'react-router-dom';
import Menu from '@mui/material/Menu';
import type { PopoverProps } from '@mui/material/Popover';
import MenuItem from '@mui/material/MenuItem';
import type { IConnection } from '@automatisch/types';
import type { IConnection } from 'types';
import * as URLS from 'config/urls';
import useFormatMessage from 'hooks/useFormatMessage';

View File

@@ -11,7 +11,7 @@ import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
import { DateTime } from 'luxon';
import * as React from 'react';
import type { IConnection } from '@automatisch/types';
import type { IConnection } from 'types';
import ConnectionContextMenu from 'components/AppConnectionContextMenu';
import { DELETE_CONNECTION } from 'graphql/mutations/delete-connection';
import { TEST_CONNECTION } from 'graphql/queries/test-connection';
@@ -83,8 +83,8 @@ function AppConnectionRow(props: AppConnectionRowProps): React.ReactElement {
enqueueSnackbar(formatMessage('connection.deletedMessage'), {
variant: 'success',
SnackbarProps: {
'data-test': 'snackbar-delete-connection-success'
}
'data-test': 'snackbar-delete-connection-success',
},
});
} else if (action.type === 'test') {
setVerificationVisible(true);

View File

@@ -1,7 +1,7 @@
import * as React from 'react';
import { useQuery } from '@apollo/client';
import type { IConnection } from '@automatisch/types';
import type { IConnection } from 'types';
import { GET_APP_CONNECTIONS } from 'graphql/queries/get-app-connections';
import AppConnectionRow from 'components/AppConnectionRow';
import NoResultFound from 'components/NoResultFound';

View File

@@ -8,7 +8,7 @@ import * as URLS from 'config/urls';
import AppFlowRow from 'components/FlowRow';
import NoResultFound from 'components/NoResultFound';
import useFormatMessage from 'hooks/useFormatMessage';
import type { IFlow } from '@automatisch/types';
import type { IFlow } from 'types';
type AppFlowsProps = {
appKey: string;

View File

@@ -7,7 +7,7 @@ import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos';
import useFormatMessage from 'hooks/useFormatMessage';
import AppIcon from 'components/AppIcon';
import type { IApp } from '@automatisch/types';
import type { IApp } from 'types';
import { CardContent, Typography } from './style';

View File

@@ -7,13 +7,7 @@ import ListItem from '@mui/material/ListItem';
import TextField from '@mui/material/TextField';
import Autocomplete from '@mui/material/Autocomplete';
import Chip from '@mui/material/Chip';
import type {
IApp,
IStep,
ISubstep,
ITrigger,
IAction,
} from '@automatisch/types';
import type { IApp, IStep, ISubstep, ITrigger, IAction } from 'types';
import useFormatMessage from 'hooks/useFormatMessage';
import useApps from 'hooks/useApps';

View File

@@ -6,7 +6,7 @@ import ListItem from '@mui/material/ListItem';
import TextField from '@mui/material/TextField';
import * as React from 'react';
import type { IApp, IConnection, IStep, ISubstep } from '@automatisch/types';
import type { IApp, IConnection, IStep, ISubstep } from 'types';
import AddAppConnection from 'components/AddAppConnection';
import AppAuthClientsDialog from 'components/AppAuthClientsDialog/index.ee';
import FlowSubstepTitle from 'components/FlowSubstepTitle';

View File

@@ -1,9 +1,12 @@
import * as React from 'react';
import { Controller, useFormContext } from 'react-hook-form';
import FormHelperText from '@mui/material/FormHelperText';
import Autocomplete, { AutocompleteProps, createFilterOptions } from '@mui/material/Autocomplete';
import Autocomplete, {
AutocompleteProps,
createFilterOptions,
} from '@mui/material/Autocomplete';
import Typography from '@mui/material/Typography';
import type { IFieldDropdownOption } from '@automatisch/types';
import type { IFieldDropdownOption } from 'types';
interface ControlledAutocompleteProps
extends AutocompleteProps<IFieldDropdownOption, boolean, boolean, boolean> {
@@ -23,8 +26,8 @@ const filterOptions = createFilterOptions<IFieldDropdownOption>({
stringify: ({ label, value }) => `
${label}
${value}
`
})
`,
});
function ControlledAutocomplete(
props: ControlledAutocompleteProps

View File

@@ -3,7 +3,7 @@ import Popper from '@mui/material/Popper';
import Tab from '@mui/material/Tab';
import * as React from 'react';
import type { IFieldDropdownOption } from '@automatisch/types';
import type { IFieldDropdownOption } from 'types';
import Suggestions from 'components/PowerInput/Suggestions';
import TabPanel from 'components/TabPanel';

View File

@@ -1,4 +1,4 @@
import type { IFieldDropdownOption } from '@automatisch/types';
import type { IFieldDropdownOption } from 'types';
import ListItemButton from '@mui/material/ListItemButton';
import ListItemText from '@mui/material/ListItemText';
import throttle from 'lodash/throttle';
@@ -13,7 +13,7 @@ import { SearchInputWrapper } from './style';
interface OptionsProps {
data: readonly IFieldDropdownOption[];
onOptionClick: (event: React.MouseEvent, option: any) => void;
};
}
const SHORT_LIST_LENGTH = 4;
const LIST_ITEM_HEIGHT = 64;
@@ -21,9 +21,11 @@ const LIST_ITEM_HEIGHT = 64;
const computeListHeight = (currentLength: number) => {
const numberOfRenderedItems = Math.min(SHORT_LIST_LENGTH, currentLength);
return LIST_ITEM_HEIGHT * numberOfRenderedItems;
}
};
const renderItemFactory = ({ onOptionClick }: Pick<OptionsProps, 'onOptionClick'>) => (props: ListChildComponentProps) => {
const renderItemFactory =
({ onOptionClick }: Pick<OptionsProps, 'onOptionClick'>) =>
(props: ListChildComponentProps) => {
const { index, style, data } = props;
const suboption = data[index];
@@ -53,31 +55,34 @@ const renderItemFactory = ({ onOptionClick }: Pick<OptionsProps, 'onOptionClick'
/>
</ListItemButton>
);
};
};
const Options = (props: OptionsProps) => {
const formatMessage = useFormatMessage();
const {
data,
onOptionClick
} = props;
const [filteredData, setFilteredData] = React.useState<readonly IFieldDropdownOption[]>(
data
);
const { data, onOptionClick } = props;
const [filteredData, setFilteredData] =
React.useState<readonly IFieldDropdownOption[]>(data);
React.useEffect(function syncOptions() {
React.useEffect(
function syncOptions() {
setFilteredData((filteredData) => {
if (filteredData.length === 0 && filteredData.length !== data.length) {
return data;
}
return filteredData;
})
}, [data]);
});
},
[data]
);
const renderItem = React.useMemo(() => renderItemFactory({
onOptionClick
}), [onOptionClick]);
const renderItem = React.useMemo(
() =>
renderItemFactory({
onOptionClick,
}),
[onOptionClick]
);
const onSearchChange = React.useMemo(
() =>
@@ -89,12 +94,16 @@ const Options = (props: OptionsProps) => {
return;
}
const newFilteredData = data.filter(option => `${option.label}\n${option.value}`.toLowerCase().includes(search.toLowerCase()));
const newFilteredData = data.filter((option) =>
`${option.label}\n${option.value}`
.toLowerCase()
.includes(search.toLowerCase())
);
setFilteredData(newFilteredData);
}, 400),
[data]
);
);
return (
<>
@@ -122,5 +131,4 @@ const Options = (props: OptionsProps) => {
);
};
export default Options;

View File

@@ -5,7 +5,7 @@ import FormHelperText from '@mui/material/FormHelperText';
import { AutocompleteProps } from '@mui/material/Autocomplete';
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
import ClearIcon from '@mui/icons-material/Clear';
import type { IFieldDropdownOption } from '@automatisch/types';
import type { IFieldDropdownOption } from 'types';
import { ActionButtonsWrapper } from './style';
import ClickAwayListener from '@mui/base/ClickAwayListener';

View File

@@ -19,6 +19,7 @@ type DrawerLink = {
Icon: React.ElementType;
primary: string;
to: string;
target?: '_blank';
badgeContent?: React.ReactNode;
dataTest?: string;
};
@@ -69,7 +70,7 @@ export default function Drawer(props: DrawerProps): React.ReactElement {
<List sx={{ py: 0, mt: 3 }}>
{bottomLinks.map(
({ Icon, badgeContent, primary, to, dataTest }, index) => (
({ Icon, badgeContent, primary, to, dataTest, target }, index) => (
<ListItemLink
key={`${to}-${index}`}
icon={
@@ -77,9 +78,10 @@ export default function Drawer(props: DrawerProps): React.ReactElement {
<Icon htmlColor={theme.palette.primary.main} />
</Badge>
}
primary={formatMessage(primary)}
primary={primary}
to={to}
onClick={closeOnClick}
target={target}
data-test={dataTest}
/>
)

View File

@@ -8,7 +8,7 @@ import IconButton from '@mui/material/IconButton';
import RemoveIcon from '@mui/icons-material/Remove';
import AddIcon from '@mui/icons-material/Add';
import { IFieldDynamic } from '@automatisch/types';
import { IFieldDynamic } from 'types';
import InputCreator from 'components/InputCreator';
import { EditorContext } from 'contexts/Editor';

View File

@@ -3,7 +3,7 @@ import { useMutation } from '@apollo/client';
import Box from '@mui/material/Box';
import IconButton from '@mui/material/IconButton';
import AddIcon from '@mui/icons-material/Add';
import type { IFlow, IStep } from '@automatisch/types';
import type { IFlow, IStep } from 'types';
import { GET_FLOW } from 'graphql/queries/get-flow';
import { CREATE_STEP } from 'graphql/mutations/create-step';

View File

@@ -17,7 +17,7 @@ import useFormatMessage from 'hooks/useFormatMessage';
import { UPDATE_FLOW_STATUS } from 'graphql/mutations/update-flow-status';
import { UPDATE_FLOW } from 'graphql/mutations/update-flow';
import { GET_FLOW } from 'graphql/queries/get-flow';
import type { IFlow } from '@automatisch/types';
import type { IFlow } from 'types';
import * as URLS from 'config/urls';
export default function EditorLayout(): React.ReactElement {

View File

@@ -4,7 +4,7 @@ import Stack from '@mui/material/Stack';
import Box from '@mui/material/Box';
import Tooltip from '@mui/material/Tooltip';
import Typography from '@mui/material/Typography';
import type { IExecution } from '@automatisch/types';
import type { IExecution } from 'types';
import useFormatMessage from 'hooks/useFormatMessage';

View File

@@ -5,7 +5,7 @@ import CardActionArea from '@mui/material/CardActionArea';
import Chip from '@mui/material/Chip';
import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos';
import { DateTime } from 'luxon';
import type { IExecution } from '@automatisch/types';
import type { IExecution } from 'types';
import * as URLS from 'config/urls';
import useFormatMessage from 'hooks/useFormatMessage';

View File

@@ -8,7 +8,7 @@ import Tab from '@mui/material/Tab';
import Typography from '@mui/material/Typography';
import Tooltip from '@mui/material/Tooltip';
import Box from '@mui/material/Box';
import type { IApp, IExecutionStep, IStep } from '@automatisch/types';
import type { IApp, IExecutionStep, IStep } from 'types';
import TabPanel from 'components/TabPanel';
import SearchableJSONViewer from 'components/SearchableJSONViewer';

View File

@@ -1,5 +1,5 @@
import * as React from 'react';
import type { IStep } from '@automatisch/types';
import type { IStep } from 'types';
import AppIcon from 'components/AppIcon';
import IntermediateStepCount from 'components/IntermediateStepCount';

View File

@@ -7,7 +7,7 @@ import Chip from '@mui/material/Chip';
import MoreHorizIcon from '@mui/icons-material/MoreHoriz';
import { DateTime } from 'luxon';
import type { IFlow } from '@automatisch/types';
import type { IFlow } from 'types';
import FlowAppIcons from 'components/FlowAppIcons';
import FlowContextMenu from 'components/FlowContextMenu';
import useFormatMessage from 'hooks/useFormatMessage';

View File

@@ -14,13 +14,7 @@ import CheckCircleIcon from '@mui/icons-material/CheckCircle';
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup';
import type { BaseSchema } from 'yup';
import type {
IApp,
ITrigger,
IAction,
IStep,
ISubstep,
} from '@automatisch/types';
import type { IApp, ITrigger, IAction, IStep, ISubstep } from 'types';
import { EditorContext } from 'contexts/Editor';
import { StepExecutionsProvider } from 'contexts/StepExecutions';

View File

@@ -8,7 +8,7 @@ import Divider from '@mui/material/Divider';
import IconButton from '@mui/material/IconButton';
import RemoveIcon from '@mui/icons-material/Remove';
import AddIcon from '@mui/icons-material/Add';
import type { IField, IFieldText, IFieldDropdown } from '@automatisch/types';
import type { IField, IFieldText, IFieldDropdown } from 'types';
import useFormatMessage from 'hooks/useFormatMessage';
import InputCreator from 'components/InputCreator';
@@ -19,7 +19,7 @@ type TGroupItem = {
operator: string;
value: string;
id: string;
}
};
type TGroup = Record<'and', TGroupItem[]>;
@@ -31,7 +31,7 @@ const createGroupItem = (): TGroupItem => ({
});
const createGroup = (): TGroup => ({
and: [createGroupItem()]
and: [createGroupItem()],
});
const operators = [
@@ -69,7 +69,9 @@ const operators = [
},
];
const createStringArgument = (argumentOptions: Omit<IFieldText, 'type' | 'required' | 'variables'>): IField => {
const createStringArgument = (
argumentOptions: Omit<IFieldText, 'type' | 'required' | 'variables'>
): IField => {
return {
...argumentOptions,
type: 'string',
@@ -78,7 +80,9 @@ const createStringArgument = (argumentOptions: Omit<IFieldText, 'type' | 'requir
};
};
const createDropdownArgument = (argumentOptions: Omit<IFieldDropdown, 'type' | 'required'>): IField => {
const createDropdownArgument = (
argumentOptions: Omit<IFieldDropdown, 'type' | 'required'>
): IField => {
return {
...argumentOptions,
required: true,
@@ -91,9 +95,7 @@ type FilterConditionsProps = {
};
function FilterConditions(props: FilterConditionsProps): React.ReactElement {
const {
stepId
} = props;
const { stepId } = props;
const formatMessage = useFormatMessage();
const { control, setValue, getValues } = useFormContext();
const groups = useWatch({ control, name: 'parameters.or' });
@@ -110,7 +112,7 @@ function FilterConditions(props: FilterConditionsProps): React.ReactElement {
const appendGroup = React.useCallback(() => {
const values = getValues('parameters.or');
setValue('parameters.or', values.concat(createGroup()))
setValue('parameters.or', values.concat(createGroup()));
}, []);
const appendGroupItem = React.useCallback((index) => {
@@ -124,48 +126,89 @@ function FilterConditions(props: FilterConditionsProps): React.ReactElement {
if (group.length === 1) {
const groups: TGroup[] = getValues('parameters.or');
setValue('parameters.or', groups.filter((group, index) => index !== groupIndex));
setValue(
'parameters.or',
groups.filter((group, index) => index !== groupIndex)
);
} else {
setValue(`parameters.or.${groupIndex}.and`, group.filter((groupItem, index) => index !== groupItemIndex));
setValue(
`parameters.or.${groupIndex}.and`,
group.filter((groupItem, index) => index !== groupItemIndex)
);
}
}, []);
return (
<React.Fragment>
<Stack sx={{ width: "100%" }} direction="column" spacing={2} mt={2}>
<Stack sx={{ width: '100%' }} direction="column" spacing={2} mt={2}>
{groups?.map((group: TGroup, groupIndex: number) => (
<>
{groupIndex !== 0 && <Divider />}
<Typography variant="subtitle2" gutterBottom>
{groupIndex === 0 && formatMessage('filterConditions.onlyContinueIf')}
{groupIndex !== 0 && formatMessage('filterConditions.orContinueIf')}
{groupIndex === 0 &&
formatMessage('filterConditions.onlyContinueIf')}
{groupIndex !== 0 &&
formatMessage('filterConditions.orContinueIf')}
</Typography>
{group?.and?.map((groupItem: TGroupItem, groupItemIndex: number) => (
{group?.and?.map(
(groupItem: TGroupItem, groupItemIndex: number) => (
<Stack direction="row" spacing={2} key={`item-${groupItem.id}`}>
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={{ xs: 2 }} sx={{ display: 'flex', flex: 1 }}>
<Box sx={{ display: 'flex', flex: '1 0 0px', maxWidth: ['100%', '33%'] }}>
<Stack
direction={{ xs: 'column', sm: 'row' }}
spacing={{ xs: 2 }}
sx={{ display: 'flex', flex: 1 }}
>
<Box
sx={{
display: 'flex',
flex: '1 0 0px',
maxWidth: ['100%', '33%'],
}}
>
<InputCreator
schema={createStringArgument({ key: `or.${groupIndex}.and.${groupItemIndex}.key`, label: 'Choose field' })}
schema={createStringArgument({
key: `or.${groupIndex}.and.${groupItemIndex}.key`,
label: 'Choose field',
})}
namePrefix="parameters"
stepId={stepId}
disabled={editorContext.readOnly}
/>
</Box>
<Box sx={{ display: 'flex', flex: '1 0 0px', maxWidth: ['100%', '33%'] }}>
<Box
sx={{
display: 'flex',
flex: '1 0 0px',
maxWidth: ['100%', '33%'],
}}
>
<InputCreator
schema={createDropdownArgument({ key: `or.${groupIndex}.and.${groupItemIndex}.operator`, options: operators, label: 'Choose condition' })}
schema={createDropdownArgument({
key: `or.${groupIndex}.and.${groupItemIndex}.operator`,
options: operators,
label: 'Choose condition',
})}
namePrefix="parameters"
stepId={stepId}
disabled={editorContext.readOnly}
/>
</Box>
<Box sx={{ display: 'flex', flex: '1 0 0px', maxWidth: ['100%', '33%'] }}>
<Box
sx={{
display: 'flex',
flex: '1 0 0px',
maxWidth: ['100%', '33%'],
}}
>
<InputCreator
schema={createStringArgument({ key: `or.${groupIndex}.and.${groupItemIndex}.value`, label: 'Enter text' })}
schema={createStringArgument({
key: `or.${groupIndex}.and.${groupItemIndex}.value`,
label: 'Enter text',
})}
namePrefix="parameters"
stepId={stepId}
disabled={editorContext.readOnly}
@@ -182,7 +225,8 @@ function FilterConditions(props: FilterConditionsProps): React.ReactElement {
<RemoveIcon />
</IconButton>
</Stack>
))}
)
)}
<Stack spacing={1} direction="row">
<IconButton
@@ -194,14 +238,16 @@ function FilterConditions(props: FilterConditionsProps): React.ReactElement {
<AddIcon /> And
</IconButton>
{(groups.length - 1) === groupIndex && <IconButton
{groups.length - 1 === groupIndex && (
<IconButton
size="small"
edge="start"
onClick={appendGroup}
sx={{ width: 61, height: 61 }}
>
<AddIcon /> Or
</IconButton>}
</IconButton>
)}
</Stack>
</>
))}

View File

@@ -4,7 +4,7 @@ import Collapse from '@mui/material/Collapse';
import ListItem from '@mui/material/ListItem';
import Button from '@mui/material/Button';
import Stack from '@mui/material/Stack';
import type { IStep, ISubstep } from '@automatisch/types';
import type { IStep, ISubstep } from 'types';
import { EditorContext } from 'contexts/Editor';
import FlowSubstepTitle from 'components/FlowSubstepTitle';
@@ -54,7 +54,7 @@ function FlowSubstep(props: FlowSubstepProps): React.ReactElement {
pb: 3,
flexDirection: 'column',
alignItems: 'flex-start',
position: 'relative'
position: 'relative',
}}
>
{!!args?.length && (

View File

@@ -1,7 +1,7 @@
import * as React from 'react';
import MuiTextField from '@mui/material/TextField';
import CircularProgress from '@mui/material/CircularProgress';
import type { IField, IFieldDropdownOption } from '@automatisch/types';
import type { IField, IFieldDropdownOption } from 'types';
import useDynamicFields from 'hooks/useDynamicFields';
import useDynamicData from 'hooks/useDynamicData';

View File

@@ -1,6 +1,6 @@
import * as React from 'react';
import { JSONTree } from 'react-json-tree';
import type { IJSONObject } from '@automatisch/types';
import type { IJSONObject } from 'types';
type JSONViewerProps = {
data: IJSONObject;

View File

@@ -7,12 +7,14 @@ import AppsIcon from '@mui/icons-material/Apps';
import SwapCallsIcon from '@mui/icons-material/SwapCalls';
import HistoryIcon from '@mui/icons-material/History';
import NotificationsIcon from '@mui/icons-material/Notifications';
import ArrowBackIosNew from '@mui/icons-material/ArrowBackIosNew';
import * as URLS from 'config/urls';
import useFormatMessage from 'hooks/useFormatMessage';
import useVersion from 'hooks/useVersion';
import AppBar from 'components/AppBar';
import Drawer from 'components/Drawer';
import useAutomatischInfo from 'hooks/useAutomatischInfo';
import useConfig from 'hooks/useConfig';
type PublicLayoutProps = {
children: React.ReactNode;
@@ -40,48 +42,95 @@ const drawerLinks = [
];
type GenerateDrawerBottomLinksOptions = {
isMation: boolean;
loading: boolean;
disableNotificationsPage: boolean;
notificationBadgeContent: number;
additionalDrawerLink?: string;
additionalDrawerLinkText?: string;
additionalDrawerLinkIcon?: string;
formatMessage: ReturnType<typeof useFormatMessage>;
};
const generateDrawerBottomLinks = ({
isMation,
loading,
const generateDrawerBottomLinks = async ({
disableNotificationsPage,
notificationBadgeContent = 0,
additionalDrawerLink,
additionalDrawerLinkText,
formatMessage,
}: GenerateDrawerBottomLinksOptions) => {
if (loading || isMation) {
return [];
}
return [
{
const notificationsPageLinkObject = {
Icon: NotificationsIcon,
primary: 'settingsDrawer.notifications',
primary: formatMessage('settingsDrawer.notifications'),
to: URLS.UPDATES,
badgeContent: notificationBadgeContent,
},
];
};
const hasAdditionalDrawerLink =
additionalDrawerLink && additionalDrawerLinkText;
const additionalDrawerLinkObject = {
Icon: ArrowBackIosNew,
primary: additionalDrawerLinkText || '',
to: additionalDrawerLink || '',
target: '_blank' as const,
};
const links = [];
if (!disableNotificationsPage) {
links.push(notificationsPageLinkObject);
}
if (hasAdditionalDrawerLink) {
links.push(additionalDrawerLinkObject);
}
return links;
};
type Link = {
Icon: React.ElementType;
primary: string;
target?: '_blank';
to: string;
badgeContent?: React.ReactNode;
};
export default function PublicLayout({
children,
}: PublicLayoutProps): React.ReactElement {
const version = useVersion();
const { isMation, loading } = useAutomatischInfo();
const { config, loading } = useConfig([
'disableNotificationsPage',
'additionalDrawerLink',
'additionalDrawerLinkText',
]);
const theme = useTheme();
const formatMessage = useFormatMessage();
const [bottomLinks, setBottomLinks] = React.useState<Link[]>([]);
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('lg'));
const [isDrawerOpen, setDrawerOpen] = React.useState(!matchSmallScreens);
const openDrawer = () => setDrawerOpen(true);
const closeDrawer = () => setDrawerOpen(false);
const drawerBottomLinks = generateDrawerBottomLinks({
React.useEffect(() => {
async function perform() {
const newBottomLinks = await generateDrawerBottomLinks({
notificationBadgeContent: version.newVersionCount,
loading,
isMation,
disableNotificationsPage: config?.disableNotificationsPage as boolean,
additionalDrawerLink: config?.additionalDrawerLink as string,
additionalDrawerLinkText: config?.additionalDrawerLinkText as string,
formatMessage,
});
setBottomLinks(newBottomLinks);
}
if (loading) return;
perform();
}, [config, loading, version.newVersionCount]);
return (
<>
<AppBar
@@ -93,7 +142,7 @@ export default function PublicLayout({
<Box sx={{ display: 'flex' }}>
<Drawer
links={drawerLinks}
bottomLinks={drawerBottomLinks}
bottomLinks={bottomLinks}
open={isDrawerOpen}
onOpen={openDrawer}
onClose={closeDrawer}

View File

@@ -9,6 +9,7 @@ type ListItemLinkProps = {
icon: React.ReactNode;
primary: string;
to: string;
target?: '_blank';
onClick?: (event: React.SyntheticEvent) => void;
'data-test'?: string;
};
@@ -16,14 +17,29 @@ type ListItemLinkProps = {
export default function ListItemLink(
props: ListItemLinkProps
): React.ReactElement {
const { icon, primary, to, onClick, 'data-test': dataTest } = props;
const { icon, primary, to, onClick, 'data-test': dataTest, target } = props;
const selected = useMatch({ path: to, end: true });
const CustomLink = React.useMemo(
() =>
React.forwardRef<HTMLAnchorElement, Omit<LinkProps, 'to'>>(
function InLineLink(linkProps, ref) {
return <Link ref={ref} to={to} {...linkProps} />;
try {
// challenge the link to check if it's absolute URL
new URL(to); // should throw an error if it's not an absolute URL
return (
<a
{...linkProps}
ref={ref}
href={to}
target={target}
rel="noopener noreferrer"
/>
);
} catch {
return <Link ref={ref} {...linkProps} to={to} />;
}
}
),
[to]
@@ -37,6 +53,7 @@ export default function ListItemLink(
selected={!!selected}
onClick={onClick}
data-test={dataTest}
target={target}
>
<ListItemIcon sx={{ minWidth: 52 }}>{icon}</ListItemIcon>
<ListItemText

View File

@@ -15,6 +15,27 @@ const MetadataProvider = ({
document.title = (config?.title as string) || 'Automatisch';
}, [config?.title]);
React.useEffect(() => {
const existingFaviconElement = document.querySelector(
"link[rel~='icon']"
) as HTMLLinkElement | null;
if (config?.disableFavicon === true) {
existingFaviconElement?.remove();
}
if (config?.disableFavicon === false) {
if (existingFaviconElement) {
existingFaviconElement.href = '/browser-tab.ico';
} else {
const newFaviconElement = document.createElement('link');
newFaviconElement.rel = 'icon';
document.head.appendChild(newFaviconElement);
newFaviconElement.href = '/browser-tab.ico';
}
}
}, [config?.disableFavicon]);
return <>{children}</>;
};

View File

@@ -14,7 +14,7 @@ import Typography from '@mui/material/Typography';
import * as React from 'react';
import { useFormContext } from 'react-hook-form';
import { IPermissionCatalog } from '@automatisch/types';
import { IPermissionCatalog } from 'types';
import ControlledCheckbox from 'components/ControlledCheckbox';
import useFormatMessage from 'hooks/useFormatMessage';
@@ -118,7 +118,9 @@ export default function PermissionSettings(props: PermissionSettingsProps) {
{action.subjects.includes(subject) && (
<ControlledCheckbox
name={`${fieldPrefix}.${action.key}.conditions.${condition.key}`}
dataTest={`${condition.key}-${action.key.toLowerCase()}-checkbox`}
dataTest={`${
condition.key
}-${action.key.toLowerCase()}-checkbox`}
defaultValue={defaultChecked}
disabled={
getValues(

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