Compare commits
1 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
465dc8ba38 |
@@ -8,7 +8,7 @@
|
||||
"version": "latest"
|
||||
},
|
||||
"ghcr.io/devcontainers/features/node:1": {
|
||||
"version": 20
|
||||
"version": 16
|
||||
},
|
||||
"ghcr.io/devcontainers/features/common-utils:1": {
|
||||
"username": "vscode",
|
||||
|
@@ -6,7 +6,8 @@
|
||||
"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} lint",
|
||||
"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",
|
||||
"build:docs": "cd ./packages/docs && yarn install && yarn build"
|
||||
},
|
||||
"workspaces": {
|
||||
|
@@ -1 +0,0 @@
|
||||
<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>
|
Before Width: | Height: | Size: 1.6 KiB |
@@ -1,44 +0,0 @@
|
||||
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,
|
||||
};
|
@@ -1,8 +0,0 @@
|
||||
import verifyCredentials from './verify-credentials.js';
|
||||
|
||||
const isStillVerified = async ($) => {
|
||||
await verifyCredentials($);
|
||||
return true;
|
||||
};
|
||||
|
||||
export default isStillVerified;
|
@@ -1,14 +0,0 @@
|
||||
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;
|
@@ -1,10 +0,0 @@
|
||||
const addAuthToken = ($, requestConfig) => {
|
||||
if (requestConfig.additionalProperties?.skipAddingAuthToken)
|
||||
return requestConfig;
|
||||
|
||||
requestConfig.url = requestConfig.url + `?key=${$.auth.data.authToken}`;
|
||||
|
||||
return requestConfig;
|
||||
};
|
||||
|
||||
export default addAuthToken;
|
@@ -1,11 +0,0 @@
|
||||
const setBaseUrl = ($, requestConfig) => {
|
||||
const subdomain = $.auth.data.instanceUrl;
|
||||
|
||||
if (subdomain) {
|
||||
requestConfig.baseURL = `https://${subdomain}.airbrake.io`;
|
||||
}
|
||||
|
||||
return requestConfig;
|
||||
};
|
||||
|
||||
export default setBaseUrl;
|
@@ -1,17 +0,0 @@
|
||||
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,
|
||||
});
|
@@ -1,6 +1,5 @@
|
||||
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';
|
||||
@@ -9,12 +8,10 @@ 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,
|
||||
@@ -23,7 +20,6 @@ const transformers = {
|
||||
markdownToHtml,
|
||||
pluralize,
|
||||
replace,
|
||||
stringToBase64,
|
||||
trimWhitespace,
|
||||
useDefaultValue,
|
||||
};
|
||||
@@ -41,7 +37,6 @@ 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' },
|
||||
@@ -50,7 +45,6 @@ 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' },
|
||||
],
|
||||
|
@@ -1,8 +0,0 @@
|
||||
const base64ToString = ($) => {
|
||||
const input = $.step.parameters.input;
|
||||
const decodedString = Buffer.from(input, 'base64').toString('utf8');
|
||||
|
||||
return decodedString;
|
||||
};
|
||||
|
||||
export default base64ToString;
|
@@ -1,8 +0,0 @@
|
||||
const stringtoBase64 = ($) => {
|
||||
const input = $.step.parameters.input;
|
||||
const base64String = Buffer.from(input).toString('base64');
|
||||
|
||||
return base64String;
|
||||
};
|
||||
|
||||
export default stringtoBase64;
|
@@ -1,4 +1,3 @@
|
||||
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';
|
||||
@@ -7,7 +6,6 @@ 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';
|
||||
@@ -17,7 +15,6 @@ import formatPhoneNumber from './numbers/format-phone-number.js';
|
||||
import formatDateTime from './date-time/format-date-time.js';
|
||||
|
||||
const options = {
|
||||
base64ToString,
|
||||
capitalize,
|
||||
extractEmailAddress,
|
||||
extractNumber,
|
||||
@@ -26,7 +23,6 @@ const options = {
|
||||
markdownToHtml,
|
||||
pluralize,
|
||||
replace,
|
||||
stringToBase64,
|
||||
trimWhitespace,
|
||||
useDefaultValue,
|
||||
performMathOperation,
|
||||
|
@@ -1,12 +0,0 @@
|
||||
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;
|
@@ -1,12 +0,0 @@
|
||||
const stringToBase64 = [
|
||||
{
|
||||
label: 'Input',
|
||||
key: 'input',
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Text that will be converted to Base64.',
|
||||
variables: true,
|
||||
},
|
||||
];
|
||||
|
||||
export default stringToBase64;
|
@@ -1,5 +1,11 @@
|
||||
import createDatabaseItem from './create-database-item/index.js';
|
||||
import createPage from './create-page/index.js';
|
||||
import findDatabaseItem from './find-database-item/index.js';
|
||||
import updateDatabaseItem from './update-database-item/index.js';
|
||||
|
||||
export default [createDatabaseItem, createPage, findDatabaseItem];
|
||||
export default [
|
||||
createDatabaseItem,
|
||||
createPage,
|
||||
findDatabaseItem,
|
||||
updateDatabaseItem,
|
||||
];
|
||||
|
@@ -0,0 +1,157 @@
|
||||
import defineAction from '../../../../helpers/define-action.js';
|
||||
|
||||
export default defineAction({
|
||||
name: 'Update database item',
|
||||
key: 'updateDatabaseItem',
|
||||
description: 'Updates a database item.',
|
||||
arguments: [
|
||||
{
|
||||
label: 'Database',
|
||||
key: 'databaseId',
|
||||
type: 'dropdown',
|
||||
required: true,
|
||||
variables: true,
|
||||
source: {
|
||||
type: 'query',
|
||||
name: 'getDynamicData',
|
||||
arguments: [
|
||||
{
|
||||
name: 'key',
|
||||
value: 'listDatabases',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Item',
|
||||
key: 'itemId',
|
||||
type: 'dropdown',
|
||||
required: true,
|
||||
variables: true,
|
||||
dependsOn: ['parameters.databaseId'],
|
||||
source: {
|
||||
type: 'query',
|
||||
name: 'getDynamicData',
|
||||
arguments: [
|
||||
{
|
||||
name: 'key',
|
||||
value: 'listDatabaseItems',
|
||||
},
|
||||
{
|
||||
name: 'parameters.databaseId',
|
||||
value: '{parameters.databaseId}',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Name',
|
||||
key: 'name',
|
||||
type: 'string',
|
||||
required: false,
|
||||
description:
|
||||
'This field has a 2000 character limit. Any characters beyond 2000 will not be included.',
|
||||
variables: true,
|
||||
},
|
||||
{
|
||||
label: 'Tags',
|
||||
key: 'tags',
|
||||
type: 'dynamic',
|
||||
required: false,
|
||||
description: '',
|
||||
fields: [
|
||||
{
|
||||
label: 'Tag',
|
||||
key: 'tag',
|
||||
type: 'dropdown',
|
||||
required: true,
|
||||
variables: true,
|
||||
dependsOn: ['parameters.databaseId'],
|
||||
source: {
|
||||
type: 'query',
|
||||
name: 'getDynamicData',
|
||||
arguments: [
|
||||
{
|
||||
name: 'key',
|
||||
value: 'listTags',
|
||||
},
|
||||
{
|
||||
name: 'parameters.databaseId',
|
||||
value: '{parameters.databaseId}',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Content',
|
||||
key: 'content',
|
||||
type: 'string',
|
||||
required: false,
|
||||
description:
|
||||
'You can choose to add extra text to the database item, with a limit of up to 2000 characters if desired.',
|
||||
variables: true,
|
||||
},
|
||||
],
|
||||
|
||||
async run($) {
|
||||
const itemId = $.step.parameters.itemId;
|
||||
const name = $.step.parameters.name;
|
||||
const truncatedName = name.slice(0, 2000);
|
||||
const content = $.step.parameters.content;
|
||||
const truncatedContent = content.slice(0, 2000);
|
||||
const tags = $.step.parameters.tags;
|
||||
const formattedTags = tags
|
||||
.filter((tag) => tag.tag !== '')
|
||||
.map((tag) => tag.tag);
|
||||
|
||||
const body = {
|
||||
properties: {},
|
||||
};
|
||||
|
||||
if (truncatedName) {
|
||||
body.properties.Name = {
|
||||
title: [
|
||||
{
|
||||
text: {
|
||||
content: truncatedName,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
if (formattedTags?.length) {
|
||||
body.properties.Tags = {
|
||||
multi_select: formattedTags.map((tag) => ({ name: tag })),
|
||||
};
|
||||
}
|
||||
|
||||
if (truncatedContent) {
|
||||
const response = await $.http.get(`/v1/blocks/${itemId}/children`);
|
||||
const firstBlockId = response.data.results[0].id;
|
||||
|
||||
const body = {
|
||||
paragraph: {
|
||||
rich_text: [
|
||||
{
|
||||
type: 'text',
|
||||
text: {
|
||||
content: truncatedContent,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
await $.http.patch(`/v1/blocks/${firstBlockId}`, body);
|
||||
}
|
||||
|
||||
const { data } = await $.http.patch(`/v1/pages/${itemId}`, body);
|
||||
|
||||
$.setActionItem({
|
||||
raw: data,
|
||||
});
|
||||
},
|
||||
});
|
@@ -1,4 +1,6 @@
|
||||
import listDatabaseItems from './list-database-items/index.js';
|
||||
import listDatabases from './list-databases/index.js';
|
||||
import listParentPages from './list-parent-pages/index.js';
|
||||
import listTags from './list-tags/index.js';
|
||||
|
||||
export default [listDatabases, listParentPages];
|
||||
export default [listDatabaseItems, listDatabases, listParentPages, listTags];
|
||||
|
@@ -0,0 +1,38 @@
|
||||
export default {
|
||||
name: 'List database items',
|
||||
key: 'listDatabaseItems',
|
||||
|
||||
async run($) {
|
||||
const databases = {
|
||||
data: [],
|
||||
error: null,
|
||||
};
|
||||
const payload = {
|
||||
start_cursor: undefined,
|
||||
};
|
||||
const databaseId = $.step.parameters.databaseId;
|
||||
|
||||
if (!databaseId) {
|
||||
return databases;
|
||||
}
|
||||
|
||||
do {
|
||||
const response = await $.http.post(
|
||||
`/v1/databases/${databaseId}/query`,
|
||||
payload
|
||||
);
|
||||
|
||||
payload.start_cursor = response.data.next_cursor;
|
||||
|
||||
for (const database of response.data.results) {
|
||||
databases.data.push({
|
||||
value: database.id,
|
||||
name:
|
||||
database.properties.Name?.title?.[0]?.plain_text || 'Untitled Page',
|
||||
});
|
||||
}
|
||||
} while (payload.start_cursor);
|
||||
|
||||
return databases;
|
||||
},
|
||||
};
|
@@ -22,7 +22,7 @@ export default {
|
||||
for (const database of response.data.results) {
|
||||
databases.data.push({
|
||||
value: database.id,
|
||||
name: database.title[0].plain_text,
|
||||
name: database.title?.[0]?.plain_text || 'Untitled Database',
|
||||
});
|
||||
}
|
||||
} while (payload.start_cursor);
|
||||
|
@@ -0,0 +1,38 @@
|
||||
export default {
|
||||
name: 'List tags',
|
||||
key: 'listTags',
|
||||
|
||||
async run($) {
|
||||
const tags = {
|
||||
data: [],
|
||||
error: null,
|
||||
};
|
||||
const databaseId = $.step.parameters.databaseId;
|
||||
let allTags;
|
||||
|
||||
if (!databaseId) {
|
||||
return tags;
|
||||
}
|
||||
|
||||
const response = await $.http.get(`/v1/databases/${databaseId}`);
|
||||
const tagsExist =
|
||||
response.data.properties.Tags.multi_select.options.length !== 0;
|
||||
|
||||
if (tagsExist) {
|
||||
allTags = response.data.properties.Tags.multi_select.options.map(
|
||||
(tag) => tag.name
|
||||
);
|
||||
} else {
|
||||
return tags;
|
||||
}
|
||||
|
||||
for (const tag of allTags) {
|
||||
tags.data.push({
|
||||
value: tag,
|
||||
name: tag,
|
||||
});
|
||||
}
|
||||
|
||||
return tags;
|
||||
},
|
||||
};
|
@@ -18,9 +18,7 @@ const port = process.env.PORT || '3000';
|
||||
const serveWebAppSeparately =
|
||||
process.env.SERVE_WEB_APP_SEPARATELY === 'true' ? true : false;
|
||||
|
||||
let apiUrl = new URL(
|
||||
process.env.API_URL || `${protocol}://${host}:${port}`
|
||||
).toString();
|
||||
let apiUrl = new URL(`${protocol}://${host}:${port}`).toString();
|
||||
apiUrl = apiUrl.substring(0, apiUrl.length - 1);
|
||||
|
||||
// use apiUrl by default, which has less priority over the following cases
|
||||
@@ -90,10 +88,6 @@ 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) {
|
||||
|
@@ -1,10 +1,7 @@
|
||||
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({
|
||||
|
@@ -1,17 +1,9 @@
|
||||
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)) {
|
||||
@@ -26,7 +18,7 @@ const getConfig = async (_parent, params) => {
|
||||
computedConfig[key] = value?.data;
|
||||
|
||||
return computedConfig;
|
||||
}, defaultConfig);
|
||||
}, {});
|
||||
};
|
||||
|
||||
export default getConfig;
|
||||
|
@@ -2,7 +2,6 @@ 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', () => {
|
||||
@@ -57,10 +56,6 @@ 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,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -87,48 +82,6 @@ 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',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@@ -6,6 +6,31 @@ 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 () => {
|
||||
@@ -77,3 +102,4 @@ describe('graphQL getCurrentUser query', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -40,7 +40,23 @@ describe('graphQL getExecutions query', () => {
|
||||
}
|
||||
`;
|
||||
|
||||
describe('and without correct permissions', () => {
|
||||
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', () => {
|
||||
it('should throw not authorized error', async () => {
|
||||
const userWithoutPermissions = await createUser();
|
||||
const token = createAuthTokenByUserId(userWithoutPermissions.id);
|
||||
@@ -470,3 +486,4 @@ describe('graphQL getExecutions query', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -40,6 +40,23 @@ 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();
|
||||
@@ -128,7 +145,9 @@ describe('graphQL getFlow query', () => {
|
||||
{
|
||||
appKey: actionStep.appKey,
|
||||
connection: {
|
||||
createdAt: actionConnection.createdAt.getTime().toString(),
|
||||
createdAt: actionConnection.createdAt
|
||||
.getTime()
|
||||
.toString(),
|
||||
id: actionConnection.id,
|
||||
verified: actionConnection.verified,
|
||||
},
|
||||
@@ -215,7 +234,9 @@ describe('graphQL getFlow query', () => {
|
||||
{
|
||||
appKey: actionStep.appKey,
|
||||
connection: {
|
||||
createdAt: actionConnection.createdAt.getTime().toString(),
|
||||
createdAt: actionConnection.createdAt
|
||||
.getTime()
|
||||
.toString(),
|
||||
id: actionConnection.id,
|
||||
verified: actionConnection.verified,
|
||||
},
|
||||
@@ -238,3 +259,4 @@ describe('graphQL getFlow query', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -17,6 +17,7 @@ describe('graphQL getRole query', () => {
|
||||
userWithoutPermissions,
|
||||
tokenWithPermissions,
|
||||
tokenWithoutPermissions,
|
||||
invalidToken,
|
||||
permissionOne,
|
||||
permissionTwo;
|
||||
|
||||
@@ -73,8 +74,24 @@ 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);
|
||||
@@ -162,3 +179,4 @@ describe('graphQL getRole query', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -15,7 +15,8 @@ describe('graphQL getRoles query', () => {
|
||||
userWithPermissions,
|
||||
userWithoutPermissions,
|
||||
tokenWithPermissions,
|
||||
tokenWithoutPermissions;
|
||||
tokenWithoutPermissions,
|
||||
invalidToken;
|
||||
|
||||
beforeEach(async () => {
|
||||
currentUserRole = await createRole({ name: 'Current user role' });
|
||||
@@ -52,8 +53,24 @@ 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);
|
||||
@@ -132,3 +149,4 @@ describe('graphQL getRoles query', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -16,6 +16,22 @@ 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 () => {
|
||||
@@ -98,3 +114,4 @@ describe('graphQL getTrialStatus query', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -8,6 +8,31 @@ 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();
|
||||
@@ -59,7 +84,9 @@ 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 () => {
|
||||
@@ -144,3 +171,4 @@ describe('graphQL getUser query', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -30,6 +30,20 @@ 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();
|
||||
@@ -72,7 +86,9 @@ 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 () => {
|
||||
@@ -146,3 +162,4 @@ describe('graphQL getUsers query', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -3,7 +3,7 @@ import jwt from 'jsonwebtoken';
|
||||
import appConfig from '../config/app.js';
|
||||
import User from '../models/user.js';
|
||||
|
||||
export const isAuthenticated = async (_parent, _args, req) => {
|
||||
const isAuthenticated = rule()(async (_parent, _args, req) => {
|
||||
const token = req.headers['authorization'];
|
||||
|
||||
if (token == null) return false;
|
||||
@@ -26,13 +26,12 @@ export const isAuthenticated = async (_parent, _args, req) => {
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const isAuthenticatedRule = rule()(isAuthenticated);
|
||||
|
||||
export const authenticationRules = {
|
||||
const authentication = shield(
|
||||
{
|
||||
Query: {
|
||||
'*': isAuthenticatedRule,
|
||||
'*': isAuthenticated,
|
||||
getAutomatischInfo: allow,
|
||||
getConfig: allow,
|
||||
getNotifications: allow,
|
||||
@@ -40,18 +39,16 @@ export const authenticationRules = {
|
||||
listSamlAuthProviders: allow,
|
||||
},
|
||||
Mutation: {
|
||||
'*': isAuthenticatedRule,
|
||||
'*': isAuthenticated,
|
||||
forgotPassword: allow,
|
||||
login: allow,
|
||||
registerUser: allow,
|
||||
resetPassword: allow,
|
||||
},
|
||||
};
|
||||
|
||||
const authenticationOptions = {
|
||||
},
|
||||
{
|
||||
allowExternalErrors: true,
|
||||
};
|
||||
|
||||
const authentication = shield(authenticationRules, authenticationOptions);
|
||||
}
|
||||
);
|
||||
|
||||
export default authentication;
|
||||
|
@@ -1,78 +0,0 @@
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@@ -1,9 +1,6 @@
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import handlebars from 'handlebars';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import * as handlebars from 'handlebars';
|
||||
|
||||
const compileEmail = (emailPath, replacements = {}) => {
|
||||
const filePath = path.join(__dirname, `../views/emails/${emailPath}.ee.hbs`);
|
||||
|
@@ -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);
|
||||
|
@@ -32,12 +32,6 @@ export default defineConfig({
|
||||
],
|
||||
sidebar: {
|
||||
'/apps/': [
|
||||
{
|
||||
text: 'Airbrake',
|
||||
collapsible: true,
|
||||
collapsed: true,
|
||||
items: [{ text: 'Connection', link: '/apps/airbrake/connection' }],
|
||||
},
|
||||
{
|
||||
text: 'Carbone',
|
||||
collapsible: true,
|
||||
@@ -311,7 +305,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' }
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@@ -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,5 +42,3 @@ 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 |
|
||||
|
@@ -1,13 +0,0 @@
|
||||
# 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.
|
@@ -7,6 +7,8 @@ items:
|
||||
desc: Creates a page inside a parent page.
|
||||
- name: Find database item
|
||||
desc: Searches for an item in a database by property.
|
||||
- name: Update database item
|
||||
desc: Updates a database item.
|
||||
---
|
||||
|
||||
<script setup>
|
||||
|
@@ -1 +0,0 @@
|
||||
<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>
|
Before Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
@@ -2,6 +2,7 @@
|
||||
<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
|
||||
|
@@ -2,6 +2,13 @@
|
||||
"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",
|
||||
|
@@ -15,7 +15,6 @@ 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 = {
|
||||
@@ -87,11 +86,19 @@ 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);
|
||||
@@ -109,15 +116,6 @@ 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
|
||||
|
@@ -19,7 +19,6 @@ type DrawerLink = {
|
||||
Icon: React.ElementType;
|
||||
primary: string;
|
||||
to: string;
|
||||
target?: '_blank';
|
||||
badgeContent?: React.ReactNode;
|
||||
dataTest?: string;
|
||||
};
|
||||
@@ -70,7 +69,7 @@ export default function Drawer(props: DrawerProps): React.ReactElement {
|
||||
|
||||
<List sx={{ py: 0, mt: 3 }}>
|
||||
{bottomLinks.map(
|
||||
({ Icon, badgeContent, primary, to, dataTest, target }, index) => (
|
||||
({ Icon, badgeContent, primary, to, dataTest }, index) => (
|
||||
<ListItemLink
|
||||
key={`${to}-${index}`}
|
||||
icon={
|
||||
@@ -78,10 +77,9 @@ export default function Drawer(props: DrawerProps): React.ReactElement {
|
||||
<Icon htmlColor={theme.palette.primary.main} />
|
||||
</Badge>
|
||||
}
|
||||
primary={primary}
|
||||
primary={formatMessage(primary)}
|
||||
to={to}
|
||||
onClick={closeOnClick}
|
||||
target={target}
|
||||
data-test={dataTest}
|
||||
/>
|
||||
)
|
||||
|
@@ -7,14 +7,12 @@ 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 useConfig from 'hooks/useConfig';
|
||||
import useAutomatischInfo from 'hooks/useAutomatischInfo';
|
||||
|
||||
type PublicLayoutProps = {
|
||||
children: React.ReactNode;
|
||||
@@ -42,95 +40,48 @@ const drawerLinks = [
|
||||
];
|
||||
|
||||
type GenerateDrawerBottomLinksOptions = {
|
||||
disableNotificationsPage: boolean;
|
||||
isMation: boolean;
|
||||
loading: boolean;
|
||||
notificationBadgeContent: number;
|
||||
additionalDrawerLink?: string;
|
||||
additionalDrawerLinkText?: string;
|
||||
additionalDrawerLinkIcon?: string;
|
||||
formatMessage: ReturnType<typeof useFormatMessage>;
|
||||
};
|
||||
|
||||
const generateDrawerBottomLinks = async ({
|
||||
disableNotificationsPage,
|
||||
const generateDrawerBottomLinks = ({
|
||||
isMation,
|
||||
loading,
|
||||
notificationBadgeContent = 0,
|
||||
additionalDrawerLink,
|
||||
additionalDrawerLinkText,
|
||||
formatMessage,
|
||||
}: GenerateDrawerBottomLinksOptions) => {
|
||||
const notificationsPageLinkObject = {
|
||||
if (loading || isMation) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
Icon: NotificationsIcon,
|
||||
primary: formatMessage('settingsDrawer.notifications'),
|
||||
primary: '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 { config, loading } = useConfig([
|
||||
'disableNotificationsPage',
|
||||
'additionalDrawerLink',
|
||||
'additionalDrawerLinkText',
|
||||
]);
|
||||
const { isMation, loading } = useAutomatischInfo();
|
||||
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);
|
||||
|
||||
React.useEffect(() => {
|
||||
async function perform() {
|
||||
const newBottomLinks = await generateDrawerBottomLinks({
|
||||
const drawerBottomLinks = generateDrawerBottomLinks({
|
||||
notificationBadgeContent: version.newVersionCount,
|
||||
disableNotificationsPage: config?.disableNotificationsPage as boolean,
|
||||
additionalDrawerLink: config?.additionalDrawerLink as string,
|
||||
additionalDrawerLinkText: config?.additionalDrawerLinkText as string,
|
||||
formatMessage,
|
||||
loading,
|
||||
isMation,
|
||||
});
|
||||
|
||||
setBottomLinks(newBottomLinks);
|
||||
}
|
||||
|
||||
if (loading) return;
|
||||
|
||||
perform();
|
||||
}, [config, loading, version.newVersionCount]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AppBar
|
||||
@@ -142,7 +93,7 @@ export default function PublicLayout({
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
<Drawer
|
||||
links={drawerLinks}
|
||||
bottomLinks={bottomLinks}
|
||||
bottomLinks={drawerBottomLinks}
|
||||
open={isDrawerOpen}
|
||||
onOpen={openDrawer}
|
||||
onClose={closeDrawer}
|
||||
|
@@ -9,7 +9,6 @@ type ListItemLinkProps = {
|
||||
icon: React.ReactNode;
|
||||
primary: string;
|
||||
to: string;
|
||||
target?: '_blank';
|
||||
onClick?: (event: React.SyntheticEvent) => void;
|
||||
'data-test'?: string;
|
||||
};
|
||||
@@ -17,29 +16,14 @@ type ListItemLinkProps = {
|
||||
export default function ListItemLink(
|
||||
props: ListItemLinkProps
|
||||
): React.ReactElement {
|
||||
const { icon, primary, to, onClick, 'data-test': dataTest, target } = props;
|
||||
const { icon, primary, to, onClick, 'data-test': dataTest } = props;
|
||||
const selected = useMatch({ path: to, end: true });
|
||||
|
||||
const CustomLink = React.useMemo(
|
||||
() =>
|
||||
React.forwardRef<HTMLAnchorElement, Omit<LinkProps, 'to'>>(
|
||||
function InLineLink(linkProps, ref) {
|
||||
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} />;
|
||||
}
|
||||
return <Link ref={ref} to={to} {...linkProps} />;
|
||||
}
|
||||
),
|
||||
[to]
|
||||
@@ -53,7 +37,6 @@ export default function ListItemLink(
|
||||
selected={!!selected}
|
||||
onClick={onClick}
|
||||
data-test={dataTest}
|
||||
target={target}
|
||||
>
|
||||
<ListItemIcon sx={{ minWidth: 52 }}>{icon}</ListItemIcon>
|
||||
<ListItemText
|
||||
|
@@ -15,27 +15,6 @@ 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}</>;
|
||||
};
|
||||
|
||||
|
@@ -9,7 +9,6 @@ import PaymentIcon from '@mui/icons-material/Payment';
|
||||
|
||||
import * as URLS from 'config/urls';
|
||||
import useAutomatischInfo from 'hooks/useAutomatischInfo';
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
import AppBar from 'components/AppBar';
|
||||
import Drawer from 'components/Drawer';
|
||||
|
||||
@@ -23,8 +22,8 @@ function createDrawerLinks({ isCloud }: { isCloud: boolean }) {
|
||||
Icon: AccountCircleIcon,
|
||||
primary: 'settingsDrawer.myProfile',
|
||||
to: URLS.SETTINGS_PROFILE,
|
||||
},
|
||||
];
|
||||
}
|
||||
]
|
||||
|
||||
if (isCloud) {
|
||||
items.push({
|
||||
@@ -37,12 +36,19 @@ function createDrawerLinks({ isCloud }: { isCloud: boolean }) {
|
||||
return items;
|
||||
}
|
||||
|
||||
const drawerBottomLinks = [
|
||||
{
|
||||
Icon: ArrowBackIosNewIcon,
|
||||
primary: 'settingsDrawer.goBack',
|
||||
to: '/',
|
||||
},
|
||||
];
|
||||
|
||||
export default function SettingsLayout({
|
||||
children,
|
||||
}: SettingsLayoutProps): React.ReactElement {
|
||||
const { isCloud } = useAutomatischInfo();
|
||||
const theme = useTheme();
|
||||
const formatMessage = useFormatMessage();
|
||||
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('lg'));
|
||||
const [isDrawerOpen, setDrawerOpen] = React.useState(!matchSmallScreens);
|
||||
|
||||
@@ -50,14 +56,6 @@ export default function SettingsLayout({
|
||||
const closeDrawer = () => setDrawerOpen(false);
|
||||
const drawerLinks = createDrawerLinks({ isCloud });
|
||||
|
||||
const drawerBottomLinks = [
|
||||
{
|
||||
Icon: ArrowBackIosNewIcon,
|
||||
primary: formatMessage('settingsDrawer.goBack'),
|
||||
to: '/',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<AppBar
|
||||
|
12
yarn.lock
12
yarn.lock
@@ -15728,9 +15728,9 @@ vite-node@1.1.3:
|
||||
vite "^5.0.0"
|
||||
|
||||
vite@^3.1.6:
|
||||
version "3.2.8"
|
||||
resolved "https://registry.yarnpkg.com/vite/-/vite-3.2.8.tgz#0697e13addf99ed44b838b8462a3a922fdd9d37b"
|
||||
integrity sha512-EtQU16PLIJpAZol2cTLttNP1mX6L0SyI0pgQB1VOoWeQnMSvtiwovV3D6NcjN8CZQWWyESD2v5NGnpz5RvgOZA==
|
||||
version "3.2.7"
|
||||
resolved "https://registry.npmjs.org/vite/-/vite-3.2.7.tgz"
|
||||
integrity sha512-29pdXjk49xAP0QBr0xXqu2s5jiQIXNvE/xwd0vUizYT2Hzqe4BksNNoWllFVXJf4eLZ+UlVQmXfB4lWrc+t18g==
|
||||
dependencies:
|
||||
esbuild "^0.15.9"
|
||||
postcss "^8.4.18"
|
||||
@@ -15740,9 +15740,9 @@ vite@^3.1.6:
|
||||
fsevents "~2.3.2"
|
||||
|
||||
vite@^5.0.0:
|
||||
version "5.0.12"
|
||||
resolved "https://registry.yarnpkg.com/vite/-/vite-5.0.12.tgz#8a2ffd4da36c132aec4adafe05d7adde38333c47"
|
||||
integrity sha512-4hsnEkG3q0N4Tzf1+t6NdN9dg/L3BM+q8SWgbSPnJvrgH2kgdyzfVJwbR1ic69/4uMJJ/3dqDZZE5/WwqW8U1w==
|
||||
version "5.0.11"
|
||||
resolved "https://registry.yarnpkg.com/vite/-/vite-5.0.11.tgz#31562e41e004cb68e1d51f5d2c641ab313b289e4"
|
||||
integrity sha512-XBMnDjZcNAw/G1gEiskiM1v6yzM4GE5aMGvhWTlHAYYhxb7S3/V1s3m2LDHa8Vh6yIWYYB0iJwsEaS523c4oYA==
|
||||
dependencies:
|
||||
esbuild "^0.19.3"
|
||||
postcss "^8.4.32"
|
||||
|
Reference in New Issue
Block a user