Compare commits

..

47 Commits

Author SHA1 Message Date
Ali BARIN
321019d36a sample js usage in ts 2023-12-22 12:24:32 +00:00
QAComet
d070e976b0 test(e2e-tests): run only on relevant changes in pull requests (#1495) 2023-12-15 18:51:58 +01:00
Ali BARIN
0caf6bfabb Merge pull request #1494 from automatisch/aut-551
feat: hide notifications page in mation instances
2023-12-15 18:00:08 +01:00
Ali BARIN
b842d7938f feat: hide notifications page in mation instances 2023-12-15 16:28:41 +00:00
Ali BARIN
cebbf84375 Merge pull request #1491 from automatisch/aut-548
feat: apply conditional mation styling
2023-12-15 11:40:12 +01:00
Ali BARIN
8608431490 feat: add conditional mation logo by default 2023-12-14 15:59:37 +00:00
Ali BARIN
78ba18b176 feat: apply conditional mation styling 2023-12-14 14:28:07 +00:00
Ali BARIN
f8c30c8526 Merge pull request #1475 from automatisch/aut-547
feat(queries/getAutomatischInfo): add mation
2023-12-14 11:15:35 +01:00
QAComet
693c9b85a5 test: run UI workflow only on changes outside of backend apps (#1462) 2023-12-14 10:45:15 +01:00
Ali BARIN
70bb7defd1 feat(queries/getAutomatischInfo): add mation 2023-12-12 17:37:27 +00:00
Ali BARIN
160377ca31 Merge pull request #1473 from automatisch/aut-545
docs(salesforce): update connection steps
2023-12-12 18:17:07 +01:00
Ali BARIN
2c0ce77a4e docs(salesforce): update connection steps 2023-12-12 17:08:15 +00:00
Ali BARIN
77fbb0c9da Merge pull request #1470 from automatisch/aut-538
fix(odoo): introduce secure connection option
2023-12-11 16:44:11 +01:00
Ali BARIN
5971425d23 fix(odoo): introduce secure connection option 2023-12-11 15:36:50 +00:00
Ali BARIN
aefff5c861 Merge pull request #1449 from automatisch/AUT-414
feat(zendesk): add new users trigger
2023-11-30 12:55:02 +01:00
Rıdvan Akca
a296b5e645 feat(zendesk): add new users trigger 2023-11-30 14:42:25 +03:00
Ali BARIN
eb486a3a07 Merge pull request #1456 from automatisch/AUT-431
feat(notion): add updated database items trigger
2023-11-29 16:54:45 +01:00
Ali BARIN
062b8521ba Merge pull request #1454 from automatisch/AUT-418
feat(zendesk): add delete user action
2023-11-29 14:37:42 +01:00
Rıdvan Akca
1b07f3195a feat(zendesk): add delete user action 2023-11-29 16:30:05 +03:00
Rıdvan Akca
dfa7d4cb8d feat(notion): add updated database items trigger 2023-11-29 16:18:38 +03:00
Ali BARIN
a14dd9666c Merge pull request #1451 from automatisch/AUT-415
feat(zendesk): add create user action
2023-11-29 14:10:46 +01:00
Rıdvan Akca
b07bd4374f feat(zendesk): add create user action 2023-11-29 16:01:55 +03:00
Ali BARIN
b4e12b0ea8 Merge pull request #1448 from automatisch/AUT-413
feat(zendesk): add delete ticket action
2023-11-29 13:35:58 +01:00
Ali BARIN
ee5c17bb85 Merge pull request #1447 from automatisch/AUT-412
feat(zendesk): add find ticket action
2023-11-29 13:32:29 +01:00
Rıdvan Akca
16c9d3400c feat(zendesk): add delete ticket action 2023-11-29 14:27:54 +03:00
Rıdvan Akca
4dd994348d feat(zendesk): add find ticket action 2023-11-29 13:17:04 +03:00
Ali BARIN
f0cbfafc24 Merge pull request #1443 from automatisch/AUT-411
feat(zendesk): add update ticket action
2023-11-28 15:02:21 +01:00
Ali BARIN
d3f38f5488 Merge pull request #1455 from automatisch/rename-discord-scheduled-event-action
feat(discord/create-scheduled-event): remove new prefix
2023-11-28 13:14:25 +01:00
Ali BARIN
737090a67a feat(discord/create-scheduled-event): remove new prefix 2023-11-28 11:56:36 +00:00
Ali BARIN
4f66a4d090 Merge pull request #1446 from automatisch/AUT-465
feat: embed external fonts used in the codebase
2023-11-28 12:47:32 +01:00
Rıdvan Akca
df54f909c1 feat(zendesk): add update ticket action 2023-11-28 14:40:26 +03:00
Ali BARIN
772b195eca Merge pull request #1441 from automatisch/AUT-409
feat(zendesk): add new tickets trigger
2023-11-28 11:52:22 +01:00
Ali BARIN
87866e34ed Merge pull request #1450 from felifluid/feature/action-discord-create-event
feat(discord): add createEvent action
2023-11-27 18:08:20 +01:00
Ali BARIN
c98ac05097 feat(discord/create-new-scheduled-event): rework fields 2023-11-27 17:01:40 +00:00
DerKlobold
36f991b6f9 feat: discord createNewEvent action in docs
adds a simple entry in the docs for the createNewEvent action
2023-11-26 18:59:01 +01:00
DerKlobold
a81c5164fc feat: discord createNewEvent action
adds the action of creating a new event on a discord server. provides some sort of logic-check to make sure the correct fields have been filled, depending of the type given.
2023-11-26 18:58:09 +01:00
DerKlobold
5942482690 feat: list discord voice channel dynamic data
adds a helper function to provide dynamic data of voice and stage channels of a discord server
2023-11-26 18:56:50 +01:00
Rıdvan Akca
4f538ca2fc feat(zendesk): add new tickets trigger 2023-11-24 15:22:35 +03:00
Kasia
9f2281a3e2 feat: embed external fonts used in the codebase 2023-11-24 12:20:28 +00:00
Ali BARIN
b0d2f28c78 Merge pull request #1444 from automatisch/AUT-487
fix(zendesk): get after_cursor from meta field
2023-11-23 13:42:01 +01:00
Rıdvan Akca
d4380a4426 fix(zendesk): get after_cursor from meta field 2023-11-23 15:17:11 +03:00
Ali BARIN
ae2738d4cc Merge pull request #1435 from mohammedzaher/removebg
feat(removebg): add `remove image background` action
2023-11-22 16:28:55 +01:00
Ali BARIN
aa5ae028b2 feat(removebg/remove-image-background): update wording 2023-11-22 15:01:02 +00:00
Mohammed Zaher
7ab8c76aa0 docs(removebg): Add Remove image background action 2023-11-16 15:43:22 +00:00
Mohammed Zaher
8075b65e14 feat(removebg): Add Remove image background action 2023-11-16 15:39:46 +00:00
Ali BARIN
073ce3bf1b Merge pull request #1433 from automatisch/AUT-445
feat(reddit): provide user-agent header
2023-11-14 17:25:29 +01:00
Rıdvan Akca
80fcbfe01b feat(reddit): provide user-agent header 2023-11-14 14:08:01 +03:00
78 changed files with 1523 additions and 340 deletions

View File

@@ -4,6 +4,11 @@ on:
branches:
- main
pull_request:
paths:
- 'packages/backend/**'
- 'packages/e2e-tests/**'
- 'packages/web/**'
- '!packages/backend/src/apps/**'
workflow_dispatch:
env:

View File

@@ -1,34 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="428" height="512" viewBox="0 0 428 512">
<defs>
<style>
.cls-1 {
fill: #e25444;
}
.cls-1, .cls-2, .cls-3 {
fill-rule: evenodd;
}
.cls-2 {
fill: #7b1d13;
}
.cls-3 {
fill: #58150d;
}
</style>
</defs>
<path class="cls-1" d="M378,99L295,257l83,158,34-19V118Z"/>
<path class="cls-2" d="M378,99L212,118,127.5,257,212,396l166,19V99Z"/>
<path class="cls-3" d="M43,99L16,111V403l27,12L212,257Z"/>
<path class="cls-1" d="M42.637,98.667l169.587,47.111V372.444L42.637,415.111V98.667Z"/>
<path class="cls-3" d="M212.313,170.667l-72.008-11.556,72.008-81.778,71.83,81.778Z"/>
<path class="cls-3" d="M284.143,159.111l-71.919,11.733-71.919-11.733V77.333"/>
<path class="cls-3" d="M212.313,342.222l-72.008,13.334,72.008,70.222,71.83-70.222Z"/>
<path class="cls-2" d="M212,16L140,54V159l72.224-20.333Z"/>
<path class="cls-2" d="M212.224,196.444l-71.919,7.823V309.105l71.919,8.228V196.444Z"/>
<path class="cls-2" d="M212.224,373.333L140.305,355.3V458.363L212.224,496V373.333Z"/>
<path class="cls-1" d="M284.143,355.3l-71.919,18.038V496l71.919-37.637V355.3Z"/>
<path class="cls-1" d="M212.224,196.444l71.919,7.823V309.105l-71.919,8.228V196.444Z"/>
<path class="cls-1" d="M212,16l72,38V159l-72-20V16Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -1,56 +0,0 @@
import verifyCredentials from './verify-credentials';
import isStillVerified from './is-still-verified';
export default {
fields: [
{
key: 'oAuthRedirectUrl',
label: 'OAuth Redirect URL',
type: 'string' as const,
required: true,
readOnly: true,
value: '{WEB_APP_URL}/app/amazon-s3/connections/add',
placeholder: null,
description:
'When asked to input a redirect URL in AWS, enter the URL above.',
clickToCopy: true,
},
{
key: 'accessKeyId',
label: 'Access Key ID',
type: 'string' as const,
required: true,
readOnly: false,
value: null,
placeholder: null,
description: null,
clickToCopy: false,
},
{
key: 'secretAccessKey',
label: 'Secret Access Key',
type: 'string' as const,
required: true,
readOnly: false,
value: null,
placeholder: null,
description: null,
clickToCopy: false,
},
{
key: 'screenName',
label: 'Screen Name',
type: 'string' as const,
required: true,
readOnly: false,
value: null,
placeholder: null,
description:
'Screen name of your connection to be used on Automatisch UI.',
clickToCopy: false,
},
],
verifyCredentials,
isStillVerified,
};

View File

@@ -1,9 +0,0 @@
import { IGlobalVariable } from '@automatisch/types';
import getCurrentUser from '../common/get-current-user';
const isStillVerified = async ($: IGlobalVariable) => {
const currentUser = await getCurrentUser($);
return !!currentUser.resourceName;
};
export default isStillVerified;

View File

@@ -1,9 +0,0 @@
import { IGlobalVariable } from '@automatisch/types';
const verifyCredentials = async ($: IGlobalVariable) => {
const { data } = await $.http.get('/');
console.log('data:', data);
};
export default verifyCredentials;

View File

@@ -1,153 +0,0 @@
import { IJSONObject, TBeforeRequest } from '@automatisch/types';
import crypto from 'crypto';
import { getISODate, getYYYYMMDD } from './get-current-date';
function hmac(key: string | Buffer, data: string) {
return crypto.createHmac('sha256', key).update(data).digest('hex');
}
function hmacWoHex(key: Buffer | string, data: string) {
return crypto.createHmac('sha256', key).update(data).digest();
}
function hash(data: string) {
return crypto.createHash('sha256').update(data).digest('hex');
}
function prepareCanonicalRequest(
method: string,
path: string,
queryParams: IJSONObject | string,
headers: IJSONObject,
payload: string
) {
const canonicalRequest = [method, encodeURIComponent(path)];
// Step 3: Canonical Query String
if (typeof queryParams === 'string') {
canonicalRequest.push('');
} else {
const sortedQueryParams = Object.keys(queryParams)
.map(
(key) =>
`${encodeURIComponent(key)}=${encodeURIComponent(
queryParams[key] as string
)}`
)
.sort();
canonicalRequest.push(sortedQueryParams.join('&'));
}
// Step 4: Canonical Headers
const sortedHeaders = Object.keys(headers)
.sort()
.map((key) => `${key.toLowerCase()}:${(headers[key] as string).trim()}`);
canonicalRequest.push(sortedHeaders.join('\n'));
// Step 5: Signed Headers
const signedHeaders = Object.keys(headers)
.sort()
.map((key) => key.toLowerCase())
.join(';');
canonicalRequest.push(signedHeaders);
const hashedPayload = hash(payload);
canonicalRequest.push(hashedPayload);
return canonicalRequest.join('\n');
}
function prepareStringToSign(
datetime: string,
credentialScope: string,
hashedCanonicalRequest: string
) {
const stringToSign = [
'AWS4-HMAC-SHA256',
datetime,
credentialScope,
hashedCanonicalRequest,
];
return stringToSign.join('\n');
}
function calculateSigningKey(
secretKey: string,
date: string,
region: string,
service: string
) {
const dateKey = hmacWoHex('AWS4' + secretKey, date);
const dateRegionKey = hmacWoHex(dateKey, region);
const dateRegionServiceKey = hmacWoHex(dateRegionKey, service);
const signingKey = hmacWoHex(dateRegionServiceKey, 'aws4_request');
return signingKey;
}
function createAuthorizationHeader(
accessKey: string,
credentialScope: string,
signedHeaders: string,
signature: string
) {
return `AWS4-HMAC-SHA256 Credential=${accessKey}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
}
const addAuthHeader: TBeforeRequest = ($, requestConfig) => {
const accessKeyId = $.auth.data.accessKeyId as string;
const secretAccessKey = $.auth.data.secretAccessKey as string;
const date = getYYYYMMDD();
const formattedDate = getISODate();
const region = 'us-east-1';
const method = 'GET';
const path = '/';
const queryParams = '';
const payload = '';
const headers = {
Host: 's3.amazonaws.com',
'X-Amz-Content-Sha256': hash(payload),
'X-Amz-Date': formattedDate,
};
const headerKeys = Object.keys(headers)
.sort()
.map((header) => header.toLowerCase())
.join(';');
const canonicalRequest = prepareCanonicalRequest(
method,
path,
queryParams,
headers,
payload
);
const stringToSign = prepareStringToSign(
formattedDate,
`${date}/${region}/s3/aws4_request`,
hash(canonicalRequest)
);
const signingKey = calculateSigningKey(secretAccessKey, date, region, 's3');
const signature = hmac(signingKey, stringToSign);
const authorizationHeader = createAuthorizationHeader(
accessKeyId,
`${date}/${region}/s3/aws4_request`,
headerKeys,
signature
);
if ($.auth.data?.secretAccessKey && $.auth.data?.accessKeyId) {
requestConfig.headers.Authorization = authorizationHeader;
requestConfig.headers['Host'] = 's3.amazonaws.com';
requestConfig.headers['X-Amz-Content-Sha256'] = hash(payload);
requestConfig.headers['X-Amz-Date'] = formattedDate;
}
return requestConfig;
};
export default addAuthHeader;

View File

@@ -1,13 +0,0 @@
export const getYYYYMMDD = () => {
const today = new Date();
const year = today.getFullYear();
const month = (today.getMonth() + 1).toString().padStart(2, '0');
const day = today.getDate().toString().padStart(2, '0');
const formattedDate = `${year}${month}${day}`;
return formattedDate;
};
export const getISODate = () => {
return new Date().toISOString().replace(/[:-]|\.\d{3}/g, '');
};

View File

@@ -1,16 +0,0 @@
import defineApp from '../../helpers/define-app';
import addAuthHeader from './common/add-auth-header';
import auth from './auth';
export default defineApp({
name: 'Amazon S3',
key: 'amazon-s3',
baseUrl: '',
apiBaseUrl: 'https://s3.amazonaws.com',
iconUrl: '{BASE_URL}/apps/amazon-s3/assets/favicon.svg',
authDocUrl: 'https://automatisch.io/docs/apps/amazon-s3/connection',
primaryColor: '7B1D13',
supportsConnections: true,
beforeRequest: [addAuthHeader],
auth,
});

View File

@@ -0,0 +1,102 @@
import defineAction from '../../../../helpers/define-action';
export default defineAction({
name: 'Create a scheduled event',
key: 'createScheduledEvent',
description: 'Creates a scheduled event',
arguments: [
{
label: 'Type',
key: 'entityType',
type: 'dropdown' as const,
required: true,
variables: true,
options: [
{ label: 'Stage channel', value: 1 },
{ label: 'Voice channel', value: 2 },
{ label: 'External', value: 3 }
],
additionalFields: {
type: 'query',
name: 'getDynamicFields',
arguments: [
{
name: 'key',
value: 'listExternalScheduledEventFields',
},
{
name: 'parameters.entityType',
value: '{parameters.entityType}',
},
],
},
},
{
label: 'Name',
key: 'name',
type: 'string' as const,
required: true,
variables: true,
},
{
label: 'Description',
key: 'description',
type: 'string' as const,
required: false,
variables: true,
},
{
label: 'Image',
key: 'image',
type: 'string' as const,
required: false,
description: 'Image as DataURI scheme [data:image/<jpeg/png/gif>;base64,BASE64_ENCODED_<JPEG/PNG/GIF>_IMAGE_DATA]',
variables: true,
},
],
async run($) {
type entity_metadata = {
location: string
}
type guild_event = {
channel_id: number,
name: string,
privacy_level: number,
scheduled_start_time: string,
scheduled_end_time?: string,
description?: string,
entity_type?: number,
entity_metadata?: entity_metadata,
image?: string, //data:image/jpeg;base64,BASE64_ENCODED_JPEG_IMAGE_DATA
}
const data: guild_event = {
channel_id: $.step.parameters.channel_id as number,
name: $.step.parameters.name as string,
privacy_level: 2,
scheduled_start_time: $.step.parameters.scheduledStartTime as string,
scheduled_end_time: $.step.parameters.scheduledEndTime as string,
description: $.step.parameters.description as string,
entity_type: $.step.parameters.entityType as number,
image: $.step.parameters.image as string,
};
const isExternal = $.step.parameters.entityType === 3;
if (isExternal) {
data.entity_metadata = {
location: $.step.parameters.location as string,
};
data.channel_id = null;
}
const response = await $.http?.post(
`/guilds/${$.auth.data.guildId}/scheduled-events`,
data
);
$.setActionItem({ raw: response.data });
},
});

View File

@@ -1,3 +1,4 @@
import sendMessageToChannel from './send-message-to-channel';
import createScheduledEvent from './create-scheduled-event';
export default [sendMessageToChannel];
export default [sendMessageToChannel, createScheduledEvent];

View File

@@ -1,3 +1,4 @@
import listChannels from './list-channels';
import listVoiceChannels from './list-voice-channels';
export default [listChannels];
export default [listChannels, listVoiceChannels];

View File

@@ -0,0 +1,34 @@
import { IGlobalVariable, IJSONObject } from '@automatisch/types';
export default {
name: 'List voice channels',
key: 'listVoiceChannels',
async run($: IGlobalVariable) {
const channels: {
data: IJSONObject[];
error: IJSONObject | null;
} = {
data: [],
error: null,
};
const response = await $.http.get(
`/guilds/${$.auth.data.guildId}/channels`
);
channels.data = response.data
.filter((channel: IJSONObject) => {
// filter in voice and stage channels only
return channel.type === 2 || channel.type === 13;
})
.map((channel: IJSONObject) => {
return {
value: channel.id,
name: channel.name,
};
});
return channels;
},
};

View File

@@ -0,0 +1,3 @@
import listExternalScheduledEventFields from './list-external-scheduled-event-fields';
export default [listExternalScheduledEventFields];

View File

@@ -0,0 +1,83 @@
import { IGlobalVariable } from '@automatisch/types';
export default {
name: 'List external scheduled event fields',
key: 'listExternalScheduledEventFields',
async run($: IGlobalVariable) {
const isExternal = $.step.parameters.entityType === 3;
if (isExternal) {
return [
{
label: 'Location',
key: 'location',
type: 'string' as const,
required: true,
description: 'The location of the event (1-100 characters). This will be omitted if type is NOT EXTERNAL',
variables: true,
},
{
label: 'Start-Time',
key: 'scheduledStartTime',
type: 'string' as const,
required: true,
description: 'The time the event will start [ISO8601]',
variables: true,
},
{
label: 'End-Time',
key: 'scheduledEndTime',
type: 'string' as const,
required: true,
description: 'The time the event will end [ISO8601]. This will be omitted if type is NOT EXTERNAL',
variables: true,
},
];
}
return [
{
label: 'Channel',
key: 'channel_id',
type: 'dropdown' as const,
required: true,
description: 'Pick a voice or stage channel to link the event to. This will be omitted if type is EXTERNAL',
variables: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listVoiceChannels',
},
],
},
},
{
label: 'Location',
key: 'location',
type: 'string' as const,
required: false,
description: 'The location of the event (1-100 characters). This will be omitted if type is NOT EXTERNAL',
variables: true,
},
{
label: 'Start-Time',
key: 'scheduledStartTime',
type: 'string' as const,
required: true,
description: 'The time the event will start [ISO8601]',
variables: true,
},
{
label: 'End-Time',
key: 'scheduledEndTime',
type: 'string' as const,
required: false,
description: 'The time the event will end [ISO8601]. This will be omitted if type is NOT EXTERNAL',
variables: true,
},
];
},
};

View File

@@ -4,6 +4,7 @@ import auth from './auth';
import dynamicData from './dynamic-data';
import actions from './actions';
import triggers from './triggers';
import dynamicFields from './dynamic-fields';
export default defineApp({
name: 'Discord',
@@ -17,6 +18,7 @@ export default defineApp({
beforeRequest: [addAuthHeader],
auth,
dynamicData,
dynamicFields,
triggers,
actions,
});

View File

@@ -1,5 +1,5 @@
import { IGlobalVariable } from '@automatisch/types';
import getCurrentUser from '../../amazon-s3/common/get-current-user';
import getCurrentUser from '../common/get-current-user';
const isStillVerified = async ($: IGlobalVariable) => {
const currentUser = await getCurrentUser($);

View File

@@ -1,5 +1,5 @@
import { IField, IGlobalVariable } from '@automatisch/types';
import getCurrentUser from '../../amazon-s3/common/get-current-user';
import getCurrentUser from '../common/get-current-user';
type TUser = {
displayName: string;

View File

@@ -1,7 +1,9 @@
import { IGlobalVariable } from '@automatisch/types';
const getCurrentUser = async ($: IGlobalVariable) => {
const { data: currentUser } = await $.http.get('/');
const { data: currentUser } = await $.http.get(
'https://people.googleapis.com/v1/people/me?personFields=names,emailAddresses'
);
return currentUser;
};

View File

@@ -1,3 +1,4 @@
import newDatabaseItems from './new-database-items';
import updatedDatabaseItems from './updated-database-items';
export default [newDatabaseItems];
export default [newDatabaseItems, updatedDatabaseItems];

View File

@@ -0,0 +1,33 @@
import defineTrigger from '../../../../helpers/define-trigger';
import updatedDatabaseItems from './updated-database-items';
export default defineTrigger({
name: 'Updated database items',
key: 'updatedDatabaseItems',
pollInterval: 15,
description:
'Triggers when there is an update to an item in a chosen database',
arguments: [
{
label: 'Database',
key: 'databaseId',
type: 'dropdown' as const,
required: false,
variables: false,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listDatabases',
},
],
},
},
],
async run($) {
await updatedDatabaseItems($);
},
});

View File

@@ -0,0 +1,51 @@
import { IGlobalVariable } from '@automatisch/types';
type DatabaseItem = {
id: string;
last_edited_time: string;
};
type ResponseData = {
results: DatabaseItem[];
next_cursor?: string;
};
type Payload = {
sorts: [
{
timestamp: 'created_time' | 'last_edited_time';
direction: 'ascending' | 'descending';
}
];
start_cursor?: string;
};
const updatedDatabaseItems = async ($: IGlobalVariable) => {
const payload: Payload = {
sorts: [
{
timestamp: 'last_edited_time',
direction: 'descending',
},
],
};
const databaseId = $.step.parameters.databaseId as string;
const path = `/v1/databases/${databaseId}/query`;
do {
const response = await $.http.post<ResponseData>(path, payload);
payload.start_cursor = response.data.next_cursor;
for (const databaseItem of response.data.results) {
$.pushTriggerItem({
raw: databaseItem,
meta: {
internalId: `${databaseItem.id}-${databaseItem.last_edited_time}`,
},
});
}
} while (payload.start_cursor);
};
export default updatedDatabaseItems;

View File

@@ -11,7 +11,7 @@ export default {
readOnly: false,
value: null,
placeholder: null,
description: 'Host name of your Odoo Server',
description: 'Host name of your Odoo Server (e.g. sub.domain.com without the protocol)',
clickToCopy: false,
},
{
@@ -25,6 +25,27 @@ export default {
description: 'Port that the host is running on, defaults to 443 (HTTPS)',
clickToCopy: false,
},
{
key: 'secure',
label: 'Secure',
type: 'dropdown' as const,
required: true,
readOnly: false,
value: 'true',
description: 'True if the host communicates via secure protocol.',
variables: false,
clickToCopy: false,
options: [
{
label: 'True',
value: 'true',
},
{
label: 'False',
value: 'false',
},
],
},
{
key: 'databaseName',
label: 'Database Name',
@@ -40,7 +61,7 @@ export default {
key: 'email',
label: 'Email Address',
type: 'string' as const,
requires: true,
required: true,
readOnly: false,
value: null,
placeholder: null,

View File

@@ -32,8 +32,10 @@ export const asyncMethodCall = async <T = number>($: IGlobalVariable, { method,
export const getClient = ($: IGlobalVariable, { path = 'common' }) => {
const host = $.auth.data.host as string;
const port = Number($.auth.data.port as string);
const secure = $.auth.data.secure === 'true';
const createClientFunction = secure ? xmlrpc.createSecureClient : xmlrpc.createClient;
return xmlrpc.createClient(
return createClientFunction(
{
host,
port,

View File

@@ -1,10 +1,22 @@
import { TBeforeRequest } from '@automatisch/types';
import appConfig from '../../../config/app';
const addAuthHeader: TBeforeRequest = ($, requestConfig) => {
const screenName = $.auth.data?.screenName as string;
if ($.auth.data?.accessToken) {
requestConfig.headers.Authorization = `${$.auth.data.tokenType} ${$.auth.data.accessToken}`;
}
if (screenName) {
requestConfig.headers[
'User-Agent'
] = `web:automatisch:${appConfig.version} (by /u/${screenName})`;
} else {
requestConfig.headers[
'User-Agent'
] = `web:automatisch:${appConfig.version}`;
}
return requestConfig;
};

View File

@@ -0,0 +1,3 @@
import removeImageBackground from './remove-image-background';
export default [removeImageBackground];

View File

@@ -0,0 +1,82 @@
import defineAction from '../../../../helpers/define-action';
export default defineAction({
name: 'Remove image background',
key: 'removeImageBackground',
description:
'Removes the background of an image.',
arguments: [
{
label: 'Image file',
key: 'imageFileB64',
type: 'string' as const,
required: true,
variables: true,
description: 'Provide a JPG or PNG file in Base64 format, up to 12 MB (see remove.bg/supported-images)',
},
{
label: 'Size',
key: 'size',
type: 'dropdown' as const,
required: true,
value: 'auto',
options: [
{ label: 'Auto', value: 'auto' },
{ label: 'Preview (up to 0.25 megapixels)', value: 'preview' },
{ label: 'Full (up to 10 megapixels)', value: 'full' },
]
},
{
label: 'Background color',
key: 'bgColor',
type: 'string' as const,
description: 'Adds a solid color background. Can be a hex color code (e.g. 81d4fa, fff) or a color name (e.g. green)',
required: false,
},
{
label: 'Background image URL',
key: 'bgImageUrl',
type: 'string' as const,
description: 'Adds a background image from a URL.',
required: false,
},
{
label: 'Output image format',
key: 'outputFormat',
type: 'dropdown' as const,
description: 'Note: Use PNG to preserve transparency',
required: true,
value: 'auto',
options: [
{ label: 'Auto', value: 'auto' },
{ label: 'PNG', value: 'png' },
{ label: 'JPG', value: 'jpg' },
{ label: 'ZIP', value: 'zip' }
]
}
],
async run($) {
const imageFileB64 = $.step.parameters.imageFileB64 as string;
const size = $.step.parameters.size as string;
const bgColor = $.step.parameters.bgColor as string;
const bgImageUrl = $.step.parameters.bgImageUrl as string;
const outputFormat = $.step.parameters.outputFormat as string;
const body = JSON.stringify({
image_file_b64: imageFileB64,
size: size,
bg_color: bgColor,
bg_image_url: bgImageUrl,
format: outputFormat
});
const response = await $.http.post('/removebg', body, {
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
});
$.setActionItem({ raw: response.data });
}
});

View File

@@ -1,6 +1,7 @@
import defineApp from '../../helpers/define-app';
import addAuthHeader from './common/add-auth-header';
import auth from './auth';
import actions from './actions';
export default defineApp({
name: 'Remove.bg',
@@ -13,4 +14,5 @@ export default defineApp({
primaryColor: '55636c',
beforeRequest: [addAuthHeader],
auth,
actions,
});

View File

@@ -0,0 +1,102 @@
export const fields = [
{
label: 'Name',
key: 'name',
type: 'string' as const,
required: true,
variables: true,
description: '',
},
{
label: 'Email',
key: 'email',
type: 'string' as const,
required: true,
variables: true,
description:
'It is essential to be distinctive. Zendesk prohibits the existence of identical users sharing the same email address.',
},
{
label: 'Details',
key: 'details',
type: 'string' as const,
required: false,
variables: true,
description: '',
},
{
label: 'Notes',
key: 'notes',
type: 'string' as const,
required: false,
variables: true,
description:
'Within this field, you have the capability to save any remarks or comments you may have concerning the user.',
},
{
label: 'Phone',
key: 'phone',
type: 'string' as const,
required: false,
variables: true,
description:
"The user's contact number should be entered in the following format: +1 (555) 123-4567.",
},
{
label: 'Tags',
key: 'tags',
type: 'string' as const,
required: false,
variables: true,
description: 'A comma separated list of tags.',
},
{
label: 'Role',
key: 'role',
type: 'string' as const,
required: false,
variables: true,
description:
"It can take on one of the designated roles: 'end-user', 'agent', or 'admin'. If a different value is set or none is specified, the default is 'end-user.'",
},
{
label: 'Organization',
key: 'organizationId',
type: 'dropdown' as const,
required: false,
variables: true,
description: 'Assign this user to a specific organization.',
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listOrganizations',
},
],
},
},
{
label: 'External Id',
key: 'externalId',
type: 'string' as const,
required: false,
variables: true,
description:
'An exclusive external identifier; you can utilize this to link organizations with an external record.',
},
{
label: 'Verified',
key: 'verified',
type: 'dropdown' as const,
required: false,
description:
"Specify if you can verify that the user's assertion of their identity is accurate.",
variables: true,
options: [
{ label: 'True', value: 'true' },
{ label: 'False', value: 'false' },
],
},
];

View File

@@ -0,0 +1,53 @@
import { IJSONObject } from '@automatisch/types';
import defineAction from '../../../../helpers/define-action';
import { fields } from './fields';
type Payload = {
user: IJSONObject;
};
export default defineAction({
name: 'Create user',
key: 'createUser',
description: 'Creates a new user.',
arguments: fields,
async run($) {
const {
name,
email,
details,
notes,
phone,
role,
organizationId,
externalId,
verified,
} = $.step.parameters;
const tags = $.step.parameters.tags as string;
const formattedTags = tags.split(',');
const payload: Payload = {
user: {
name,
email,
details,
notes,
phone,
organization_id: organizationId,
external_id: externalId,
verified: verified || 'false',
tags: formattedTags,
},
};
if (role) {
payload.user.role = role;
}
const response = await $.http.post('/api/v2/users', payload);
$.setActionItem({ raw: response.data });
},
});

View File

@@ -0,0 +1,35 @@
import defineAction from '../../../../helpers/define-action';
export default defineAction({
name: 'Delete ticket',
key: 'deleteTicket',
description: 'Deletes an existing ticket.',
arguments: [
{
label: 'Ticket',
key: 'ticketId',
type: 'dropdown' as const,
required: true,
variables: true,
description: 'Select the ticket you want to delete.',
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listFirstPageOfTickets',
},
],
},
},
],
async run($) {
const ticketId = $.step.parameters.ticketId;
const response = await $.http.delete(`/api/v2/tickets/${ticketId}`);
$.setActionItem({ raw: { data: response.data } });
},
});

View File

@@ -0,0 +1,43 @@
import defineAction from '../../../../helpers/define-action';
export default defineAction({
name: 'Delete user',
key: 'deleteUser',
description: 'Deletes an existing user.',
arguments: [
{
label: 'User',
key: 'userId',
type: 'dropdown' as const,
required: true,
variables: true,
description: 'Select the user you want to modify.',
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listUsers',
},
{
name: 'parameters.showUserRole',
value: 'true',
},
{
name: 'parameters.includeAllUsers',
value: 'true',
},
],
},
},
],
async run($) {
const userId = $.step.parameters.userId;
const response = await $.http.delete(`/api/v2/users/${userId}`);
$.setActionItem({ raw: response.data });
},
});

View File

@@ -0,0 +1,32 @@
import defineAction from '../../../../helpers/define-action';
export default defineAction({
name: 'Find ticket',
key: 'findTicket',
description: 'Finds an existing ticket.',
arguments: [
{
label: 'Query',
key: 'query',
type: 'string' as const,
required: true,
variables: true,
description:
'Write a search string that specifies the way we will search for the ticket in Zendesk.',
},
],
async run($) {
const query = $.step.parameters.query;
const params = {
query: `type:ticket ${query}`,
sort_by: 'created_at',
sort_order: 'desc',
};
const response = await $.http.get('/api/v2/search', { params });
$.setActionItem({ raw: response.data.results[0] });
},
});

View File

@@ -1,3 +1,15 @@
import createTicket from './create-ticket';
import createUser from './create-user';
import deleteTicket from './delete-ticket';
import deleteUser from './delete-user';
import findTicket from './find-ticket';
import updateTicket from './update-ticket';
export default [createTicket];
export default [
createTicket,
createUser,
deleteTicket,
deleteUser,
findTicket,
updateTicket,
];

View File

@@ -0,0 +1,167 @@
export const fields = [
{
label: 'Ticket',
key: 'ticketId',
type: 'dropdown' as const,
required: true,
variables: true,
description: 'Select the ticket you want to change.',
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listFirstPageOfTickets',
},
],
},
},
{
label: 'Subject',
key: 'subject',
type: 'string' as const,
required: false,
variables: true,
description: '',
},
{
label: 'Assignee',
key: 'assigneeId',
type: 'dropdown' as const,
required: false,
variables: true,
description:
'Note: An error occurs if the assignee is not in the default group (or the specific group chosen below).',
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listUsers',
},
{
name: 'parameters.showUserRole',
value: 'true',
},
{
name: 'parameters.includeAdmins',
value: 'true',
},
],
},
},
{
label: 'Group',
key: 'groupId',
type: 'dropdown' as const,
required: false,
variables: true,
description: 'Allocate this ticket to a specific group.',
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listGroups',
},
],
},
},
{
label: 'New Status',
key: 'status',
type: 'dropdown' as const,
required: false,
variables: true,
description: '',
options: [
{ label: 'New', value: 'new' },
{ label: 'Open', value: 'open' },
{ label: 'Pending', value: 'pending' },
{ label: 'Hold', value: 'hold' },
{ label: 'Solved', value: 'solved' },
{ label: 'Closed', value: 'closed' },
],
},
{
label: 'New comment to add to the ticket',
key: 'comment',
type: 'string' as const,
required: false,
variables: true,
description: '',
},
{
label: 'Should the first comment be public?',
key: 'publicOrNot',
type: 'dropdown' as const,
required: false,
variables: true,
description: '',
options: [
{ label: 'Yes', value: 'yes' },
{ label: 'No', value: 'no' },
],
},
{
label: 'Tags',
key: 'tags',
type: 'string' as const,
required: false,
variables: true,
description: 'A comma separated list of tags.',
},
{
label: 'Type',
key: 'type',
type: 'dropdown' as const,
required: false,
variables: true,
description: '',
options: [
{ label: 'Problem', value: 'problem' },
{ label: 'Incident', value: 'incident' },
{ label: 'Question', value: 'question' },
{ label: 'Task', value: 'task' },
],
},
{
label: 'Priority',
key: 'priority',
type: 'dropdown' as const,
required: false,
variables: true,
description: '',
options: [
{ label: 'Urgent', value: 'urgent' },
{ label: 'High', value: 'high' },
{ label: 'Normal', value: 'normal' },
{ label: 'Low', value: 'low' },
],
},
{
label: 'Submitter',
key: 'submitterId',
type: 'dropdown' as const,
required: false,
variables: true,
description: '',
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listUsers',
},
{
name: 'parameters.includeAdmins',
value: 'false',
},
],
},
},
];

View File

@@ -0,0 +1,57 @@
import defineAction from '../../../../helpers/define-action';
import { fields } from './fields';
import isEmpty from 'lodash/isEmpty';
import omitBy from 'lodash/omitBy';
export default defineAction({
name: 'Update ticket',
key: 'updateTicket',
description: 'Modify the status of an existing ticket or append comments.',
arguments: fields,
async run($) {
const {
ticketId,
subject,
assigneeId,
groupId,
status,
comment,
publicOrNot,
type,
priority,
submitterId,
} = $.step.parameters;
const tags = $.step.parameters.tags as string;
const formattedTags = tags.split(',');
const payload = {
subject,
assignee_id: assigneeId,
group_id: groupId,
status,
comment: {
body: comment,
public: publicOrNot,
},
tags: formattedTags,
type,
priority,
submitter_id: submitterId,
};
const fieldsToRemoveIfEmpty = ['group_id', 'status', 'type', 'priority'];
const filteredPayload = omitBy(
payload,
(value, key) => fieldsToRemoveIfEmpty.includes(key) && isEmpty(value)
);
const response = await $.http.put(`/api/v2/tickets/${ticketId}`, {
ticket: filteredPayload,
});
$.setActionItem({ raw: response.data });
},
});

View File

@@ -1,13 +1,20 @@
import listUsers from './list-users';
import listBrands from './list-brands';
import listFirstPageOfTickets from './list-first-page-of-tickets';
import listGroups from './list-groups';
import listOrganizations from './list-organizations';
import listSharingAgreements from './list-sharing-agreements';
import listTicketForms from './list-ticket-forms';
import listViews from './list-views';
export default [
listUsers,
listBrands,
listFirstPageOfTickets,
listGroups,
listOrganizations,
listSharingAgreements,
listFirstPageOfTickets,
listTicketForms,
listViews,
];

View File

@@ -0,0 +1,33 @@
import { IGlobalVariable, IJSONObject } from '@automatisch/types';
export default {
name: 'List first page of tickets',
key: 'listFirstPageOfTickets',
async run($: IGlobalVariable) {
const tickets: {
data: IJSONObject[];
} = {
data: [],
};
const params = {
'page[size]': 100,
sort: '-id',
};
const response = await $.http.get('/api/v2/tickets', { params });
const allTickets = response.data.tickets;
if (allTickets?.length) {
for (const ticket of allTickets) {
tickets.data.push({
value: ticket.id,
name: ticket.subject,
});
}
}
return tickets;
},
};

View File

@@ -21,7 +21,7 @@ export default {
const response = await $.http.get('/api/v2/groups', { params });
const allGroups = response?.data?.groups;
hasMore = response?.data?.meta?.has_more;
params['page[after]'] = response.data.links?.after_cursor;
params['page[after]'] = response.data.meta?.after_cursor;
if (allGroups?.length) {
for (const group of allGroups) {

View File

@@ -0,0 +1,38 @@
import { IGlobalVariable, IJSONObject } from '@automatisch/types';
export default {
name: 'List organizations',
key: 'listOrganizations',
async run($: IGlobalVariable) {
const organizations: {
data: IJSONObject[];
} = {
data: [],
};
let hasMore;
const params = {
'page[size]': 100,
'page[after]': undefined as unknown as string,
};
do {
const response = await $.http.get('/api/v2/organizations', { params });
const allOrganizations = response?.data?.organizations;
hasMore = response?.data?.meta?.has_more;
params['page[after]'] = response.data.meta?.after_cursor;
if (allOrganizations?.length) {
for (const organization of allOrganizations) {
organizations.data.push({
value: organization.id,
name: organization.name,
});
}
}
} while (hasMore);
return organizations;
},
};

View File

@@ -25,7 +25,7 @@ export default {
const response = await $.http.get('/api/v2/users', { params });
const allUsers = response?.data?.users;
hasMore = response?.data?.meta?.has_more;
params['page[after]'] = response.data.links?.after_cursor;
params['page[after]'] = response.data.meta?.after_cursor;
if (allUsers?.length) {
for (const user of allUsers) {

View File

@@ -0,0 +1,38 @@
import { IGlobalVariable, IJSONObject } from '@automatisch/types';
export default {
name: 'List views',
key: 'listViews',
async run($: IGlobalVariable) {
const views: {
data: IJSONObject[];
} = {
data: [],
};
let hasMore;
const params = {
'page[size]': 100,
'page[after]': undefined as unknown as string,
};
do {
const response = await $.http.get('/api/v2/views', { params });
const allViews = response?.data?.views;
hasMore = response?.data?.meta?.has_more;
params['page[after]'] = response.data.meta?.after_cursor;
if (allViews?.length) {
for (const view of allViews) {
views.data.push({
value: view.id,
name: view.title,
});
}
}
} while (hasMore);
return views;
},
};

View File

@@ -1,6 +1,7 @@
import defineApp from '../../helpers/define-app';
import addAuthHeader from './common/add-auth-headers';
import auth from './auth';
import triggers from './triggers';
import actions from './actions';
import dynamicData from './dynamic-data';
@@ -15,6 +16,7 @@ export default defineApp({
supportsConnections: true,
beforeRequest: [addAuthHeader],
auth,
triggers,
actions,
dynamicData,
});

View File

@@ -0,0 +1,4 @@
import newTickets from './new-tickets';
import newUsers from './new-users';
export default [newTickets, newUsers];

View File

@@ -0,0 +1,59 @@
import defineTrigger from '../../../../helpers/define-trigger';
export default defineTrigger({
name: 'New tickets',
key: 'newTickets',
pollInterval: 15,
description: 'Triggers when a new ticket is created in a specific view.',
arguments: [
{
label: 'View',
key: 'viewId',
type: 'dropdown' as const,
required: true,
variables: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listViews',
},
],
},
},
],
async run($) {
const viewId = $.step.parameters.viewId;
const params = {
'page[size]': 100,
'page[after]': undefined as unknown as string,
sort_by: 'nice_id',
sort_order: 'desc',
};
let hasMore;
do {
const response = await $.http.get(`/api/v2/views/${viewId}/tickets`, {
params,
});
const allTickets = response?.data?.tickets;
hasMore = response?.data?.meta?.has_more;
params['page[after]'] = response.data.meta?.after_cursor;
if (allTickets?.length) {
for (const ticket of allTickets) {
$.pushTriggerItem({
raw: ticket,
meta: {
internalId: ticket.id.toString(),
},
});
}
}
} while (hasMore);
},
});

View File

@@ -0,0 +1,83 @@
import Crypto from 'crypto';
import defineTrigger from '../../../../helpers/define-trigger';
export default defineTrigger({
name: 'New users',
key: 'newUsers',
type: 'webhook',
description: 'Triggers upon the creation of a new user.',
async run($) {
const dataItem = {
raw: $.request.body,
meta: {
internalId: Crypto.randomUUID(),
},
};
$.pushTriggerItem(dataItem);
},
async testRun($) {
const params = {
query: 'type:user',
sort_by: 'created_at',
sort_order: 'desc',
};
const response = await $.http.get('/api/v2/search', { params });
const lastUser = response.data.results[0];
const computedWebhookEvent = {
id: Crypto.randomUUID(),
time: lastUser.created_at,
type: 'zen:event-type:user.created',
event: {},
detail: {
id: lastUser.id,
role: lastUser.role,
email: lastUser.email,
created_at: lastUser.created_at,
updated_at: lastUser.updated_at,
external_id: lastUser.external_id,
organization_id: lastUser.organization_id,
default_group_id: lastUser.default_group_id,
},
subject: `zen:user:${lastUser.id}`,
account_id: '',
zendesk_event_version: '2022-11-06',
};
const dataItem = {
raw: computedWebhookEvent,
meta: {
internalId: computedWebhookEvent.id,
},
};
$.pushTriggerItem(dataItem);
},
async registerHook($) {
const payload = {
webhook: {
name: `Flow ID: ${$.flow.id}`,
status: 'active',
subscriptions: ['zen:event-type:user.created'],
endpoint: $.webhookUrl,
http_method: 'POST',
request_format: 'json',
},
};
const response = await $.http.post('/api/v2/webhooks', payload);
const id = response.data.webhook.id;
await $.flow.setRemoteWebhookId(id);
},
async unregisterHook($) {
await $.http.delete(`/api/v2/webhooks/${$.flow.remoteWebhookId}`);
},
});

View File

@@ -49,6 +49,7 @@ type AppConfig = {
smtpPassword: string;
fromEmail: string;
isCloud: boolean;
isMation: boolean;
isSelfHosted: boolean;
paddleVendorId: number;
paddleVendorAuthCode: string;
@@ -127,6 +128,7 @@ const appConfig: AppConfig = {
fromEmail: process.env.FROM_EMAIL,
isCloud: process.env.AUTOMATISCH_CLOUD === 'true',
isSelfHosted: process.env.AUTOMATISCH_CLOUD !== 'true',
isMation: process.env.MATION === 'true',
paddleVendorId: Number(process.env.PADDLE_VENDOR_ID),
paddleVendorAuthCode: process.env.PADDLE_VENDOR_AUTH_CODE,
paddlePublicKey: process.env.PADDLE_PUBLIC_KEY,

View File

@@ -9,6 +9,7 @@ describe('graphQL getAutomatischInfo query', () => {
query {
getAutomatischInfo {
isCloud
isMation
license {
id
name
@@ -24,6 +25,7 @@ describe('graphQL getAutomatischInfo query', () => {
jest.spyOn(license, 'getLicense').mockResolvedValue(false);
jest.replaceProperty(appConfig, 'isCloud', false);
jest.replaceProperty(appConfig, 'isMation', false);
});
it('should return empty license data', async () => {
@@ -36,6 +38,7 @@ describe('graphQL getAutomatischInfo query', () => {
data: {
getAutomatischInfo: {
isCloud: false,
isMation: false,
license: {
id: null,
name: null,
@@ -77,6 +80,7 @@ describe('graphQL getAutomatischInfo query', () => {
data: {
getAutomatischInfo: {
isCloud: true,
isMation: false,
license: {
expireAt: '2025-08-09T10:56:54.144Z',
id: '123123',
@@ -105,6 +109,69 @@ describe('graphQL getAutomatischInfo query', () => {
const expectedResponsePayload = {
data: {
getAutomatischInfo: {
isCloud: false,
isMation: false,
license: {
expireAt: '2025-08-09T10:56:54.144Z',
id: '123123',
name: 'Test License',
verified: true,
},
},
},
};
expect(response.body).toEqual(expectedResponsePayload);
});
});
describe('and with mation flag enabled', () => {
beforeEach(async () => {
jest.replaceProperty(appConfig, 'isCloud', false);
jest.replaceProperty(appConfig, 'isMation', true);
});
it('should return all license data', async () => {
const response = await request(app)
.post('/graphql')
.send({ query })
.expect(200);
const expectedResponsePayload = {
data: {
getAutomatischInfo: {
isCloud: false,
isMation: true,
license: {
expireAt: '2025-08-09T10:56:54.144Z',
id: '123123',
name: 'Test License',
verified: true,
},
},
},
};
expect(response.body).toEqual(expectedResponsePayload);
});
});
describe('and with mation flag disabled', () => {
beforeEach(async () => {
jest.replaceProperty(appConfig, 'isCloud', false);
jest.replaceProperty(appConfig, 'isMation', false);
});
it('should return all license data', async () => {
const response = await request(app)
.post('/graphql')
.send({ query })
.expect(200);
const expectedResponsePayload = {
data: {
getAutomatischInfo: {
isMation: false,
isCloud: false,
license: {
expireAt: '2025-08-09T10:56:54.144Z',

View File

@@ -13,6 +13,7 @@ const getAutomatischInfo = async () => {
return {
isCloud: appConfig.isCloud,
isMation: appConfig.isMation,
license: computedLicense,
};
};

View File

@@ -0,0 +1,10 @@
import appConfig from '../../config/app';
const getUseJsFile = async () => {
return {
canInvoke: true,
appConfig,
};
};
export default getUseJsFile;

View File

@@ -6,6 +6,7 @@ import getApps from './queries/get-apps';
import getAutomatischInfo from './queries/get-automatisch-info';
import getBillingAndUsage from './queries/get-billing-and-usage.ee';
import getConfig from './queries/get-config.ee';
import getUseJsFile from './queries/get-use-js-file.js';
import getConnectedApps from './queries/get-connected-apps';
import getCurrentUser from './queries/get-current-user';
import getDynamicData from './queries/get-dynamic-data';
@@ -68,6 +69,7 @@ const queryResolvers = {
healthcheck,
listSamlAuthProviders,
testConnection,
getUseJsFile,
};
export default queryResolvers;

View File

@@ -11,6 +11,7 @@ type Query {
getConnectedApps(name: String): [App]
testConnection(id: String!): Connection
getFlow(id: String!): Flow
getUseJsFile: JSONObject
getFlows(
limit: Int!
offset: Int!
@@ -646,6 +647,7 @@ type AppHealth {
type GetAutomatischInfo {
isCloud: Boolean
isMation: Boolean
license: License
}

View File

@@ -2,11 +2,12 @@
"compilerOptions": {
"baseUrl": ".",
"declaration": true,
"allowJs": true,
"esModuleInterop": true,
"lib": ["es2021"],
"module": "commonjs",
"moduleResolution": "node",
"noImplicitAny": true,
"noImplicitAny": false,
"outDir": "dist",
"paths": {
"*": ["../../node_modules/*", "node_modules/*", "src/types/*"]

View File

@@ -303,7 +303,10 @@ export default defineConfig({
text: 'Remove.bg',
collapsible: true,
collapsed: true,
items: [{ text: 'Connection', link: '/apps/removebg/connection' }],
items: [
{ text: 'Actions', link: '/apps/removebg/actions' },
{ text: 'Connection', link: '/apps/removebg/connection' }
],
},
{
text: 'RSS',

View File

@@ -3,6 +3,8 @@ favicon: /favicons/discord.svg
items:
- name: Send a message to channel
desc: Sends a message to a specific channel you specify.
- name: Create a scheduled event
desc: Creates a scheduled event.
---
<script setup>

View File

@@ -3,6 +3,8 @@ favicon: /favicons/notion.svg
items:
- name: New database items
desc: Triggers when a new database item is created.
- name: Updated database items
desc: Triggers when there is an update to an item in a chosen database.
---
<script setup>

View File

@@ -0,0 +1,12 @@
---
favicon: /favicons/removebg.svg
items:
- name: Remove Image Background
desc: Remove backgrounds 100% automatically in 5 seconds with one click.
---
<script setup>
import CustomListing from '../../components/CustomListing.vue'
</script>
<CustomListing />

View File

@@ -12,7 +12,9 @@ connection in Automatisch. If any of the steps are outdated, please let us know!
1. Enter necessary information in the form.
1. Check **Enable OAuth Settings** checkbox.
1. Copy **OAuth Redirect URL** from Automatisch and paste it to the **Callback URL** field.
1. Add any scopes you plan to use in the **Selected OAuth Scopes** section.
1. Add any scopes you plan to use in the **Selected OAuth Scopes** section. We suggest `full` and `refresh_token, offline_access` scopes.
1. Uncheck "Require Proof Key for Code Exchange (PKCE) Extension for Supported Authorization Flows" checkbox.
1. Check "Enable Authorization Code and Credentials Flow" checkbox
1. Click on the **Save** button at the bottom of the page.
1. Acknowledge the information and click on the **Continue** button.
1. In the **API (Enable OAuth Settings)** section, click the **Manager Consumer Details** button.

View File

@@ -3,6 +3,16 @@ favicon: /favicons/zendesk.svg
items:
- name: Create ticket
desc: Creates a new ticket.
- name: Create user
desc: Creates a new user.
- name: Delete ticket
desc: Deletes an existing ticket.
- name: Delete user
desc: Deletes an existing user.
- name: Find ticket
desc: Finds an existing ticket.
- name: Update ticket
desc: Modify the status of an existing ticket or append comments.
---
<script setup>

View File

@@ -0,0 +1,14 @@
---
favicon: /favicons/zendesk.svg
items:
- name: New tickets
desc: Triggers when a new ticket is created in a specific view.
- name: New users
desc: Triggers upon the creation of a new user.
---
<script setup>
import CustomListing from '../../components/CustomListing.vue'
</script>
<CustomListing />

View File

@@ -31,7 +31,7 @@ The following integrations are currently supported by Automatisch.
- [PostgreSQL](/apps/postgresql/actions)
- [Pushover](/apps/pushover/actions)
- [Reddit](/apps/reddit/triggers)
- [Remove.bg](/apps/removebg/connection)
- [Remove.bg](/apps/removebg/actions)
- [RSS](/apps/rss/triggers)
- [Salesforce](/apps/salesforce/triggers)
- [Scheduler](/apps/scheduler/triggers)

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -25,12 +25,52 @@
-->
<title>Automatisch</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap"
rel="stylesheet"
rel="preload"
href="/fonts/Inter-Regular.ttf"
as="font"
crossorigin
type="font/ttf"
/>
<link
rel="preload"
href="/fonts/Inter-Medium.ttf"
as="font"
crossorigin
type="font/ttf"
/>
<link
rel="preload"
href="/fonts/Inter-Bold.ttf"
as="font"
crossorigin
type="font/ttf"
/>
<style>
@font-face {
font-family: 'Inter';
src: url('/fonts/Inter-Regular.ttf') format('truetype');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Inter';
src: url('/fonts/Inter-Medium.ttf') format('truetype');
font-weight: 500;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Inter';
src: url('/fonts/Inter-Bold.ttf') format('truetype');
font-weight: 700;
font-style: normal;
font-display: swap;
}
</style>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>

View File

@@ -2,7 +2,7 @@ import styled from '@emotion/styled';
export const LogoImage = styled('img')(() => ({
maxWidth: 200,
maxHeight: 50,
maxHeight: 22,
width: '100%',
height: 'auto',
}));

View File

@@ -0,0 +1,22 @@
import Typography from '@mui/material/Typography';
import * as React from 'react';
import { FormattedMessage } from 'react-intl';
import MationLogo from 'components/MationLogo';
import useAutomatischInfo from 'hooks/useAutomatischInfo';
const DefaultLogo = () => {
const { isMation, loading } = useAutomatischInfo();
if (loading) return <React.Fragment />;
if (isMation) return <MationLogo />;
return (
<Typography variant="h6" component="h1" data-test="typography-logo" noWrap>
<FormattedMessage id="brandText" />
</Typography>
);
};
export default DefaultLogo;

View File

@@ -12,6 +12,7 @@ import * as URLS from 'config/urls';
import useVersion from 'hooks/useVersion';
import AppBar from 'components/AppBar';
import Drawer from 'components/Drawer';
import useAutomatischInfo from 'hooks/useAutomatischInfo';
type PublicLayoutProps = {
children: React.ReactNode;
@@ -38,7 +39,22 @@ const drawerLinks = [
},
];
const generateDrawerBottomLinks = ({ notificationBadgeContent = 0 }) => [
type GenerateDrawerBottomLinksOptions = {
isMation: boolean;
loading: boolean;
notificationBadgeContent: number;
};
const generateDrawerBottomLinks = ({
isMation,
loading,
notificationBadgeContent = 0,
}: GenerateDrawerBottomLinksOptions) => {
if (loading || isMation) {
return [];
}
return [
{
Icon: NotificationsIcon,
primary: 'settingsDrawer.notifications',
@@ -46,11 +62,13 @@ const generateDrawerBottomLinks = ({ notificationBadgeContent = 0 }) => [
badgeContent: notificationBadgeContent,
},
];
};
export default function PublicLayout({
children,
}: PublicLayoutProps): React.ReactElement {
const version = useVersion();
const { isMation, loading } = useAutomatischInfo();
const theme = useTheme();
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('lg'));
const [isDrawerOpen, setDrawerOpen] = React.useState(!matchSmallScreens);
@@ -60,6 +78,8 @@ export default function PublicLayout({
const drawerBottomLinks = generateDrawerBottomLinks({
notificationBadgeContent: version.newVersionCount,
loading,
isMation,
});
return (

View File

@@ -1,8 +1,7 @@
import Typography from '@mui/material/Typography';
import * as React from 'react';
import { FormattedMessage } from 'react-intl';
import CustomLogo from 'components/CustomLogo/index.ee';
import DefaultLogo from 'components/DefaultLogo';
import useConfig from 'hooks/useConfig';
const Logo = () => {
@@ -13,11 +12,7 @@ const Logo = () => {
if (logoSvgData) return <CustomLogo />;
return (
<Typography variant="h6" component="h1" data-test="typography-logo" noWrap>
<FormattedMessage id="brandText" />
</Typography>
);
return <DefaultLogo />;
};
export default Logo;

View File

@@ -0,0 +1,3 @@
<svg width="115" height="22" viewBox="0 0 411 77" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M276.991 15.3555C281.231 15.3555 284.669 11.918 284.669 7.67773C284.669 3.43744 281.231 0 276.991 0C272.751 0 269.313 3.43744 269.313 7.67773C269.313 11.918 272.751 15.3555 276.991 15.3555ZM367.497 50.0315C367.497 41.5346 374.385 34.6464 382.882 34.6464C391.379 34.6464 398.267 41.5346 398.267 50.0315V71.2076H409.851V50.0315C409.851 35.1371 397.777 23.0627 382.882 23.0627C367.988 23.0627 355.914 35.1371 355.914 50.0315V71.2076H367.497V50.0315ZM271.199 71.2071V28.8539H282.783V71.2071H271.199ZM237.933 34.6464V71.2076H249.517V34.6464H259.608V23.0627H249.517V7.67718H237.933V23.0627H227.843V34.6464H237.933ZM176.899 50.0296C176.899 58.5265 183.787 65.4146 192.284 65.4146V76.9983C177.389 76.9983 165.315 64.924 165.315 50.0296C165.315 35.1351 177.389 23.0608 192.284 23.0608C207.178 23.0608 219.252 35.1359 219.252 50.0303L219.253 71.2068H207.669L207.669 50.0303C207.669 41.5334 200.781 34.6445 192.284 34.6445C183.787 34.6445 176.899 41.5326 176.899 50.0296ZM71.0145 50.0315C71.0145 41.5346 77.9026 34.6464 86.3995 34.6464C94.8965 34.6464 101.785 41.5346 101.785 50.0315V71.2071H113.368V50.0315C113.368 41.5346 120.256 34.6464 128.753 34.6464C137.25 34.6464 144.138 41.5346 144.138 50.0315L144.138 71.2071H155.722L155.722 50.0315C155.722 35.1371 143.647 23.0627 128.753 23.0627C120.165 23.0627 112.515 27.0767 107.576 33.3308C102.637 27.0767 94.9873 23.0627 86.3995 23.0627C71.5051 23.0627 59.4308 35.1371 59.4308 50.0315V71.2071H71.0145V50.0315ZM44.0459 65.4162V76.9999H1.69178V65.4162H44.0459ZM292.376 50.0305C292.376 64.925 304.45 76.9993 319.345 76.9993C334.239 76.9993 346.313 64.9257 346.313 50.0313C346.313 35.1369 334.239 23.0618 319.345 23.0618C304.45 23.0618 292.376 35.1361 292.376 50.0305ZM319.345 65.4157C310.848 65.4157 303.96 58.5276 303.96 50.0306C303.96 41.5337 310.848 34.6456 319.345 34.6456C327.842 34.6456 334.729 41.5345 334.729 50.0314C334.729 58.5283 327.842 65.4157 319.345 65.4157Z" fill="#ffffff"/>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1,8 @@
import * as React from 'react';
import { ReactComponent as MationLogoSvg } from './assets/mation-logo.svg';
const MationLogo = () => {
return <MationLogoSvg />;
};
export default MationLogo;

View File

@@ -7,19 +7,20 @@ import * as React from 'react';
import { IJSONObject } from '@automatisch/types';
import useConfig from 'hooks/useConfig';
import theme from 'styles/theme';
import useAutomatischInfo from 'hooks/useAutomatischInfo';
import { defaultTheme, mationTheme } from 'styles/theme';
type ThemeProviderProps = {
children: React.ReactNode;
};
const customizeTheme = (defaultTheme: typeof theme, config: IJSONObject) => {
const customizeTheme = (theme: typeof defaultTheme, config: IJSONObject) => {
// `clone` is needed so that the new theme reference triggers re-render
const shallowDefaultTheme = clone(defaultTheme);
const shallowDefaultTheme = clone(theme);
for (const key in config) {
const value = config[key];
const exists = get(defaultTheme, key);
const exists = get(theme, key);
if (exists) {
set(shallowDefaultTheme, key, value);
@@ -33,18 +34,21 @@ const ThemeProvider = ({
children,
...props
}: ThemeProviderProps): React.ReactElement => {
const { config, loading } = useConfig();
const { isMation, loading: automatischInfoLoading } = useAutomatischInfo();
const { config, loading: configLoading } = useConfig();
const customTheme = React.useMemo(() => {
if (!config) return theme;
const installationTheme = isMation ? mationTheme : defaultTheme;
const customTheme = customizeTheme(theme, config);
if (configLoading || automatischInfoLoading) return installationTheme;
const customTheme = customizeTheme(installationTheme, config || {});
return customTheme;
}, [config]);
}, [configLoading, config, isMation, automatischInfoLoading]);
// TODO: maybe a global loading state for the custom theme?
if (loading) return <></>;
if (automatischInfoLoading || configLoading) return <></>;
return (
<BaseThemeProvider theme={customTheme} {...props}>

View File

@@ -5,11 +5,15 @@ import { GET_AUTOMATISCH_INFO } from 'graphql/queries/get-automatisch-info';
export type AutomatischInfoContextParams = {
isCloud: boolean;
isMation: boolean;
loading: boolean;
};
export const AutomatischInfoContext =
React.createContext<AutomatischInfoContextParams>({
isCloud: false,
isMation: false,
loading: true,
});
type AutomatischInfoProviderProps = {
@@ -23,13 +27,15 @@ export const AutomatischInfoProvider = (
const { data, loading } = useQuery(GET_AUTOMATISCH_INFO);
const isCloud = data?.getAutomatischInfo?.isCloud;
const isMation = data?.getAutomatischInfo?.isMation;
const value = React.useMemo(() => {
return {
isCloud,
loading
isMation,
loading,
};
}, [isCloud, loading]);
}, [isCloud, isMation, loading]);
return (
<AutomatischInfoContext.Provider value={value}>

View File

@@ -4,6 +4,7 @@ export const GET_AUTOMATISCH_INFO = gql`
query GetAutomatischInfo {
getAutomatischInfo {
isCloud
isMation
}
}
`;

View File

@@ -3,6 +3,8 @@ import { AutomatischInfoContext } from 'contexts/AutomatischInfo';
type UseAutomatischInfoReturn = {
isCloud: boolean;
isMation: boolean;
loading: boolean;
};
export default function useAutomatischInfo(): UseAutomatischInfoReturn {
@@ -10,5 +12,7 @@ export default function useAutomatischInfo(): UseAutomatischInfoReturn {
return {
isCloud: automatischInfoContext.isCloud,
isMation: automatischInfoContext.isMation,
loading: automatischInfoContext.loading,
};
}

View File

@@ -12,7 +12,6 @@ import LiveChat from 'components/LiveChat/index.ee';
import routes from 'routes';
import reportWebVitals from './reportWebVitals';
ReactDOM.render(
<Router>
<SnackbarProvider>

View File

@@ -1,4 +1,5 @@
import * as React from 'react';
import { useNavigate } from 'react-router-dom';
import Box from '@mui/material/Box';
import Stack from '@mui/material/Stack';
@@ -7,6 +8,8 @@ import Container from 'components/Container';
import NotificationCard from 'components/NotificationCard';
import PageTitle from 'components/PageTitle';
import useFormatMessage from 'hooks/useFormatMessage';
import useAutomatischInfo from 'hooks/useAutomatischInfo';
import * as URLS from 'config/urls';
interface INotification {
name: string;
@@ -16,8 +19,19 @@ interface INotification {
}
export default function Updates(): React.ReactElement {
const navigate = useNavigate();
const formatMessage = useFormatMessage();
const { notifications } = useNotifications();
const { isMation, loading } = useAutomatischInfo();
React.useEffect(
function redirectToHomepageInMation() {
if (!loading && isMation) {
navigate(URLS.DASHBOARD);
}
},
[loading, isMation]
);
return (
<Box sx={{ py: 3 }}>

View File

@@ -1,3 +1,5 @@
import { deepmerge } from '@mui/utils';
import type { Theme } from '@mui/material/styles';
import { createTheme, alpha } from '@mui/material/styles';
import { cardActionAreaClasses } from '@mui/material/CardActionArea';
@@ -6,7 +8,7 @@ export const primaryMainColor = '#0059F7';
export const primaryLightColor = '#4286FF';
export const primaryDarkColor = '#001F52';
const extendedTheme = createTheme({
export const defaultTheme = createTheme({
palette: {
primary: {
main: primaryMainColor,
@@ -280,4 +282,24 @@ const extendedTheme = createTheme({
},
});
export default extendedTheme;
export const mationTheme = createTheme(deepmerge(defaultTheme, {
palette: {
primary: {
main: '#2962FF',
light: '#448AFF',
dark: '#2962FF',
contrastText: '#fff',
},
},
components: {
MuiAppBar: {
styleOverrides: {
root: ({ theme }: { theme: Theme }) => ({
zIndex: theme.zIndex.drawer + 1,
}),
},
},
},
}));
export default defaultTheme;