Compare commits
54 Commits
v0.10.0
...
shared-con
Author | SHA1 | Date | |
---|---|---|---|
![]() |
8f09681771 | ||
![]() |
3801f9cfa0 | ||
![]() |
b6eea0b5fc | ||
![]() |
099a8ea2cf | ||
![]() |
aefff5c861 | ||
![]() |
a296b5e645 | ||
![]() |
eb486a3a07 | ||
![]() |
062b8521ba | ||
![]() |
1b07f3195a | ||
![]() |
dfa7d4cb8d | ||
![]() |
a14dd9666c | ||
![]() |
b07bd4374f | ||
![]() |
b4e12b0ea8 | ||
![]() |
ee5c17bb85 | ||
![]() |
16c9d3400c | ||
![]() |
4dd994348d | ||
![]() |
f0cbfafc24 | ||
![]() |
d3f38f5488 | ||
![]() |
737090a67a | ||
![]() |
4f66a4d090 | ||
![]() |
df54f909c1 | ||
![]() |
772b195eca | ||
![]() |
87866e34ed | ||
![]() |
c98ac05097 | ||
![]() |
36f991b6f9 | ||
![]() |
a81c5164fc | ||
![]() |
5942482690 | ||
![]() |
4f538ca2fc | ||
![]() |
9f2281a3e2 | ||
![]() |
b0d2f28c78 | ||
![]() |
d4380a4426 | ||
![]() |
ae2738d4cc | ||
![]() |
aa5ae028b2 | ||
![]() |
7ab8c76aa0 | ||
![]() |
8075b65e14 | ||
![]() |
073ce3bf1b | ||
![]() |
80fcbfe01b | ||
![]() |
dba0041e5f | ||
![]() |
b8a44afd25 | ||
![]() |
e2445bf585 | ||
![]() |
50706c524e | ||
![]() |
11e0cb9398 | ||
![]() |
1e82e40802 | ||
![]() |
ff00644e62 | ||
![]() |
97bcd3792b | ||
![]() |
5738a09771 | ||
![]() |
c461cc4878 | ||
![]() |
878fab347a | ||
![]() |
354b331b08 | ||
![]() |
3b9aadb90f | ||
![]() |
7193d018ce | ||
![]() |
d5cea034ac | ||
![]() |
a2760c10b3 | ||
![]() |
3593cf3808 |
@@ -38,7 +38,7 @@
|
|||||||
"@types/xmlrpc": "^1.3.7",
|
"@types/xmlrpc": "^1.3.7",
|
||||||
"accounting": "^0.4.1",
|
"accounting": "^0.4.1",
|
||||||
"ajv-formats": "^2.1.1",
|
"ajv-formats": "^2.1.1",
|
||||||
"axios": "0.24.0",
|
"axios": "1.6.0",
|
||||||
"bcrypt": "^5.0.1",
|
"bcrypt": "^5.0.1",
|
||||||
"bullmq": "^3.0.0",
|
"bullmq": "^3.0.0",
|
||||||
"copyfiles": "^2.4.1",
|
"copyfiles": "^2.4.1",
|
||||||
@@ -59,8 +59,8 @@
|
|||||||
"http-proxy-agent": "^7.0.0",
|
"http-proxy-agent": "^7.0.0",
|
||||||
"https-proxy-agent": "^7.0.1",
|
"https-proxy-agent": "^7.0.1",
|
||||||
"jsonwebtoken": "^9.0.0",
|
"jsonwebtoken": "^9.0.0",
|
||||||
"knex": "^2.4.0",
|
|
||||||
"libphonenumber-js": "^1.10.48",
|
"libphonenumber-js": "^1.10.48",
|
||||||
|
"knex": "^2.5.1",
|
||||||
"lodash.get": "^4.4.2",
|
"lodash.get": "^4.4.2",
|
||||||
"luxon": "2.5.2",
|
"luxon": "2.5.2",
|
||||||
"memory-cache": "^0.2.0",
|
"memory-cache": "^0.2.0",
|
||||||
@@ -69,7 +69,7 @@
|
|||||||
"node-html-markdown": "^1.3.0",
|
"node-html-markdown": "^1.3.0",
|
||||||
"nodemailer": "6.7.0",
|
"nodemailer": "6.7.0",
|
||||||
"oauth-1.0a": "^2.2.6",
|
"oauth-1.0a": "^2.2.6",
|
||||||
"objection": "^3.0.0",
|
"objection": "^3.1.1",
|
||||||
"passport": "^0.6.0",
|
"passport": "^0.6.0",
|
||||||
"pg": "^8.7.1",
|
"pg": "^8.7.1",
|
||||||
"php-serialize": "^4.0.2",
|
"php-serialize": "^4.0.2",
|
||||||
|
@@ -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 });
|
||||||
|
},
|
||||||
|
});
|
@@ -1,3 +1,4 @@
|
|||||||
import sendMessageToChannel from './send-message-to-channel';
|
import sendMessageToChannel from './send-message-to-channel';
|
||||||
|
import createScheduledEvent from './create-scheduled-event';
|
||||||
|
|
||||||
export default [sendMessageToChannel];
|
export default [sendMessageToChannel, createScheduledEvent];
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
import listChannels from './list-channels';
|
import listChannels from './list-channels';
|
||||||
|
import listVoiceChannels from './list-voice-channels';
|
||||||
|
|
||||||
export default [listChannels];
|
export default [listChannels, listVoiceChannels];
|
||||||
|
@@ -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;
|
||||||
|
},
|
||||||
|
};
|
@@ -0,0 +1,3 @@
|
|||||||
|
import listExternalScheduledEventFields from './list-external-scheduled-event-fields';
|
||||||
|
|
||||||
|
export default [listExternalScheduledEventFields];
|
@@ -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,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
};
|
@@ -4,6 +4,7 @@ import auth from './auth';
|
|||||||
import dynamicData from './dynamic-data';
|
import dynamicData from './dynamic-data';
|
||||||
import actions from './actions';
|
import actions from './actions';
|
||||||
import triggers from './triggers';
|
import triggers from './triggers';
|
||||||
|
import dynamicFields from './dynamic-fields';
|
||||||
|
|
||||||
export default defineApp({
|
export default defineApp({
|
||||||
name: 'Discord',
|
name: 'Discord',
|
||||||
@@ -17,6 +18,7 @@ export default defineApp({
|
|||||||
beforeRequest: [addAuthHeader],
|
beforeRequest: [addAuthHeader],
|
||||||
auth,
|
auth,
|
||||||
dynamicData,
|
dynamicData,
|
||||||
|
dynamicFields,
|
||||||
triggers,
|
triggers,
|
||||||
actions,
|
actions,
|
||||||
});
|
});
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
import newDatabaseItems from './new-database-items';
|
import newDatabaseItems from './new-database-items';
|
||||||
|
import updatedDatabaseItems from './updated-database-items';
|
||||||
|
|
||||||
export default [newDatabaseItems];
|
export default [newDatabaseItems, updatedDatabaseItems];
|
||||||
|
@@ -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($);
|
||||||
|
},
|
||||||
|
});
|
@@ -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;
|
@@ -0,0 +1,53 @@
|
|||||||
|
import defineAction from '../../../../helpers/define-action';
|
||||||
|
import { URLSearchParams } from 'url';
|
||||||
|
|
||||||
|
export default defineAction({
|
||||||
|
name: 'Create link post',
|
||||||
|
key: 'createLinkPost',
|
||||||
|
description: 'Create a new link post within a subreddit.',
|
||||||
|
arguments: [
|
||||||
|
{
|
||||||
|
label: 'Title',
|
||||||
|
key: 'title',
|
||||||
|
type: 'string' as const,
|
||||||
|
required: true,
|
||||||
|
description:
|
||||||
|
'Heading for the recent post. Limited to 300 characters or less.',
|
||||||
|
variables: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Subreddit',
|
||||||
|
key: 'subreddit',
|
||||||
|
type: 'string' as const,
|
||||||
|
required: true,
|
||||||
|
description: 'The subreddit for posting. Note: Exclude /r/.',
|
||||||
|
variables: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Url',
|
||||||
|
key: 'url',
|
||||||
|
type: 'string' as const,
|
||||||
|
required: true,
|
||||||
|
description: '',
|
||||||
|
variables: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
async run($) {
|
||||||
|
const { title, subreddit, url } = $.step.parameters;
|
||||||
|
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
kind: 'link',
|
||||||
|
api_type: 'json',
|
||||||
|
title: title as string,
|
||||||
|
sr: subreddit as string,
|
||||||
|
url: url as string,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data } = await $.http.post('/api/submit', params.toString());
|
||||||
|
|
||||||
|
$.setActionItem({
|
||||||
|
raw: data,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
3
packages/backend/src/apps/reddit/actions/index.ts
Normal file
3
packages/backend/src/apps/reddit/actions/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import createLinkPost from './create-link-post';
|
||||||
|
|
||||||
|
export default [createLinkPost];
|
1
packages/backend/src/apps/reddit/assets/favicon.svg
Normal file
1
packages/backend/src/apps/reddit/assets/favicon.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" class="_1O4jTk-dZ-VIxsCuYB6OR8 " width="40" height="48" ><g><circle fill="#FF4500" cx="10" cy="10" r="10"></circle><path fill="#FFFFFF" d="M16.67,10A1.46,1.46,0,0,0,14.2,9a7.12,7.12,0,0,0-3.85-1.23L11,4.65,13.14,5.1a1,1,0,1,0,.13-0.61L10.82,4a0.31,0.31,0,0,0-.37.24L9.71,7.71a7.14,7.14,0,0,0-3.9,1.23A1.46,1.46,0,1,0,4.2,11.33a2.87,2.87,0,0,0,0,.44c0,2.24,2.61,4.06,5.83,4.06s5.83-1.82,5.83-4.06a2.87,2.87,0,0,0,0-.44A1.46,1.46,0,0,0,16.67,10Zm-10,1a1,1,0,1,1,1,1A1,1,0,0,1,6.67,11Zm5.81,2.75a3.84,3.84,0,0,1-2.47.77,3.84,3.84,0,0,1-2.47-.77,0.27,0.27,0,0,1,.38-0.38A3.27,3.27,0,0,0,10,14a3.28,3.28,0,0,0,2.09-.61A0.27,0.27,0,1,1,12.48,13.79Zm-0.18-1.71a1,1,0,1,1,1-1A1,1,0,0,1,12.29,12.08Z"></path></g></svg>
|
After Width: | Height: | Size: 813 B |
26
packages/backend/src/apps/reddit/auth/generate-auth-url.ts
Normal file
26
packages/backend/src/apps/reddit/auth/generate-auth-url.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { IField, IGlobalVariable } from '@automatisch/types';
|
||||||
|
import { URLSearchParams } from 'url';
|
||||||
|
import authScope from '../common/auth-scope';
|
||||||
|
|
||||||
|
export default async function generateAuthUrl($: IGlobalVariable) {
|
||||||
|
const oauthRedirectUrlField = $.app.auth.fields.find(
|
||||||
|
(field: IField) => field.key == 'oAuthRedirectUrl'
|
||||||
|
);
|
||||||
|
const redirectUri = oauthRedirectUrlField.value as string;
|
||||||
|
const state = Math.random().toString() as string;
|
||||||
|
const searchParams = new URLSearchParams({
|
||||||
|
client_id: $.auth.data.clientId as string,
|
||||||
|
response_type: 'code',
|
||||||
|
redirect_uri: redirectUri,
|
||||||
|
duration: 'permanent',
|
||||||
|
scope: authScope.join(' '),
|
||||||
|
state,
|
||||||
|
});
|
||||||
|
|
||||||
|
const url = `https://www.reddit.com/api/v1/authorize?${searchParams.toString()}`;
|
||||||
|
|
||||||
|
await $.auth.set({
|
||||||
|
url,
|
||||||
|
originalState: state,
|
||||||
|
});
|
||||||
|
}
|
48
packages/backend/src/apps/reddit/auth/index.ts
Normal file
48
packages/backend/src/apps/reddit/auth/index.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import generateAuthUrl from './generate-auth-url';
|
||||||
|
import verifyCredentials from './verify-credentials';
|
||||||
|
import refreshToken from './refresh-token';
|
||||||
|
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/reddit/connections/add',
|
||||||
|
placeholder: null,
|
||||||
|
description:
|
||||||
|
'When asked to input a redirect URL in Reddit, enter the URL above.',
|
||||||
|
clickToCopy: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'clientId',
|
||||||
|
label: 'Client ID',
|
||||||
|
type: 'string' as const,
|
||||||
|
required: true,
|
||||||
|
readOnly: false,
|
||||||
|
value: null,
|
||||||
|
placeholder: null,
|
||||||
|
description: null,
|
||||||
|
clickToCopy: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'clientSecret',
|
||||||
|
label: 'Client Secret',
|
||||||
|
type: 'string' as const,
|
||||||
|
required: true,
|
||||||
|
readOnly: false,
|
||||||
|
value: null,
|
||||||
|
placeholder: null,
|
||||||
|
description: null,
|
||||||
|
clickToCopy: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
generateAuthUrl,
|
||||||
|
verifyCredentials,
|
||||||
|
isStillVerified,
|
||||||
|
refreshToken,
|
||||||
|
};
|
@@ -0,0 +1,9 @@
|
|||||||
|
import { IGlobalVariable } from '@automatisch/types';
|
||||||
|
import getCurrentUser from '../common/get-current-user';
|
||||||
|
|
||||||
|
const isStillVerified = async ($: IGlobalVariable) => {
|
||||||
|
const currentUser = await getCurrentUser($);
|
||||||
|
return !!currentUser.id;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default isStillVerified;
|
29
packages/backend/src/apps/reddit/auth/refresh-token.ts
Normal file
29
packages/backend/src/apps/reddit/auth/refresh-token.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { URLSearchParams } from 'node:url';
|
||||||
|
import { IGlobalVariable } from '@automatisch/types';
|
||||||
|
|
||||||
|
const refreshToken = async ($: IGlobalVariable) => {
|
||||||
|
const headers = {
|
||||||
|
Authorization: `Basic ${Buffer.from(
|
||||||
|
$.auth.data.clientId + ':' + $.auth.data.clientSecret
|
||||||
|
).toString('base64')}`,
|
||||||
|
};
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
grant_type: 'refresh_token',
|
||||||
|
refresh_token: $.auth.data.refreshToken as string,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data } = await $.http.post(
|
||||||
|
'https://www.reddit.com/api/v1/access_token',
|
||||||
|
params.toString(),
|
||||||
|
{ headers }
|
||||||
|
);
|
||||||
|
|
||||||
|
await $.auth.set({
|
||||||
|
accessToken: data.access_token,
|
||||||
|
expiresIn: data.expires_in,
|
||||||
|
scope: data.scope,
|
||||||
|
tokenType: data.token_type,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default refreshToken;
|
48
packages/backend/src/apps/reddit/auth/verify-credentials.ts
Normal file
48
packages/backend/src/apps/reddit/auth/verify-credentials.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { IField, IGlobalVariable } from '@automatisch/types';
|
||||||
|
import getCurrentUser from '../common/get-current-user';
|
||||||
|
import { URLSearchParams } from 'url';
|
||||||
|
|
||||||
|
const verifyCredentials = async ($: IGlobalVariable) => {
|
||||||
|
if ($.auth.data.originalState !== $.auth.data.state) {
|
||||||
|
throw new Error(`The 'state' parameter does not match.`);
|
||||||
|
}
|
||||||
|
const oauthRedirectUrlField = $.app.auth.fields.find(
|
||||||
|
(field: IField) => field.key == 'oAuthRedirectUrl'
|
||||||
|
);
|
||||||
|
const redirectUri = oauthRedirectUrlField.value as string;
|
||||||
|
const headers = {
|
||||||
|
Authorization: `Basic ${Buffer.from(
|
||||||
|
$.auth.data.clientId + ':' + $.auth.data.clientSecret
|
||||||
|
).toString('base64')}`,
|
||||||
|
};
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
grant_type: 'authorization_code',
|
||||||
|
code: $.auth.data.code as string,
|
||||||
|
redirect_uri: redirectUri,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data } = await $.http.post(
|
||||||
|
'https://www.reddit.com/api/v1/access_token',
|
||||||
|
params.toString(),
|
||||||
|
{ headers }
|
||||||
|
);
|
||||||
|
|
||||||
|
await $.auth.set({
|
||||||
|
accessToken: data.access_token,
|
||||||
|
tokenType: data.token_type,
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentUser = await getCurrentUser($);
|
||||||
|
const screenName = currentUser?.name;
|
||||||
|
|
||||||
|
await $.auth.set({
|
||||||
|
clientId: $.auth.data.clientId,
|
||||||
|
clientSecret: $.auth.data.clientSecret,
|
||||||
|
scope: $.auth.data.scope,
|
||||||
|
expiresIn: data.expires_in,
|
||||||
|
refreshToken: data.refresh_token,
|
||||||
|
screenName,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default verifyCredentials;
|
23
packages/backend/src/apps/reddit/common/add-auth-header.ts
Normal file
23
packages/backend/src/apps/reddit/common/add-auth-header.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default addAuthHeader;
|
3
packages/backend/src/apps/reddit/common/auth-scope.ts
Normal file
3
packages/backend/src/apps/reddit/common/auth-scope.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
const authScope: string[] = ['identity', 'read', 'account', 'submit'];
|
||||||
|
|
||||||
|
export default authScope;
|
@@ -0,0 +1,8 @@
|
|||||||
|
import { IGlobalVariable } from '@automatisch/types';
|
||||||
|
|
||||||
|
const getCurrentUser = async ($: IGlobalVariable) => {
|
||||||
|
const { data: currentUser } = await $.http.get('/api/v1/me');
|
||||||
|
return currentUser;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getCurrentUser;
|
0
packages/backend/src/apps/reddit/index.d.ts
vendored
Normal file
0
packages/backend/src/apps/reddit/index.d.ts
vendored
Normal file
20
packages/backend/src/apps/reddit/index.ts
Normal file
20
packages/backend/src/apps/reddit/index.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import defineApp from '../../helpers/define-app';
|
||||||
|
import addAuthHeader from './common/add-auth-header';
|
||||||
|
import auth from './auth';
|
||||||
|
import triggers from './triggers';
|
||||||
|
import actions from './actions';
|
||||||
|
|
||||||
|
export default defineApp({
|
||||||
|
name: 'Reddit',
|
||||||
|
key: 'reddit',
|
||||||
|
baseUrl: 'https://www.reddit.com',
|
||||||
|
apiBaseUrl: 'https://oauth.reddit.com',
|
||||||
|
iconUrl: '{BASE_URL}/apps/reddit/assets/favicon.svg',
|
||||||
|
authDocUrl: 'https://automatisch.io/docs/apps/reddit/connection',
|
||||||
|
primaryColor: 'FF4500',
|
||||||
|
supportsConnections: true,
|
||||||
|
beforeRequest: [addAuthHeader],
|
||||||
|
auth,
|
||||||
|
triggers,
|
||||||
|
actions,
|
||||||
|
});
|
3
packages/backend/src/apps/reddit/triggers/index.ts
Normal file
3
packages/backend/src/apps/reddit/triggers/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import newPostsMatchingSearch from './new-posts-matching-search';
|
||||||
|
|
||||||
|
export default [newPostsMatchingSearch];
|
@@ -0,0 +1,48 @@
|
|||||||
|
import defineTrigger from '../../../../helpers/define-trigger';
|
||||||
|
|
||||||
|
export default defineTrigger({
|
||||||
|
name: 'New posts matching search',
|
||||||
|
key: 'newPostsMatchingSearch',
|
||||||
|
pollInterval: 15,
|
||||||
|
description: 'Triggers when a search string matches a new post.',
|
||||||
|
arguments: [
|
||||||
|
{
|
||||||
|
label: 'Search Query',
|
||||||
|
key: 'searchQuery',
|
||||||
|
type: 'string' as const,
|
||||||
|
required: true,
|
||||||
|
description:
|
||||||
|
'The term or expression to look for, restricted to 512 characters. If your query contains periods (e.g., automatisch.io), ensure it is enclosed in quotes ("automatisch.io").',
|
||||||
|
variables: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
async run($) {
|
||||||
|
const { searchQuery } = $.step.parameters;
|
||||||
|
const params = {
|
||||||
|
q: searchQuery,
|
||||||
|
type: 'link',
|
||||||
|
sort: 'new',
|
||||||
|
limit: 100,
|
||||||
|
after: undefined as unknown as string,
|
||||||
|
};
|
||||||
|
|
||||||
|
do {
|
||||||
|
const { data } = await $.http.get('/search', {
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
params.after = data.data.after;
|
||||||
|
|
||||||
|
if (data.data.children?.length) {
|
||||||
|
for (const item of data.data.children) {
|
||||||
|
$.pushTriggerItem({
|
||||||
|
raw: item,
|
||||||
|
meta: {
|
||||||
|
internalId: item.data.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} while (params.after);
|
||||||
|
},
|
||||||
|
});
|
3
packages/backend/src/apps/removebg/actions/index.ts
Normal file
3
packages/backend/src/apps/removebg/actions/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import removeImageBackground from './remove-image-background';
|
||||||
|
|
||||||
|
export default [removeImageBackground];
|
@@ -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 });
|
||||||
|
}
|
||||||
|
});
|
@@ -1,6 +1,7 @@
|
|||||||
import defineApp from '../../helpers/define-app';
|
import defineApp from '../../helpers/define-app';
|
||||||
import addAuthHeader from './common/add-auth-header';
|
import addAuthHeader from './common/add-auth-header';
|
||||||
import auth from './auth';
|
import auth from './auth';
|
||||||
|
import actions from './actions';
|
||||||
|
|
||||||
export default defineApp({
|
export default defineApp({
|
||||||
name: 'Remove.bg',
|
name: 'Remove.bg',
|
||||||
@@ -13,4 +14,5 @@ export default defineApp({
|
|||||||
primaryColor: '55636c',
|
primaryColor: '55636c',
|
||||||
beforeRequest: [addAuthHeader],
|
beforeRequest: [addAuthHeader],
|
||||||
auth,
|
auth,
|
||||||
|
actions,
|
||||||
});
|
});
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
import newBankTransactions from './new-bank-transactions';
|
import newBankTransactions from './new-bank-transactions';
|
||||||
|
import newPayments from './new-payments';
|
||||||
|
|
||||||
export default [newBankTransactions];
|
export default [newBankTransactions, newPayments];
|
||||||
|
109
packages/backend/src/apps/xero/triggers/new-payments/index.ts
Normal file
109
packages/backend/src/apps/xero/triggers/new-payments/index.ts
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import defineTrigger from '../../../../helpers/define-trigger';
|
||||||
|
|
||||||
|
type Params = {
|
||||||
|
page: number;
|
||||||
|
order: string;
|
||||||
|
where?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default defineTrigger({
|
||||||
|
name: 'New payments',
|
||||||
|
key: 'newPayments',
|
||||||
|
pollInterval: 15,
|
||||||
|
description: 'Triggers when a new payment is received.',
|
||||||
|
arguments: [
|
||||||
|
{
|
||||||
|
label: 'Organization',
|
||||||
|
key: 'organizationId',
|
||||||
|
type: 'dropdown' as const,
|
||||||
|
required: true,
|
||||||
|
description: '',
|
||||||
|
variables: true,
|
||||||
|
source: {
|
||||||
|
type: 'query',
|
||||||
|
name: 'getDynamicData',
|
||||||
|
arguments: [
|
||||||
|
{
|
||||||
|
name: 'key',
|
||||||
|
value: 'listOrganizations',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Payment Type',
|
||||||
|
key: 'paymentType',
|
||||||
|
type: 'dropdown' as const,
|
||||||
|
required: false,
|
||||||
|
description: '',
|
||||||
|
variables: true,
|
||||||
|
value: '',
|
||||||
|
options: [
|
||||||
|
{ label: 'Accounts Receivable', value: 'ACCRECPAYMENT' },
|
||||||
|
{ label: 'Accounts Payable', value: 'ACCPAYPAYMENT' },
|
||||||
|
{
|
||||||
|
label: 'Accounts Receivable Credit (Refund)',
|
||||||
|
value: 'ARCREDITPAYMENT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Accounts Payable Credit (Refund)',
|
||||||
|
value: 'APCREDITPAYMENT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Accounts Receivable Overpayment (Refund)',
|
||||||
|
value: 'AROVERPAYMENTPAYMENT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Accounts Receivable Prepayment (Refund)',
|
||||||
|
value: 'ARPREPAYMENTPAYMENT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Accounts Payable Prepayment (Refund)',
|
||||||
|
value: 'APPREPAYMENTPAYMENT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Accounts Payable Overpayment (Refund)',
|
||||||
|
value: 'APOVERPAYMENTPAYMENT',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
async run($) {
|
||||||
|
const paymentType = $.step.parameters.paymentType;
|
||||||
|
|
||||||
|
const params: Params = {
|
||||||
|
page: 1,
|
||||||
|
order: 'Date DESC',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (paymentType) {
|
||||||
|
params.where = `PaymentType="${paymentType}"`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let nextPage = false;
|
||||||
|
do {
|
||||||
|
const { data } = await $.http.get('/api.xro/2.0/Payments', {
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
params.page = params.page + 1;
|
||||||
|
|
||||||
|
if (data.Payments?.length) {
|
||||||
|
for (const payment of data.Payments) {
|
||||||
|
$.pushTriggerItem({
|
||||||
|
raw: payment,
|
||||||
|
meta: {
|
||||||
|
internalId: payment.PaymentID,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.Payments?.length === 100) {
|
||||||
|
nextPage = true;
|
||||||
|
} else {
|
||||||
|
nextPage = false;
|
||||||
|
}
|
||||||
|
} while (nextPage);
|
||||||
|
},
|
||||||
|
});
|
102
packages/backend/src/apps/zendesk/actions/create-user/fields.ts
Normal file
102
packages/backend/src/apps/zendesk/actions/create-user/fields.ts
Normal 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' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
@@ -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 });
|
||||||
|
},
|
||||||
|
});
|
@@ -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 } });
|
||||||
|
},
|
||||||
|
});
|
@@ -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 });
|
||||||
|
},
|
||||||
|
});
|
@@ -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] });
|
||||||
|
},
|
||||||
|
});
|
@@ -1,3 +1,15 @@
|
|||||||
import createTicket from './create-ticket';
|
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,
|
||||||
|
];
|
||||||
|
@@ -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',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
@@ -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 });
|
||||||
|
},
|
||||||
|
});
|
@@ -1,13 +1,20 @@
|
|||||||
import listUsers from './list-users';
|
import listUsers from './list-users';
|
||||||
import listBrands from './list-brands';
|
import listBrands from './list-brands';
|
||||||
|
import listFirstPageOfTickets from './list-first-page-of-tickets';
|
||||||
import listGroups from './list-groups';
|
import listGroups from './list-groups';
|
||||||
|
import listOrganizations from './list-organizations';
|
||||||
import listSharingAgreements from './list-sharing-agreements';
|
import listSharingAgreements from './list-sharing-agreements';
|
||||||
import listTicketForms from './list-ticket-forms';
|
import listTicketForms from './list-ticket-forms';
|
||||||
|
import listViews from './list-views';
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
listUsers,
|
listUsers,
|
||||||
listBrands,
|
listBrands,
|
||||||
|
listFirstPageOfTickets,
|
||||||
listGroups,
|
listGroups,
|
||||||
|
listOrganizations,
|
||||||
listSharingAgreements,
|
listSharingAgreements,
|
||||||
|
listFirstPageOfTickets,
|
||||||
listTicketForms,
|
listTicketForms,
|
||||||
|
listViews,
|
||||||
];
|
];
|
||||||
|
@@ -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;
|
||||||
|
},
|
||||||
|
};
|
@@ -21,7 +21,7 @@ export default {
|
|||||||
const response = await $.http.get('/api/v2/groups', { params });
|
const response = await $.http.get('/api/v2/groups', { params });
|
||||||
const allGroups = response?.data?.groups;
|
const allGroups = response?.data?.groups;
|
||||||
hasMore = response?.data?.meta?.has_more;
|
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) {
|
if (allGroups?.length) {
|
||||||
for (const group of allGroups) {
|
for (const group of allGroups) {
|
||||||
|
@@ -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;
|
||||||
|
},
|
||||||
|
};
|
@@ -25,7 +25,7 @@ export default {
|
|||||||
const response = await $.http.get('/api/v2/users', { params });
|
const response = await $.http.get('/api/v2/users', { params });
|
||||||
const allUsers = response?.data?.users;
|
const allUsers = response?.data?.users;
|
||||||
hasMore = response?.data?.meta?.has_more;
|
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) {
|
if (allUsers?.length) {
|
||||||
for (const user of allUsers) {
|
for (const user of allUsers) {
|
||||||
|
@@ -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;
|
||||||
|
},
|
||||||
|
};
|
@@ -1,6 +1,7 @@
|
|||||||
import defineApp from '../../helpers/define-app';
|
import defineApp from '../../helpers/define-app';
|
||||||
import addAuthHeader from './common/add-auth-headers';
|
import addAuthHeader from './common/add-auth-headers';
|
||||||
import auth from './auth';
|
import auth from './auth';
|
||||||
|
import triggers from './triggers';
|
||||||
import actions from './actions';
|
import actions from './actions';
|
||||||
import dynamicData from './dynamic-data';
|
import dynamicData from './dynamic-data';
|
||||||
|
|
||||||
@@ -15,6 +16,7 @@ export default defineApp({
|
|||||||
supportsConnections: true,
|
supportsConnections: true,
|
||||||
beforeRequest: [addAuthHeader],
|
beforeRequest: [addAuthHeader],
|
||||||
auth,
|
auth,
|
||||||
|
triggers,
|
||||||
actions,
|
actions,
|
||||||
dynamicData,
|
dynamicData,
|
||||||
});
|
});
|
||||||
|
4
packages/backend/src/apps/zendesk/triggers/index.ts
Normal file
4
packages/backend/src/apps/zendesk/triggers/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import newTickets from './new-tickets';
|
||||||
|
import newUsers from './new-users';
|
||||||
|
|
||||||
|
export default [newTickets, newUsers];
|
@@ -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);
|
||||||
|
},
|
||||||
|
});
|
@@ -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}`);
|
||||||
|
},
|
||||||
|
});
|
@@ -0,0 +1,15 @@
|
|||||||
|
import { Knex } from 'knex';
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
return knex.schema.createTable('shared_connections', (table) => {
|
||||||
|
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'));
|
||||||
|
table.uuid('connection_id').notNullable().references('id').inTable('connections');
|
||||||
|
table.uuid('role_id').notNullable().references('id').inTable('roles');
|
||||||
|
|
||||||
|
table.timestamps(true, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
return knex.schema.dropTable('shared_connections');
|
||||||
|
}
|
@@ -19,6 +19,7 @@ import login from './mutations/login';
|
|||||||
import registerUser from './mutations/register-user.ee';
|
import registerUser from './mutations/register-user.ee';
|
||||||
import resetConnection from './mutations/reset-connection';
|
import resetConnection from './mutations/reset-connection';
|
||||||
import resetPassword from './mutations/reset-password.ee';
|
import resetPassword from './mutations/reset-password.ee';
|
||||||
|
import shareConnection from './mutations/share-connection.ee';
|
||||||
import updateAppAuthClient from './mutations/update-app-auth-client.ee';
|
import updateAppAuthClient from './mutations/update-app-auth-client.ee';
|
||||||
import updateAppConfig from './mutations/update-app-config.ee';
|
import updateAppConfig from './mutations/update-app-config.ee';
|
||||||
import updateConfig from './mutations/update-config.ee';
|
import updateConfig from './mutations/update-config.ee';
|
||||||
@@ -55,6 +56,7 @@ const mutationResolvers = {
|
|||||||
registerUser,
|
registerUser,
|
||||||
resetConnection,
|
resetConnection,
|
||||||
resetPassword,
|
resetPassword,
|
||||||
|
shareConnection,
|
||||||
updateAppAuthClient,
|
updateAppAuthClient,
|
||||||
updateAppConfig,
|
updateAppConfig,
|
||||||
updateConfig,
|
updateConfig,
|
||||||
|
@@ -28,11 +28,11 @@ const createFlow = async (
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (connectionId) {
|
if (connectionId) {
|
||||||
const hasConnection = await context.currentUser
|
const connection = await context.currentUser
|
||||||
.$relatedQuery('connections')
|
.relatedConnectionsQuery()
|
||||||
.findById(connectionId);
|
.findById(connectionId);
|
||||||
|
|
||||||
if (!hasConnection) {
|
if (!connection) {
|
||||||
throw new Error('The connection does not exist!');
|
throw new Error('The connection does not exist!');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import Context from '../../types/express/context';
|
import Context from '../../types/express/context';
|
||||||
|
import Connection from '../../models/connection';
|
||||||
|
|
||||||
type Params = {
|
type Params = {
|
||||||
input: {
|
input: {
|
||||||
@@ -11,10 +12,13 @@ const deleteConnection = async (
|
|||||||
params: Params,
|
params: Params,
|
||||||
context: Context
|
context: Context
|
||||||
) => {
|
) => {
|
||||||
context.currentUser.can('delete', 'Connection');
|
const conditions = context.currentUser.can('delete', 'Connection');
|
||||||
|
const userConnections = context.currentUser.$relatedQuery('connections');
|
||||||
|
const allConnections = Connection.query();
|
||||||
|
const baseQuery = conditions.isCreator ? userConnections : allConnections;
|
||||||
|
|
||||||
await context.currentUser
|
await baseQuery
|
||||||
.$relatedQuery('connections')
|
.clone()
|
||||||
.delete()
|
.delete()
|
||||||
.findOne({
|
.findOne({
|
||||||
id: params.input.id,
|
id: params.input.id,
|
||||||
|
@@ -4,6 +4,7 @@ import deleteUserQueue from '../../queues/delete-user.ee';
|
|||||||
import flowQueue from '../../queues/flow';
|
import flowQueue from '../../queues/flow';
|
||||||
import Flow from '../../models/flow';
|
import Flow from '../../models/flow';
|
||||||
import Execution from '../../models/execution';
|
import Execution from '../../models/execution';
|
||||||
|
import User from '../../models/user';
|
||||||
import ExecutionStep from '../../models/execution-step';
|
import ExecutionStep from '../../models/execution-step';
|
||||||
import appConfig from '../../config/app';
|
import appConfig from '../../config/app';
|
||||||
|
|
||||||
@@ -14,51 +15,87 @@ const deleteCurrentUser = async (
|
|||||||
) => {
|
) => {
|
||||||
const id = context.currentUser.id;
|
const id = context.currentUser.id;
|
||||||
|
|
||||||
const flows = await context.currentUser.$relatedQuery('flows').where({
|
try {
|
||||||
active: true,
|
await User.transaction(async (trx) => {
|
||||||
});
|
const flows = await context.currentUser
|
||||||
|
.$relatedQuery('flows', trx)
|
||||||
|
.where({
|
||||||
|
active: true,
|
||||||
|
});
|
||||||
|
|
||||||
const repeatableJobs = await flowQueue.getRepeatableJobs();
|
const { count } = await context.currentUser
|
||||||
|
.$relatedQuery('connections', trx)
|
||||||
|
.joinRelated('sharedConnections')
|
||||||
|
.joinRelated('steps')
|
||||||
|
.join('flows', function () {
|
||||||
|
this
|
||||||
|
.on(
|
||||||
|
'flows.id', '=', 'steps.flow_id'
|
||||||
|
)
|
||||||
|
.andOnVal(
|
||||||
|
'flows.user_id', '<>', id
|
||||||
|
)
|
||||||
|
.andOnVal(
|
||||||
|
'flows.active', '=', true
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.count()
|
||||||
|
.first();
|
||||||
|
|
||||||
for (const flow of flows) {
|
if (count) {
|
||||||
const job = repeatableJobs.find((job) => job.id === flow.id);
|
throw new Error('The shared connections must be removed first!');
|
||||||
|
}
|
||||||
|
|
||||||
if (job) {
|
const executionIds = (
|
||||||
await flowQueue.removeRepeatableByKey(job.key);
|
await context.currentUser
|
||||||
|
.$relatedQuery('executions', trx)
|
||||||
|
.select('executions.id')
|
||||||
|
).map((execution: Execution) => execution.id);
|
||||||
|
const flowIds = flows.map((flow) => flow.id);
|
||||||
|
|
||||||
|
await ExecutionStep.query(trx).delete().whereIn('execution_id', executionIds);
|
||||||
|
await context.currentUser.$relatedQuery('executions', trx).delete();
|
||||||
|
await context.currentUser.$relatedQuery('steps', trx).delete();
|
||||||
|
await Flow.query(trx).whereIn('id', flowIds).delete();
|
||||||
|
await context.currentUser.$relatedQuery('connections', trx).delete();
|
||||||
|
await context.currentUser.$relatedQuery('identities', trx).delete();
|
||||||
|
|
||||||
|
if (appConfig.isCloud) {
|
||||||
|
await context.currentUser.$relatedQuery('subscriptions', trx).delete();
|
||||||
|
await context.currentUser.$relatedQuery('usageData', trx).delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
await context.currentUser.$query(trx).delete();
|
||||||
|
|
||||||
|
const jobName = `Delete user - ${id}`;
|
||||||
|
const jobPayload = { id };
|
||||||
|
const millisecondsFor30Days = Duration.fromObject({ days: 30 }).toMillis();
|
||||||
|
const jobOptions = {
|
||||||
|
delay: millisecondsFor30Days,
|
||||||
|
};
|
||||||
|
|
||||||
|
// must be done as the last action as this cannot be reverted via the transaction!
|
||||||
|
const repeatableJobs = await flowQueue.getRepeatableJobs();
|
||||||
|
|
||||||
|
for (const flow of flows) {
|
||||||
|
const job = repeatableJobs.find((job) => job.id === flow.id);
|
||||||
|
|
||||||
|
if (job) {
|
||||||
|
await flowQueue.removeRepeatableByKey(job.key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await deleteUserQueue.add(jobName, jobPayload, jobOptions);
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof Error) {
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throw new Error('The user deletion has failed!');
|
||||||
}
|
}
|
||||||
|
|
||||||
const executionIds = (
|
|
||||||
await context.currentUser
|
|
||||||
.$relatedQuery('executions')
|
|
||||||
.select('executions.id')
|
|
||||||
).map((execution: Execution) => execution.id);
|
|
||||||
const flowIds = flows.map((flow) => flow.id);
|
|
||||||
|
|
||||||
await ExecutionStep.query().delete().whereIn('execution_id', executionIds);
|
|
||||||
await context.currentUser.$relatedQuery('executions').delete();
|
|
||||||
await context.currentUser.$relatedQuery('steps').delete();
|
|
||||||
await Flow.query().whereIn('id', flowIds).delete();
|
|
||||||
await context.currentUser.$relatedQuery('connections').delete();
|
|
||||||
await context.currentUser.$relatedQuery('identities').delete();
|
|
||||||
|
|
||||||
if (appConfig.isCloud) {
|
|
||||||
await context.currentUser.$relatedQuery('subscriptions').delete();
|
|
||||||
await context.currentUser.$relatedQuery('usageData').delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
await context.currentUser.$query().delete();
|
|
||||||
|
|
||||||
const jobName = `Delete user - ${id}`;
|
|
||||||
const jobPayload = { id };
|
|
||||||
const millisecondsFor30Days = Duration.fromObject({ days: 30 }).toMillis();
|
|
||||||
const jobOptions = {
|
|
||||||
delay: millisecondsFor30Days,
|
|
||||||
};
|
|
||||||
|
|
||||||
await deleteUserQueue.add(jobName, jobPayload, jobOptions);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default deleteCurrentUser;
|
export default deleteCurrentUser;
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import Context from '../../types/express/context';
|
import Context from '../../types/express/context';
|
||||||
|
import Connection from '../../models/connection';
|
||||||
|
|
||||||
type Params = {
|
type Params = {
|
||||||
input: {
|
input: {
|
||||||
@@ -11,10 +12,13 @@ const resetConnection = async (
|
|||||||
params: Params,
|
params: Params,
|
||||||
context: Context
|
context: Context
|
||||||
) => {
|
) => {
|
||||||
context.currentUser.can('create', 'Connection');
|
const conditions = context.currentUser.can('update', 'Connection');
|
||||||
|
const userConnections = context.currentUser.$relatedQuery('connections');
|
||||||
|
const allConnections = Connection.query();
|
||||||
|
const baseQuery = conditions.isCreator ? userConnections : allConnections;
|
||||||
|
|
||||||
let connection = await context.currentUser
|
let connection = await baseQuery
|
||||||
.$relatedQuery('connections')
|
.clone()
|
||||||
.findOne({
|
.findOne({
|
||||||
id: params.input.id,
|
id: params.input.id,
|
||||||
})
|
})
|
||||||
|
@@ -0,0 +1,55 @@
|
|||||||
|
import Context from '../../types/express/context';
|
||||||
|
import Connection from '../../models/connection';
|
||||||
|
import SharedConnection from '../../models/shared-connection';
|
||||||
|
|
||||||
|
type Params = {
|
||||||
|
input: {
|
||||||
|
id: string;
|
||||||
|
roleIds: string[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const shareConnection = async (
|
||||||
|
_parent: unknown,
|
||||||
|
params: Params,
|
||||||
|
context: Context
|
||||||
|
) => {
|
||||||
|
const conditions = context.currentUser.can('update', 'Connection');
|
||||||
|
|
||||||
|
if (conditions.isCreator) return;
|
||||||
|
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
roleIds,
|
||||||
|
} = params.input;
|
||||||
|
|
||||||
|
const connection = await Connection
|
||||||
|
.query()
|
||||||
|
.findById(id)
|
||||||
|
.throwIfNotFound();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const updatedConnection = await Connection.transaction(async (trx) => {
|
||||||
|
await connection.$relatedQuery('sharedConnections', trx).delete();
|
||||||
|
|
||||||
|
if (roleIds?.length) {
|
||||||
|
const sharedConnections = roleIds.map((roleId) => ({
|
||||||
|
roleId,
|
||||||
|
connectionId: connection.id,
|
||||||
|
}));
|
||||||
|
|
||||||
|
await SharedConnection.query().insert(sharedConnections);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await Connection
|
||||||
|
.query(trx)
|
||||||
|
.findById(id);
|
||||||
|
});
|
||||||
|
|
||||||
|
return updatedConnection;
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error('The connection sharing preferences could not be updated!');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default shareConnection;
|
@@ -1,6 +1,7 @@
|
|||||||
import { IJSONObject } from '@automatisch/types';
|
import { IJSONObject } from '@automatisch/types';
|
||||||
import Context from '../../types/express/context';
|
import Context from '../../types/express/context';
|
||||||
import AppAuthClient from '../../models/app-auth-client';
|
import AppAuthClient from '../../models/app-auth-client';
|
||||||
|
import Connection from '../../models/connection';
|
||||||
|
|
||||||
type Params = {
|
type Params = {
|
||||||
input: {
|
input: {
|
||||||
@@ -15,10 +16,13 @@ const updateConnection = async (
|
|||||||
params: Params,
|
params: Params,
|
||||||
context: Context
|
context: Context
|
||||||
) => {
|
) => {
|
||||||
context.currentUser.can('create', 'Connection');
|
const conditions = context.currentUser.can('update', 'Connection');
|
||||||
|
const userConnections = context.currentUser.$relatedQuery('connections');
|
||||||
|
const allConnections = Connection.query();
|
||||||
|
const baseQuery = conditions.isCreator ? userConnections : allConnections;
|
||||||
|
|
||||||
let connection = await context.currentUser
|
let connection = await baseQuery
|
||||||
.$relatedQuery('connections')
|
.clone()
|
||||||
.findOne({
|
.findOne({
|
||||||
id: params.input.id,
|
id: params.input.id,
|
||||||
})
|
})
|
||||||
|
@@ -45,10 +45,11 @@ const updateStep = async (
|
|||||||
|
|
||||||
canSeeAllConnections = !conditions.isCreator;
|
canSeeAllConnections = !conditions.isCreator;
|
||||||
} catch {
|
} catch {
|
||||||
// void
|
// The user does not have permission to read any connections!
|
||||||
|
throw new Error('The connection does not exist!');
|
||||||
}
|
}
|
||||||
|
|
||||||
const userConnections = context.currentUser.$relatedQuery('connections');
|
const userConnections = context.currentUser.relatedConnectionsQuery();
|
||||||
const allConnections = Connection.query();
|
const allConnections = Connection.query();
|
||||||
const baseConnectionsQuery = canSeeAllConnections ? allConnections : userConnections;
|
const baseConnectionsQuery = canSeeAllConnections ? allConnections : userConnections;
|
||||||
|
|
||||||
|
@@ -9,28 +9,55 @@ type Params = {
|
|||||||
const getApp = async (_parent: unknown, params: Params, context: Context) => {
|
const getApp = async (_parent: unknown, params: Params, context: Context) => {
|
||||||
const conditions = context.currentUser.can('read', 'Connection');
|
const conditions = context.currentUser.can('read', 'Connection');
|
||||||
|
|
||||||
const userConnections = context.currentUser.$relatedQuery('connections');
|
|
||||||
const allConnections = Connection.query();
|
|
||||||
const connectionBaseQuery = conditions.isCreator ? userConnections : allConnections;
|
|
||||||
|
|
||||||
const app = await App.findOneByKey(params.key);
|
const app = await App.findOneByKey(params.key);
|
||||||
|
|
||||||
if (context.currentUser) {
|
if (context.currentUser) {
|
||||||
const connections = await connectionBaseQuery
|
const userConnections = context.currentUser.relatedConnectionsQuery();
|
||||||
.clone()
|
const allConnections = Connection.query();
|
||||||
.select('connections.*')
|
const connectionBaseQuery = conditions.isCreator ? userConnections : allConnections;
|
||||||
|
|
||||||
|
const connections = await Connection.query()
|
||||||
|
.with('connections', connectionBaseQuery)
|
||||||
|
.with(
|
||||||
|
'connections_with_flow_count',
|
||||||
|
Connection.query()
|
||||||
|
.clearSelect()
|
||||||
|
.select('connections.id')
|
||||||
|
.leftJoinRelated('steps')
|
||||||
|
.leftJoin('flows', function () {
|
||||||
|
this
|
||||||
|
.on(
|
||||||
|
'flows.id',
|
||||||
|
'=',
|
||||||
|
'steps.flow_id',
|
||||||
|
)
|
||||||
|
|
||||||
|
if (conditions.isCreator) {
|
||||||
|
this.andOnVal(
|
||||||
|
'flows.user_id',
|
||||||
|
'=',
|
||||||
|
context.currentUser.id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.where({
|
||||||
|
'connections.key': params.key,
|
||||||
|
'connections.draft': false,
|
||||||
|
})
|
||||||
|
.countDistinct('steps.flow_id as flowCount')
|
||||||
|
.groupBy('connections.id')
|
||||||
|
)
|
||||||
|
.select(
|
||||||
|
'connections.*',
|
||||||
|
'connections_with_flow_count.flowCount as flowCount'
|
||||||
|
)
|
||||||
|
.from('connections')
|
||||||
.withGraphFetched({
|
.withGraphFetched({
|
||||||
appConfig: true,
|
appConfig: true,
|
||||||
appAuthClient: true
|
appAuthClient: true
|
||||||
})
|
})
|
||||||
.fullOuterJoinRelated('steps')
|
.joinRaw('join connections_with_flow_count on connections.id = connections_with_flow_count.id')
|
||||||
.where({
|
.orderBy('connections.created_at', 'desc');
|
||||||
'connections.key': params.key,
|
|
||||||
'connections.draft': false,
|
|
||||||
})
|
|
||||||
.countDistinct('steps.flow_id as flowCount')
|
|
||||||
.groupBy('connections.id')
|
|
||||||
.orderBy('created_at', 'desc');
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...app,
|
...app,
|
||||||
|
@@ -15,7 +15,7 @@ const getConnectedApps = async (
|
|||||||
) => {
|
) => {
|
||||||
const conditions = context.currentUser.can('read', 'Connection');
|
const conditions = context.currentUser.can('read', 'Connection');
|
||||||
|
|
||||||
const userConnections = context.currentUser.$relatedQuery('connections');
|
const userConnections = context.currentUser.relatedConnectionsQuery();
|
||||||
const allConnections = Connection.query();
|
const allConnections = Connection.query();
|
||||||
const connectionBaseQuery = conditions.isCreator ? userConnections : allConnections;
|
const connectionBaseQuery = conditions.isCreator ? userConnections : allConnections;
|
||||||
|
|
||||||
@@ -25,8 +25,9 @@ const getConnectedApps = async (
|
|||||||
|
|
||||||
let apps = await App.findAll(params.name);
|
let apps = await App.findAll(params.name);
|
||||||
|
|
||||||
const connections = await connectionBaseQuery
|
const connections = await Connection
|
||||||
.clone()
|
.query()
|
||||||
|
.with('connections', connectionBaseQuery)
|
||||||
.select('connections.key')
|
.select('connections.key')
|
||||||
.where({ draft: false })
|
.where({ draft: false })
|
||||||
.count('connections.id as count')
|
.count('connections.id as count')
|
||||||
|
@@ -0,0 +1,29 @@
|
|||||||
|
import Context from '../../types/express/context';
|
||||||
|
import Connection from '../../models/connection';
|
||||||
|
|
||||||
|
type Params = {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSharedConnectionRoleIds = async (
|
||||||
|
_parent: unknown,
|
||||||
|
params: Params,
|
||||||
|
context: Context
|
||||||
|
) => {
|
||||||
|
const conditions = context.currentUser.can('update', 'Connection');
|
||||||
|
|
||||||
|
if (conditions.isCreator) return;
|
||||||
|
|
||||||
|
const connection = await Connection
|
||||||
|
.query()
|
||||||
|
.findById(params.id)
|
||||||
|
.throwIfNotFound();
|
||||||
|
|
||||||
|
const sharedConnections = await connection.$relatedQuery('sharedConnections');
|
||||||
|
|
||||||
|
const roleIds = sharedConnections.map(({ roleId }) => roleId);
|
||||||
|
|
||||||
|
return roleIds;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getSharedConnectionRoleIds;
|
@@ -13,15 +13,15 @@ const testConnection = async (
|
|||||||
params: Params,
|
params: Params,
|
||||||
context: Context
|
context: Context
|
||||||
) => {
|
) => {
|
||||||
const conditions = context.currentUser.can('update', 'Connection');
|
const conditions = context.currentUser.can('read', 'Connection');
|
||||||
const userConnections = context.currentUser.$relatedQuery('connections');
|
const userConnections = context.currentUser.relatedConnectionsQuery();
|
||||||
const allConnections = Connection.query();
|
const allConnections = Connection.query();
|
||||||
const connectionBaseQuery = conditions.isCreator ? userConnections : allConnections;
|
const connectionBaseQuery = conditions.isCreator ? userConnections : allConnections;
|
||||||
|
|
||||||
let connection = await connectionBaseQuery
|
let connection = await connectionBaseQuery
|
||||||
.clone()
|
.clone()
|
||||||
.findOne({
|
.findOne({
|
||||||
id: params.id,
|
'connections.id': params.id,
|
||||||
})
|
})
|
||||||
.throwIfNotFound();
|
.throwIfNotFound();
|
||||||
|
|
||||||
|
@@ -24,6 +24,7 @@ import getRole from './queries/get-role.ee';
|
|||||||
import getRoles from './queries/get-roles.ee';
|
import getRoles from './queries/get-roles.ee';
|
||||||
import getSamlAuthProviderRoleMappings from './queries/get-saml-auth-provider-role-mappings.ee';
|
import getSamlAuthProviderRoleMappings from './queries/get-saml-auth-provider-role-mappings.ee';
|
||||||
import getSamlAuthProvider from './queries/get-saml-auth-provider.ee';
|
import getSamlAuthProvider from './queries/get-saml-auth-provider.ee';
|
||||||
|
import getSharedConnectionRoleIds from './queries/get-shared-connection-role-ids.ee';
|
||||||
import getStepWithTestExecutions from './queries/get-step-with-test-executions';
|
import getStepWithTestExecutions from './queries/get-step-with-test-executions';
|
||||||
import getSubscriptionStatus from './queries/get-subscription-status.ee';
|
import getSubscriptionStatus from './queries/get-subscription-status.ee';
|
||||||
import getTrialStatus from './queries/get-trial-status.ee';
|
import getTrialStatus from './queries/get-trial-status.ee';
|
||||||
@@ -60,6 +61,7 @@ const queryResolvers = {
|
|||||||
getRoles,
|
getRoles,
|
||||||
getSamlAuthProvider,
|
getSamlAuthProvider,
|
||||||
getSamlAuthProviderRoleMappings,
|
getSamlAuthProviderRoleMappings,
|
||||||
|
getSharedConnectionRoleIds,
|
||||||
getStepWithTestExecutions,
|
getStepWithTestExecutions,
|
||||||
getSubscriptionStatus,
|
getSubscriptionStatus,
|
||||||
getTrialStatus,
|
getTrialStatus,
|
||||||
|
@@ -53,6 +53,7 @@ type Query {
|
|||||||
getNotifications: [Notification]
|
getNotifications: [Notification]
|
||||||
getSamlAuthProvider: SamlAuthProvider
|
getSamlAuthProvider: SamlAuthProvider
|
||||||
getSamlAuthProviderRoleMappings(id: String!): [SamlAuthProvidersRoleMapping]
|
getSamlAuthProviderRoleMappings(id: String!): [SamlAuthProvidersRoleMapping]
|
||||||
|
getSharedConnectionRoleIds(id: String!): [String]
|
||||||
getSubscriptionStatus: GetSubscriptionStatus
|
getSubscriptionStatus: GetSubscriptionStatus
|
||||||
getTrialStatus: GetTrialStatus
|
getTrialStatus: GetTrialStatus
|
||||||
getUser(id: String!): User
|
getUser(id: String!): User
|
||||||
@@ -83,6 +84,7 @@ type Mutation {
|
|||||||
registerUser(input: RegisterUserInput): User
|
registerUser(input: RegisterUserInput): User
|
||||||
resetConnection(input: ResetConnectionInput): Connection
|
resetConnection(input: ResetConnectionInput): Connection
|
||||||
resetPassword(input: ResetPasswordInput): Boolean
|
resetPassword(input: ResetPasswordInput): Boolean
|
||||||
|
shareConnection(input: ShareConnectionInput): Connection
|
||||||
updateAppAuthClient(input: UpdateAppAuthClientInput): AppAuthClient
|
updateAppAuthClient(input: UpdateAppAuthClientInput): AppAuthClient
|
||||||
updateAppConfig(input: UpdateAppConfigInput): AppConfig
|
updateAppConfig(input: UpdateAppConfigInput): AppConfig
|
||||||
updateConfig(input: JSONObject): JSONObject
|
updateConfig(input: JSONObject): JSONObject
|
||||||
@@ -244,6 +246,7 @@ type AuthLink {
|
|||||||
type Connection {
|
type Connection {
|
||||||
id: String
|
id: String
|
||||||
key: String
|
key: String
|
||||||
|
shared: Boolean
|
||||||
reconnectable: Boolean
|
reconnectable: Boolean
|
||||||
appAuthClientId: String
|
appAuthClientId: String
|
||||||
formattedData: ConnectionData
|
formattedData: ConnectionData
|
||||||
@@ -810,6 +813,11 @@ input ExecutionFiltersInput {
|
|||||||
status: String
|
status: String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input ShareConnectionInput {
|
||||||
|
id: String!
|
||||||
|
roleIds: [String]
|
||||||
|
}
|
||||||
|
|
||||||
schema {
|
schema {
|
||||||
query: Query
|
query: Query
|
||||||
mutation: Mutation
|
mutation: Mutation
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { IHttpClientParams } from '@automatisch/types';
|
import { IHttpClientParams } from '@automatisch/types';
|
||||||
import { AxiosRequestConfig } from 'axios';
|
import { InternalAxiosRequestConfig } from 'axios';
|
||||||
import { URL } from 'node:url';
|
import { URL } from 'node:url';
|
||||||
export { AxiosInstance as IHttpClient } from 'axios';
|
export { AxiosInstance as IHttpClient } from 'axios';
|
||||||
|
|
||||||
@@ -7,8 +7,8 @@ import HttpError from '../../errors/http';
|
|||||||
import axios from '../axios-with-proxy';
|
import axios from '../axios-with-proxy';
|
||||||
|
|
||||||
const removeBaseUrlForAbsoluteUrls = (
|
const removeBaseUrlForAbsoluteUrls = (
|
||||||
requestConfig: AxiosRequestConfig
|
requestConfig: InternalAxiosRequestConfig
|
||||||
): AxiosRequestConfig => {
|
): InternalAxiosRequestConfig => {
|
||||||
try {
|
try {
|
||||||
const url = new URL(requestConfig.url);
|
const url = new URL(requestConfig.url);
|
||||||
requestConfig.baseURL = url.origin;
|
requestConfig.baseURL = url.origin;
|
||||||
@@ -30,12 +30,21 @@ export default function createHttpClient({
|
|||||||
});
|
});
|
||||||
|
|
||||||
instance.interceptors.request.use(
|
instance.interceptors.request.use(
|
||||||
(requestConfig: AxiosRequestConfig): AxiosRequestConfig => {
|
(requestConfig: InternalAxiosRequestConfig): InternalAxiosRequestConfig => {
|
||||||
const newRequestConfig = removeBaseUrlForAbsoluteUrls(requestConfig);
|
const newRequestConfig = removeBaseUrlForAbsoluteUrls(requestConfig);
|
||||||
|
|
||||||
return beforeRequest.reduce((newConfig, beforeRequestFunc) => {
|
const result = beforeRequest.reduce((newConfig, beforeRequestFunc) => {
|
||||||
return beforeRequestFunc($, newConfig);
|
return beforeRequestFunc($, newConfig);
|
||||||
}, newRequestConfig);
|
}, newRequestConfig);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* axios seems to want InternalAxiosRequestConfig returned not AxioRequestConfig
|
||||||
|
* anymore even though requests do require AxiosRequestConfig.
|
||||||
|
*
|
||||||
|
* Since both interfaces are very similar (InternalAxiosRequestConfig
|
||||||
|
* extends AxiosRequestConfig), we can utilize an assertion below
|
||||||
|
**/
|
||||||
|
return result as InternalAxiosRequestConfig;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@@ -1,18 +1,18 @@
|
|||||||
import { QueryContext, ModelOptions } from 'objection';
|
import { IJSONObject, IRequest } from '@automatisch/types';
|
||||||
import type { RelationMappings } from 'objection';
|
|
||||||
import { AES, enc } from 'crypto-js';
|
import { AES, enc } from 'crypto-js';
|
||||||
import { IRequest } from '@automatisch/types';
|
import type { RelationMappings } from 'objection';
|
||||||
import App from './app';
|
import { ModelOptions, QueryContext } from 'objection';
|
||||||
import AppConfig from './app-config';
|
|
||||||
import AppAuthClient from './app-auth-client';
|
|
||||||
import Base from './base';
|
|
||||||
import User from './user';
|
|
||||||
import Step from './step';
|
|
||||||
import ExtendedQueryBuilder from './query-builder';
|
|
||||||
import appConfig from '../config/app';
|
import appConfig from '../config/app';
|
||||||
import { IJSONObject } from '@automatisch/types';
|
|
||||||
import Telemetry from '../helpers/telemetry';
|
|
||||||
import globalVariable from '../helpers/global-variable';
|
import globalVariable from '../helpers/global-variable';
|
||||||
|
import Telemetry from '../helpers/telemetry';
|
||||||
|
import App from './app';
|
||||||
|
import AppAuthClient from './app-auth-client';
|
||||||
|
import AppConfig from './app-config';
|
||||||
|
import Base from './base';
|
||||||
|
import ExtendedQueryBuilder from './query-builder';
|
||||||
|
import SharedConnection from './shared-connection';
|
||||||
|
import Step from './step';
|
||||||
|
import User from './user';
|
||||||
|
|
||||||
class Connection extends Base {
|
class Connection extends Base {
|
||||||
id!: string;
|
id!: string;
|
||||||
@@ -24,6 +24,9 @@ class Connection extends Base {
|
|||||||
draft: boolean;
|
draft: boolean;
|
||||||
count?: number;
|
count?: number;
|
||||||
flowCount?: number;
|
flowCount?: number;
|
||||||
|
sharedConnections?: SharedConnection[];
|
||||||
|
// computed via `User.relevantConnectionsQuery`
|
||||||
|
shared?: boolean;
|
||||||
user?: User;
|
user?: User;
|
||||||
steps?: Step[];
|
steps?: Step[];
|
||||||
triggerSteps?: Step[];
|
triggerSteps?: Step[];
|
||||||
@@ -46,6 +49,7 @@ class Connection extends Base {
|
|||||||
appAuthClientId: { type: 'string', format: 'uuid' },
|
appAuthClientId: { type: 'string', format: 'uuid' },
|
||||||
verified: { type: 'boolean', default: false },
|
verified: { type: 'boolean', default: false },
|
||||||
draft: { type: 'boolean' },
|
draft: { type: 'boolean' },
|
||||||
|
shared: { type: 'boolean', readOnly: true, },
|
||||||
deletedAt: { type: 'string' },
|
deletedAt: { type: 'string' },
|
||||||
createdAt: { type: 'string' },
|
createdAt: { type: 'string' },
|
||||||
updatedAt: { type: 'string' },
|
updatedAt: { type: 'string' },
|
||||||
@@ -100,6 +104,14 @@ class Connection extends Base {
|
|||||||
to: 'app_auth_clients.id',
|
to: 'app_auth_clients.id',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
sharedConnections: {
|
||||||
|
relation: Base.HasManyRelation,
|
||||||
|
modelClass: SharedConnection,
|
||||||
|
join: {
|
||||||
|
from: 'connections.id',
|
||||||
|
to: 'shared_connections.connection_id',
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
get reconnectable() {
|
get reconnectable() {
|
||||||
|
45
packages/backend/src/models/shared-connection.ts
Normal file
45
packages/backend/src/models/shared-connection.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import Base from './base';
|
||||||
|
import Role from './role';
|
||||||
|
import User from './user';
|
||||||
|
|
||||||
|
class SharedConnection extends Base {
|
||||||
|
id!: string;
|
||||||
|
roleId!: string;
|
||||||
|
connectionId!: string;
|
||||||
|
|
||||||
|
static tableName = 'shared_connections';
|
||||||
|
|
||||||
|
static jsonSchema = {
|
||||||
|
type: 'object',
|
||||||
|
required: ['roleId', 'connectionId'],
|
||||||
|
|
||||||
|
properties: {
|
||||||
|
id: { type: 'string', format: 'uuid' },
|
||||||
|
roleId: { type: 'string', format: 'uuid' },
|
||||||
|
connectionId: { type: 'string', format: 'uuid' },
|
||||||
|
createdAt: { type: 'string' },
|
||||||
|
updatedAt: { type: 'string' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
static relationMappings = () => ({
|
||||||
|
roles: {
|
||||||
|
relation: Base.HasManyRelation,
|
||||||
|
modelClass: Role,
|
||||||
|
join: {
|
||||||
|
from: 'shared_connections.role_id',
|
||||||
|
to: 'roles.id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
users: {
|
||||||
|
relation: Base.HasManyRelation,
|
||||||
|
modelClass: User,
|
||||||
|
join: {
|
||||||
|
from: 'shared_connections.role_id',
|
||||||
|
to: 'users.role_id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SharedConnection;
|
@@ -1,7 +1,7 @@
|
|||||||
import bcrypt from 'bcrypt';
|
import bcrypt from 'bcrypt';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import crypto from 'node:crypto';
|
import crypto from 'node:crypto';
|
||||||
import { ModelOptions, QueryContext } from 'objection';
|
import { raw, ModelOptions, QueryContext } from 'objection';
|
||||||
|
|
||||||
import appConfig from '../config/app';
|
import appConfig from '../config/app';
|
||||||
import { hasValidLicense } from '../helpers/license.ee';
|
import { hasValidLicense } from '../helpers/license.ee';
|
||||||
@@ -28,6 +28,7 @@ class User extends Base {
|
|||||||
resetPasswordTokenSentAt: string;
|
resetPasswordTokenSentAt: string;
|
||||||
trialExpiryDate: string;
|
trialExpiryDate: string;
|
||||||
connections?: Connection[];
|
connections?: Connection[];
|
||||||
|
sharedConnections?: Connection[];
|
||||||
flows?: Flow[];
|
flows?: Flow[];
|
||||||
steps?: Step[];
|
steps?: Step[];
|
||||||
executions?: Execution[];
|
executions?: Execution[];
|
||||||
@@ -69,6 +70,18 @@ class User extends Base {
|
|||||||
to: 'connections.user_id',
|
to: 'connections.user_id',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
sharedConnections: {
|
||||||
|
relation: Base.ManyToManyRelation,
|
||||||
|
modelClass: Connection,
|
||||||
|
join: {
|
||||||
|
from: 'users.role_id',
|
||||||
|
through: {
|
||||||
|
from: 'shared_connections.role_id',
|
||||||
|
to: 'shared_connections.connection_id',
|
||||||
|
},
|
||||||
|
to: 'connections.id',
|
||||||
|
},
|
||||||
|
},
|
||||||
flows: {
|
flows: {
|
||||||
relation: Base.HasManyRelation,
|
relation: Base.HasManyRelation,
|
||||||
modelClass: Flow,
|
modelClass: Flow,
|
||||||
@@ -165,6 +178,40 @@ class User extends Base {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
relatedConnectionsQuery() {
|
||||||
|
return Connection
|
||||||
|
.query()
|
||||||
|
.select('connections.*', raw('shared_connections.role_id IS NOT NULL as shared'))
|
||||||
|
.leftJoin(
|
||||||
|
'shared_connections',
|
||||||
|
'connections.id',
|
||||||
|
'=',
|
||||||
|
'shared_connections.connection_id'
|
||||||
|
)
|
||||||
|
.join(
|
||||||
|
'users',
|
||||||
|
function () {
|
||||||
|
this
|
||||||
|
.on(
|
||||||
|
'users.id',
|
||||||
|
'=',
|
||||||
|
'connections.user_id',
|
||||||
|
)
|
||||||
|
.orOn(
|
||||||
|
'users.role_id',
|
||||||
|
'=',
|
||||||
|
'shared_connections.role_id'
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
'users.id',
|
||||||
|
'=',
|
||||||
|
this.id
|
||||||
|
)
|
||||||
|
.groupBy('connections.id', 'shared_connections.role_id');
|
||||||
|
}
|
||||||
|
|
||||||
login(password: string) {
|
login(password: string) {
|
||||||
return bcrypt.compare(password, this.password);
|
return bcrypt.compare(password, this.password);
|
||||||
}
|
}
|
||||||
|
@@ -289,11 +289,24 @@ export default defineConfig({
|
|||||||
{ text: 'Connection', link: '/apps/pushover/connection' },
|
{ text: 'Connection', link: '/apps/pushover/connection' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: 'Reddit',
|
||||||
|
collapsible: true,
|
||||||
|
collapsed: true,
|
||||||
|
items: [
|
||||||
|
{ text: 'Triggers', link: '/apps/reddit/triggers' },
|
||||||
|
{ text: 'Actions', link: '/apps/reddit/actions' },
|
||||||
|
{ text: 'Connection', link: '/apps/reddit/connection' },
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
text: 'Remove.bg',
|
text: 'Remove.bg',
|
||||||
collapsible: true,
|
collapsible: true,
|
||||||
collapsed: 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',
|
text: 'RSS',
|
||||||
@@ -453,6 +466,15 @@ export default defineConfig({
|
|||||||
{ text: 'Connection', link: '/apps/wordpress/connection' },
|
{ text: 'Connection', link: '/apps/wordpress/connection' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: 'Xero',
|
||||||
|
collapsible: true,
|
||||||
|
collapsed: true,
|
||||||
|
items: [
|
||||||
|
{ text: 'Triggers', link: '/apps/xero/triggers' },
|
||||||
|
{ text: 'Connection', link: '/apps/xero/connection' },
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
text: 'Youtube',
|
text: 'Youtube',
|
||||||
collapsible: true,
|
collapsible: true,
|
||||||
|
@@ -3,6 +3,8 @@ favicon: /favicons/discord.svg
|
|||||||
items:
|
items:
|
||||||
- name: Send a message to channel
|
- name: Send a message to channel
|
||||||
desc: Sends a message to a specific channel you specify.
|
desc: Sends a message to a specific channel you specify.
|
||||||
|
- name: Create a scheduled event
|
||||||
|
desc: Creates a scheduled event.
|
||||||
---
|
---
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
@@ -3,6 +3,8 @@ favicon: /favicons/notion.svg
|
|||||||
items:
|
items:
|
||||||
- name: New database items
|
- name: New database items
|
||||||
desc: Triggers when a new database item is created.
|
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>
|
<script setup>
|
||||||
|
12
packages/docs/pages/apps/reddit/actions.md
Normal file
12
packages/docs/pages/apps/reddit/actions.md
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
favicon: /favicons/reddit.svg
|
||||||
|
items:
|
||||||
|
- name: Create link post
|
||||||
|
desc: Create a new link post within a subreddit.
|
||||||
|
---
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import CustomListing from '../../components/CustomListing.vue'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<CustomListing />
|
15
packages/docs/pages/apps/reddit/connection.md
Normal file
15
packages/docs/pages/apps/reddit/connection.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# Reddit
|
||||||
|
|
||||||
|
:::info
|
||||||
|
This page explains the steps you need to follow to set up the Reddit
|
||||||
|
connection in Automatisch. If any of the steps are outdated, please let us know!
|
||||||
|
:::
|
||||||
|
|
||||||
|
1. Go to [Reddit apps page](https://www.reddit.com/prefs/apps).
|
||||||
|
2. Click on the **"are you a developer? create an app..."** button in order to create an app.
|
||||||
|
3. Fill the **Name** field and choose **web app**.
|
||||||
|
4. Copy **OAuth Redirect URL** from Automatisch to **redirect uri** field.
|
||||||
|
5. Click on the **create app** button.
|
||||||
|
6. Copy the client id below **web app** text to the `Client ID` field on Automatisch.
|
||||||
|
7. Copy the **secret** value to the `Client Secret` field on Automatisch.
|
||||||
|
8. Start using Reddit integration with Automatisch!
|
12
packages/docs/pages/apps/reddit/triggers.md
Normal file
12
packages/docs/pages/apps/reddit/triggers.md
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
favicon: /favicons/reddit.svg
|
||||||
|
items:
|
||||||
|
- name: New posts matching search
|
||||||
|
desc: Triggers when a search string matches a new post.
|
||||||
|
---
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import CustomListing from '../../components/CustomListing.vue'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<CustomListing />
|
12
packages/docs/pages/apps/removebg/actions.md
Normal file
12
packages/docs/pages/apps/removebg/actions.md
Normal 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 />
|
@@ -5,6 +5,8 @@ items:
|
|||||||
desc: Creates an attachment of a specified object by given parent ID.
|
desc: Creates an attachment of a specified object by given parent ID.
|
||||||
- name: Find record
|
- name: Find record
|
||||||
desc: Finds a record of a specified object by a field and value.
|
desc: Finds a record of a specified object by a field and value.
|
||||||
|
- name: Execute query
|
||||||
|
desc: Executes a SOQL query in Salesforce.
|
||||||
---
|
---
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
@@ -3,6 +3,8 @@ favicon: /favicons/xero.svg
|
|||||||
items:
|
items:
|
||||||
- name: New bank transactions
|
- name: New bank transactions
|
||||||
desc: Triggers when a new bank transaction occurs.
|
desc: Triggers when a new bank transaction occurs.
|
||||||
|
- name: New payments
|
||||||
|
desc: Triggers when a new payment is received.
|
||||||
---
|
---
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
@@ -3,6 +3,16 @@ favicon: /favicons/zendesk.svg
|
|||||||
items:
|
items:
|
||||||
- name: Create ticket
|
- name: Create ticket
|
||||||
desc: Creates a new 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>
|
<script setup>
|
||||||
|
14
packages/docs/pages/apps/zendesk/triggers.md
Normal file
14
packages/docs/pages/apps/zendesk/triggers.md
Normal 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 />
|
@@ -30,7 +30,8 @@ The following integrations are currently supported by Automatisch.
|
|||||||
- [Placetel](/apps/placetel/triggers)
|
- [Placetel](/apps/placetel/triggers)
|
||||||
- [PostgreSQL](/apps/postgresql/actions)
|
- [PostgreSQL](/apps/postgresql/actions)
|
||||||
- [Pushover](/apps/pushover/actions)
|
- [Pushover](/apps/pushover/actions)
|
||||||
- [Remove.bg](/apps/removebg/connection)
|
- [Reddit](/apps/reddit/triggers)
|
||||||
|
- [Remove.bg](/apps/removebg/actions)
|
||||||
- [RSS](/apps/rss/triggers)
|
- [RSS](/apps/rss/triggers)
|
||||||
- [Salesforce](/apps/salesforce/triggers)
|
- [Salesforce](/apps/salesforce/triggers)
|
||||||
- [Scheduler](/apps/scheduler/triggers)
|
- [Scheduler](/apps/scheduler/triggers)
|
||||||
@@ -48,5 +49,6 @@ The following integrations are currently supported by Automatisch.
|
|||||||
- [Typeform](/apps/typeform/triggers)
|
- [Typeform](/apps/typeform/triggers)
|
||||||
- [Webhooks](/apps/webhooks/triggers)
|
- [Webhooks](/apps/webhooks/triggers)
|
||||||
- [WordPress](/apps/wordpress/triggers)
|
- [WordPress](/apps/wordpress/triggers)
|
||||||
|
- [Xero](/apps/xero/triggers)
|
||||||
- [Youtube](/apps/youtube/triggers)
|
- [Youtube](/apps/youtube/triggers)
|
||||||
- [Zendesk](/apps/zendesk/actions)
|
- [Zendesk](/apps/zendesk/actions)
|
||||||
|
1
packages/docs/pages/public/favicons/reddit.svg
Normal file
1
packages/docs/pages/public/favicons/reddit.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" class="_1O4jTk-dZ-VIxsCuYB6OR8 " width="40" height="48" ><g><circle fill="#FF4500" cx="10" cy="10" r="10"></circle><path fill="#FFFFFF" d="M16.67,10A1.46,1.46,0,0,0,14.2,9a7.12,7.12,0,0,0-3.85-1.23L11,4.65,13.14,5.1a1,1,0,1,0,.13-0.61L10.82,4a0.31,0.31,0,0,0-.37.24L9.71,7.71a7.14,7.14,0,0,0-3.9,1.23A1.46,1.46,0,1,0,4.2,11.33a2.87,2.87,0,0,0,0,.44c0,2.24,2.61,4.06,5.83,4.06s5.83-1.82,5.83-4.06a2.87,2.87,0,0,0,0-.44A1.46,1.46,0,0,0,16.67,10Zm-10,1a1,1,0,1,1,1,1A1,1,0,0,1,6.67,11Zm5.81,2.75a3.84,3.84,0,0,1-2.47.77,3.84,3.84,0,0,1-2.47-.77,0.27,0.27,0,0,1,.38-0.38A3.27,3.27,0,0,0,10,14a3.28,3.28,0,0,0,2.09-.61A0.27,0.27,0,1,1,12.48,13.79Zm-0.18-1.71a1,1,0,1,1,1-1A1,1,0,0,1,12.29,12.08Z"></path></g></svg>
|
After Width: | Height: | Size: 813 B |
@@ -2,12 +2,12 @@ const { AuthenticatedPage } = require('../authenticated-page');
|
|||||||
const { RoleConditionsModal } = require('./role-conditions-modal');
|
const { RoleConditionsModal } = require('./role-conditions-modal');
|
||||||
|
|
||||||
export class AdminCreateRolePage extends AuthenticatedPage {
|
export class AdminCreateRolePage extends AuthenticatedPage {
|
||||||
screenshotPath = '/admin/create-role'
|
screenshotPath = '/admin/create-role';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import('@playwright/test').Page} page
|
* @param {import('@playwright/test').Page} page
|
||||||
*/
|
*/
|
||||||
constructor (page) {
|
constructor(page) {
|
||||||
super(page);
|
super(page);
|
||||||
this.nameInput = page.getByTestId('name-input');
|
this.nameInput = page.getByTestId('name-input');
|
||||||
this.descriptionInput = page.getByTestId('description-input');
|
this.descriptionInput = page.getByTestId('description-input');
|
||||||
@@ -15,27 +15,28 @@ export class AdminCreateRolePage extends AuthenticatedPage {
|
|||||||
this.connectionRow = page.getByTestId('Connection-permission-row');
|
this.connectionRow = page.getByTestId('Connection-permission-row');
|
||||||
this.executionRow = page.getByTestId('Execution-permission-row');
|
this.executionRow = page.getByTestId('Execution-permission-row');
|
||||||
this.flowRow = page.getByTestId('Flow-permission-row');
|
this.flowRow = page.getByTestId('Flow-permission-row');
|
||||||
|
this.pageTitle = page.getByTestId('create-role-title');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {('Connection'|'Execution'|'Flow')} subject
|
* @param {('Connection'|'Execution'|'Flow')} subject
|
||||||
*/
|
*/
|
||||||
getRoleConditionsModal (subject) {
|
getRoleConditionsModal(subject) {
|
||||||
return new RoleConditionsModal(this.page, subject);
|
return new RoleConditionsModal(this.page, subject);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPermissionConfigs () {
|
async getPermissionConfigs() {
|
||||||
const subjects = ['Connection', 'Flow', 'Execution'];
|
const subjects = ['Connection', 'Flow', 'Execution'];
|
||||||
const permissionConfigs = [];
|
const permissionConfigs = [];
|
||||||
for (let subject of subjects) {
|
for (let subject of subjects) {
|
||||||
const row = this.getSubjectRow(subject);
|
const row = this.getSubjectRow(subject);
|
||||||
const actionInputs = await this.getRowInputs(row);
|
const actionInputs = await this.getRowInputs(row);
|
||||||
Object.keys(actionInputs).forEach(action => {
|
Object.keys(actionInputs).forEach((action) => {
|
||||||
permissionConfigs.push({
|
permissionConfigs.push({
|
||||||
action,
|
action,
|
||||||
locator: actionInputs[action],
|
locator: actionInputs[action],
|
||||||
subject,
|
subject,
|
||||||
row
|
row,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -43,50 +44,50 @@ export class AdminCreateRolePage extends AuthenticatedPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {(
|
* @param {(
|
||||||
* 'Connection' | 'Flow' | 'Execution'
|
* 'Connection' | 'Flow' | 'Execution'
|
||||||
* )} subject
|
* )} subject
|
||||||
*/
|
*/
|
||||||
getSubjectRow (subject) {
|
getSubjectRow(subject) {
|
||||||
const k = `${subject.toLowerCase()}Row`
|
const k = `${subject.toLowerCase()}Row`;
|
||||||
if (this[k]) {
|
if (this[k]) {
|
||||||
return this[k]
|
return this[k];
|
||||||
} else {
|
} else {
|
||||||
throw 'Unknown row'
|
throw 'Unknown row';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import('@playwright/test').Locator} row
|
* @param {import('@playwright/test').Locator} row
|
||||||
*/
|
*/
|
||||||
async getRowInputs (row) {
|
async getRowInputs(row) {
|
||||||
const inputs = {
|
const inputs = {
|
||||||
// settingsButton: row.getByTestId('permission-settings-button')
|
// settingsButton: row.getByTestId('permission-settings-button')
|
||||||
}
|
};
|
||||||
for (let input of ['create', 'read', 'update', 'delete', 'publish']) {
|
for (let input of ['create', 'read', 'update', 'delete', 'publish']) {
|
||||||
const testId = `${input}-checkbox`
|
const testId = `${input}-checkbox`;
|
||||||
if (await row.getByTestId(testId).count() > 0) {
|
if ((await row.getByTestId(testId).count()) > 0) {
|
||||||
inputs[input] = row.getByTestId(testId).locator('input');
|
inputs[input] = row.getByTestId(testId).locator('input');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return inputs
|
return inputs;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import('@playwright/test').Locator} row
|
* @param {import('@playwright/test').Locator} row
|
||||||
*/
|
*/
|
||||||
async clickPermissionSettings (row) {
|
async clickPermissionSettings(row) {
|
||||||
await row.getByTestId('permission-settings-button').click();
|
await row.getByTestId('permission-settings-button').click();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {string} subject
|
* @param {string} subject
|
||||||
* @param {'create'|'read'|'update'|'delete'|'publish'} action
|
* @param {'create'|'read'|'update'|'delete'|'publish'} action
|
||||||
* @param {boolean} val
|
* @param {boolean} val
|
||||||
*/
|
*/
|
||||||
async updateAction (subject, action, val) {
|
async updateAction(subject, action, val) {
|
||||||
const row = await this.getSubjectRow(subject);
|
const row = await this.getSubjectRow(subject);
|
||||||
const inputs = await this.getRowInputs(row);
|
const inputs = await this.getRowInputs(row);
|
||||||
if (inputs[action]) {
|
if (inputs[action]) {
|
||||||
@@ -100,7 +101,7 @@ export class AdminCreateRolePage extends AuthenticatedPage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`${subject} does not have action ${action}`)
|
throw new Error(`${subject} does not have action ${action}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -7,24 +7,25 @@ export class AdminCreateUserPage extends AuthenticatedPage {
|
|||||||
/**
|
/**
|
||||||
* @param {import('@playwright/test').Page} page
|
* @param {import('@playwright/test').Page} page
|
||||||
*/
|
*/
|
||||||
constructor (page) {
|
constructor(page) {
|
||||||
super(page);
|
super(page);
|
||||||
this.fullNameInput = page.getByTestId('full-name-input');
|
this.fullNameInput = page.getByTestId('full-name-input');
|
||||||
this.emailInput = page.getByTestId('email-input');
|
this.emailInput = page.getByTestId('email-input');
|
||||||
this.passwordInput = page.getByTestId('password-input');
|
this.passwordInput = page.getByTestId('password-input');
|
||||||
this.roleInput = page.getByTestId('role.id-autocomplete');
|
this.roleInput = page.getByTestId('role.id-autocomplete');
|
||||||
this.createButton = page.getByTestId('create-button');
|
this.createButton = page.getByTestId('create-button');
|
||||||
|
this.pageTitle = page.getByTestId('create-user-title');
|
||||||
}
|
}
|
||||||
|
|
||||||
seed (seed) {
|
seed(seed) {
|
||||||
faker.seed(seed || 0);
|
faker.seed(seed || 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
generateUser () {
|
generateUser() {
|
||||||
return {
|
return {
|
||||||
fullName: faker.person.fullName(),
|
fullName: faker.person.fullName(),
|
||||||
email: faker.internet.email().toLowerCase(),
|
email: faker.internet.email().toLowerCase(),
|
||||||
password: faker.internet.password()
|
password: faker.internet.password(),
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -5,5 +5,6 @@ export class AdminEditRolePage extends AdminCreateRolePage {
|
|||||||
super(page);
|
super(page);
|
||||||
delete this.createButton;
|
delete this.createButton;
|
||||||
this.updateButton = page.getByTestId('update-button');
|
this.updateButton = page.getByTestId('update-button');
|
||||||
|
this.pageTitle = page.getByTestId('edit-role-title');
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -15,6 +15,7 @@ export class AdminEditUserPage extends AuthenticatedPage {
|
|||||||
this.emailInput = page.getByTestId('email-input');
|
this.emailInput = page.getByTestId('email-input');
|
||||||
this.roleInput = page.getByTestId('role.id-autocomplete');
|
this.roleInput = page.getByTestId('role.id-autocomplete');
|
||||||
this.updateButton = page.getByTestId('update-button');
|
this.updateButton = page.getByTestId('update-button');
|
||||||
|
this.pageTitle = page.getByTestId('edit-user-title');
|
||||||
}
|
}
|
||||||
|
|
||||||
generateUser () {
|
generateUser () {
|
||||||
|
@@ -14,6 +14,7 @@ export class AdminRolesPage extends AuthenticatedPage {
|
|||||||
this.deleteRoleModal = new DeleteRoleModal(page);
|
this.deleteRoleModal = new DeleteRoleModal(page);
|
||||||
this.roleRow = page.getByTestId('role-row');
|
this.roleRow = page.getByTestId('role-row');
|
||||||
this.rolesLoader = page.getByTestId('roles-list-loader');
|
this.rolesLoader = page.getByTestId('roles-list-loader');
|
||||||
|
this.pageTitle = page.getByTestId('roles-page-title');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -28,8 +29,9 @@ export class AdminRolesPage extends AuthenticatedPage {
|
|||||||
await this.drawerMenuButton.click();
|
await this.drawerMenuButton.click();
|
||||||
}
|
}
|
||||||
await this.roleDrawerLink.click();
|
await this.roleDrawerLink.click();
|
||||||
|
await this.isMounted();
|
||||||
await this.rolesLoader.waitFor({
|
await this.rolesLoader.waitFor({
|
||||||
state: 'detached',
|
state: 'detached'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -10,7 +10,7 @@ export class AdminUsersPage extends AuthenticatedPage {
|
|||||||
/**
|
/**
|
||||||
* @param {import('@playwright/test').Page} page
|
* @param {import('@playwright/test').Page} page
|
||||||
*/
|
*/
|
||||||
constructor (page) {
|
constructor(page) {
|
||||||
super(page);
|
super(page);
|
||||||
this.createUserButton = page.getByTestId('create-user');
|
this.createUserButton = page.getByTestId('create-user');
|
||||||
this.userRow = page.getByTestId('user-row');
|
this.userRow = page.getByTestId('user-row');
|
||||||
@@ -20,14 +20,16 @@ export class AdminUsersPage extends AuthenticatedPage {
|
|||||||
this.nextPageButton = page.getByTestId('next-page-button');
|
this.nextPageButton = page.getByTestId('next-page-button');
|
||||||
this.lastPageButton = page.getByTestId('last-page-button');
|
this.lastPageButton = page.getByTestId('last-page-button');
|
||||||
this.usersLoader = page.getByTestId('users-list-loader');
|
this.usersLoader = page.getByTestId('users-list-loader');
|
||||||
|
this.pageTitle = page.getByTestId('users-page-title');
|
||||||
}
|
}
|
||||||
|
|
||||||
async navigateTo () {
|
async navigateTo() {
|
||||||
await this.profileMenuButton.click();
|
await this.profileMenuButton.click();
|
||||||
await this.adminMenuItem.click();
|
await this.adminMenuItem.click();
|
||||||
|
await this.isMounted();
|
||||||
if (await this.usersLoader.isVisible()) {
|
if (await this.usersLoader.isVisible()) {
|
||||||
await this.usersLoader.waitFor({
|
await this.usersLoader.waitFor({
|
||||||
state: 'detached'
|
state: 'detached',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -35,48 +37,48 @@ export class AdminUsersPage extends AuthenticatedPage {
|
|||||||
/**
|
/**
|
||||||
* @param {string} email
|
* @param {string} email
|
||||||
*/
|
*/
|
||||||
async getUserRowByEmail (email) {
|
async getUserRowByEmail(email) {
|
||||||
return this.userRow.filter({
|
return this.userRow.filter({
|
||||||
has: this.page.getByTestId('user-email').filter({
|
has: this.page.getByTestId('user-email').filter({
|
||||||
hasText: email
|
hasText: email,
|
||||||
})
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import('@playwright/test').Locator} row
|
* @param {import('@playwright/test').Locator} row
|
||||||
*/
|
*/
|
||||||
async getRowData (row) {
|
async getRowData(row) {
|
||||||
return {
|
return {
|
||||||
fullName: await row.getByTestId('user-full-name').textContent(),
|
fullName: await row.getByTestId('user-full-name').textContent(),
|
||||||
email: await row.getByTestId('user-email').textContent(),
|
email: await row.getByTestId('user-email').textContent(),
|
||||||
role: await row.getByTestId('user-role').textContent()
|
role: await row.getByTestId('user-role').textContent(),
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import('@playwright/test').Locator} row
|
* @param {import('@playwright/test').Locator} row
|
||||||
*/
|
*/
|
||||||
async clickEditUser (row) {
|
async clickEditUser(row) {
|
||||||
await row.getByTestId('user-edit').click();
|
await row.getByTestId('user-edit').click();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import('@playwright/test').Locator} row
|
* @param {import('@playwright/test').Locator} row
|
||||||
*/
|
*/
|
||||||
async clickDeleteUser (row) {
|
async clickDeleteUser(row) {
|
||||||
await row.getByTestId('delete-button').click();
|
await row.getByTestId('delete-button').click();
|
||||||
return this.deleteUserModal;
|
return this.deleteUserModal;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} email
|
* @param {string} email
|
||||||
* @returns {import('@playwright/test').Locator | null}
|
* @returns {import('@playwright/test').Locator | null}
|
||||||
*/
|
*/
|
||||||
async findUserPageWithEmail (email) {
|
async findUserPageWithEmail(email) {
|
||||||
if (await this.usersLoader.isVisible()) {
|
if (await this.usersLoader.isVisible()) {
|
||||||
await this.usersLoader.waitFor({
|
await this.usersLoader.waitFor({
|
||||||
state: 'detached'
|
state: 'detached',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// start at the first page
|
// start at the first page
|
||||||
@@ -88,10 +90,11 @@ export class AdminUsersPage extends AuthenticatedPage {
|
|||||||
while (true) {
|
while (true) {
|
||||||
if (await this.usersLoader.isVisible()) {
|
if (await this.usersLoader.isVisible()) {
|
||||||
await this.usersLoader.waitFor({
|
await this.usersLoader.waitFor({
|
||||||
state: 'detached'
|
state: 'detached',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const rowLocator = await this.getUserRowByEmail(email);
|
const rowLocator = await this.getUserRowByEmail(email);
|
||||||
|
console.log('rowLocator.count', email, await rowLocator.count());
|
||||||
if ((await rowLocator.count()) === 1) {
|
if ((await rowLocator.count()) === 1) {
|
||||||
return rowLocator;
|
return rowLocator;
|
||||||
}
|
}
|
||||||
@@ -103,7 +106,7 @@ export class AdminUsersPage extends AuthenticatedPage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getTotalRows () {
|
async getTotalRows() {
|
||||||
return await this.page.evaluate(() => {
|
return await this.page.evaluate(() => {
|
||||||
const node = document.querySelector('[data-total-count]');
|
const node = document.querySelector('[data-total-count]');
|
||||||
if (node) {
|
if (node) {
|
||||||
@@ -116,7 +119,7 @@ export class AdminUsersPage extends AuthenticatedPage {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getRowsPerPage () {
|
async getRowsPerPage() {
|
||||||
return await this.page.evaluate(() => {
|
return await this.page.evaluate(() => {
|
||||||
const node = document.querySelector('[data-rows-per-page]');
|
const node = document.querySelector('[data-rows-per-page]');
|
||||||
if (node) {
|
if (node) {
|
||||||
@@ -128,4 +131,4 @@ export class AdminUsersPage extends AuthenticatedPage {
|
|||||||
return 0;
|
return 0;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -12,7 +12,9 @@ export class AuthenticatedPage extends BasePage {
|
|||||||
|
|
||||||
this.profileMenuButton = this.page.getByTestId('profile-menu-button');
|
this.profileMenuButton = this.page.getByTestId('profile-menu-button');
|
||||||
this.adminMenuItem = this.page.getByRole('menuitem', { name: 'Admin' });
|
this.adminMenuItem = this.page.getByRole('menuitem', { name: 'Admin' });
|
||||||
this.userInterfaceDrawerItem = this.page.getByTestId('user-interface-drawer-link');
|
this.userInterfaceDrawerItem = this.page.getByTestId(
|
||||||
|
'user-interface-drawer-link'
|
||||||
|
);
|
||||||
this.appBar = this.page.getByTestId('app-bar');
|
this.appBar = this.page.getByTestId('app-bar');
|
||||||
this.drawerMenuButton = this.page.getByTestId('drawer-menu-button');
|
this.drawerMenuButton = this.page.getByTestId('drawer-menu-button');
|
||||||
this.goToDashboardButton = this.page.getByTestId('go-back-drawer-link');
|
this.goToDashboardButton = this.page.getByTestId('go-back-drawer-link');
|
||||||
|
@@ -15,45 +15,46 @@ export class BasePage {
|
|||||||
constructor(page) {
|
constructor(page) {
|
||||||
this.page = page;
|
this.page = page;
|
||||||
this.snackbar = page.locator('*[data-test^="snackbar"]');
|
this.snackbar = page.locator('*[data-test^="snackbar"]');
|
||||||
|
this.pageTitle = this.page.getByTestId('page-title');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finds the latest snackbar message and extracts relevant data
|
* Finds the latest snackbar message and extracts relevant data
|
||||||
* @param {string | undefined} testId
|
* @param {string | undefined} testId
|
||||||
* @returns {(
|
* @returns {(
|
||||||
* null | {
|
* null | {
|
||||||
* variant: SnackbarVariant,
|
* variant: SnackbarVariant,
|
||||||
* text: string,
|
* text: string,
|
||||||
* dataset: { [key: string]: string }
|
* dataset: { [key: string]: string }
|
||||||
* }
|
* }
|
||||||
* )}
|
* )}
|
||||||
*/
|
*/
|
||||||
async getSnackbarData (testId) {
|
async getSnackbarData(testId) {
|
||||||
if (!testId) {
|
if (!testId) {
|
||||||
testId = 'snackbar';
|
testId = 'snackbar';
|
||||||
}
|
}
|
||||||
const snack = this.page.getByTestId(testId);
|
const snack = this.page.getByTestId(testId);
|
||||||
return {
|
return {
|
||||||
variant: await snack.getAttribute('data-snackbar-variant'),
|
variant: await snack.getAttribute('data-snackbar-variant'),
|
||||||
text: await snack.evaluate(node => node.innerText),
|
text: await snack.evaluate((node) => node.innerText),
|
||||||
dataset: await snack.evaluate(node => {
|
dataset: await snack.evaluate((node) => {
|
||||||
function getChildren (n) {
|
function getChildren(n) {
|
||||||
return [n].concat(
|
return [n].concat(
|
||||||
...Array.from(n.children).map(c => getChildren(c))
|
...Array.from(n.children).map((c) => getChildren(c))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const datasets = getChildren(node).map(
|
const datasets = getChildren(node).map((n) =>
|
||||||
n => Object.assign({}, n.dataset)
|
Object.assign({}, n.dataset)
|
||||||
);
|
);
|
||||||
return Object.assign({}, ...datasets);
|
return Object.assign({}, ...datasets);
|
||||||
})
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Closes all snackbars, should be replaced later
|
* Closes all snackbars, should be replaced later
|
||||||
*/
|
*/
|
||||||
async closeSnackbar () {
|
async closeSnackbar() {
|
||||||
const snackbars = await this.snackbar.all();
|
const snackbars = await this.snackbar.all();
|
||||||
for (const snackbar of snackbars) {
|
for (const snackbar of snackbars) {
|
||||||
await snackbar.click();
|
await snackbar.click();
|
||||||
@@ -78,4 +79,8 @@ export class BasePage {
|
|||||||
|
|
||||||
return await this.page.screenshot({ path: computedPath, ...restOptions });
|
return await this.page.screenshot({ path: computedPath, ...restOptions });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async isMounted() {
|
||||||
|
await this.pageTitle.waitFor({ state: 'attached' });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -5,12 +5,12 @@ export class LoginPage extends BasePage {
|
|||||||
static defaultEmail = process.env.LOGIN_EMAIL;
|
static defaultEmail = process.env.LOGIN_EMAIL;
|
||||||
static defaultPassword = process.env.LOGIN_PASSWORD;
|
static defaultPassword = process.env.LOGIN_PASSWORD;
|
||||||
|
|
||||||
static setDefaultLogin (email, password) {
|
static setDefaultLogin(email, password) {
|
||||||
this.defaultEmail = email;
|
this.defaultEmail = email;
|
||||||
this.defaultPassword = password;
|
this.defaultPassword = password;
|
||||||
}
|
}
|
||||||
|
|
||||||
static resetDefaultLogin () {
|
static resetDefaultLogin() {
|
||||||
this.defaultEmail = process.env.LOGIN_EMAIL;
|
this.defaultEmail = process.env.LOGIN_EMAIL;
|
||||||
this.defaultPassword = process.env.LOGIN_PASSWORD;
|
this.defaultPassword = process.env.LOGIN_PASSWORD;
|
||||||
}
|
}
|
||||||
@@ -25,6 +25,7 @@ export class LoginPage extends BasePage {
|
|||||||
this.emailTextField = this.page.getByTestId('email-text-field');
|
this.emailTextField = this.page.getByTestId('email-text-field');
|
||||||
this.passwordTextField = this.page.getByTestId('password-text-field');
|
this.passwordTextField = this.page.getByTestId('password-text-field');
|
||||||
this.loginButton = this.page.getByTestId('login-button');
|
this.loginButton = this.page.getByTestId('login-button');
|
||||||
|
this.pageTitle = this.page.getByTestId('login-form-title');
|
||||||
}
|
}
|
||||||
|
|
||||||
async open() {
|
async open() {
|
||||||
|
@@ -22,6 +22,7 @@ test.describe('Role management page', () => {
|
|||||||
await test.step('Create a new role', async () => {
|
await test.step('Create a new role', async () => {
|
||||||
await adminRolesPage.navigateTo();
|
await adminRolesPage.navigateTo();
|
||||||
await adminRolesPage.createRoleButton.click();
|
await adminRolesPage.createRoleButton.click();
|
||||||
|
await adminCreateRolePage.isMounted();
|
||||||
await adminCreateRolePage.nameInput.fill('Create Edit Test');
|
await adminCreateRolePage.nameInput.fill('Create Edit Test');
|
||||||
await adminCreateRolePage.descriptionInput.fill('Test description');
|
await adminCreateRolePage.descriptionInput.fill('Test description');
|
||||||
await adminCreateRolePage.createButton.click();
|
await adminCreateRolePage.createButton.click();
|
||||||
@@ -54,6 +55,7 @@ test.describe('Role management page', () => {
|
|||||||
|
|
||||||
await test.step('Edit the role', async () => {
|
await test.step('Edit the role', async () => {
|
||||||
await adminRolesPage.clickEditRole(roleRow);
|
await adminRolesPage.clickEditRole(roleRow);
|
||||||
|
await adminEditRolePage.isMounted();
|
||||||
await adminEditRolePage.nameInput.fill('Create Update Test');
|
await adminEditRolePage.nameInput.fill('Create Update Test');
|
||||||
await adminEditRolePage.descriptionInput.fill('Update test description');
|
await adminEditRolePage.descriptionInput.fill('Update test description');
|
||||||
await adminEditRolePage.updateButton.click();
|
await adminEditRolePage.updateButton.click();
|
||||||
@@ -70,6 +72,7 @@ test.describe('Role management page', () => {
|
|||||||
roleRow = await test.step(
|
roleRow = await test.step(
|
||||||
'Make sure changes reflected on roles page',
|
'Make sure changes reflected on roles page',
|
||||||
async () => {
|
async () => {
|
||||||
|
await adminRolesPage.isMounted();
|
||||||
const roleRow = await adminRolesPage.getRoleRowByName(
|
const roleRow = await adminRolesPage.getRoleRowByName(
|
||||||
'Create Update Test'
|
'Create Update Test'
|
||||||
);
|
);
|
||||||
@@ -110,6 +113,8 @@ test.describe('Role management page', () => {
|
|||||||
// This test breaks right now
|
// This test breaks right now
|
||||||
test.skip('Make sure create/edit role page is scrollable', async ({
|
test.skip('Make sure create/edit role page is scrollable', async ({
|
||||||
adminRolesPage,
|
adminRolesPage,
|
||||||
|
adminEditRolePage,
|
||||||
|
adminCreateRolePage,
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
const initViewportSize = page.viewportSize;
|
const initViewportSize = page.viewportSize;
|
||||||
@@ -121,6 +126,7 @@ test.describe('Role management page', () => {
|
|||||||
await test.step('Ensure create role page is scrollable', async () => {
|
await test.step('Ensure create role page is scrollable', async () => {
|
||||||
await adminRolesPage.navigateTo(true);
|
await adminRolesPage.navigateTo(true);
|
||||||
await adminRolesPage.createRoleButton.click();
|
await adminRolesPage.createRoleButton.click();
|
||||||
|
await adminCreateRolePage.isMounted();
|
||||||
|
|
||||||
const initScrollTop = await page.evaluate(() => {
|
const initScrollTop = await page.evaluate(() => {
|
||||||
return document.documentElement.scrollTop;
|
return document.documentElement.scrollTop;
|
||||||
@@ -138,6 +144,7 @@ test.describe('Role management page', () => {
|
|||||||
await adminRolesPage.navigateTo(true);
|
await adminRolesPage.navigateTo(true);
|
||||||
const adminRow = await adminRolesPage.getRoleRowByName('Admin');
|
const adminRow = await adminRolesPage.getRoleRowByName('Admin');
|
||||||
await adminRolesPage.clickEditRole(adminRow);
|
await adminRolesPage.clickEditRole(adminRow);
|
||||||
|
await adminEditRolePage.isMounted();
|
||||||
|
|
||||||
const initScrollTop = await page.evaluate(() => {
|
const initScrollTop = await page.evaluate(() => {
|
||||||
return document.documentElement.scrollTop;
|
return document.documentElement.scrollTop;
|
||||||
@@ -166,6 +173,7 @@ test.describe('Role management page', () => {
|
|||||||
await adminRolesPage.navigateTo();
|
await adminRolesPage.navigateTo();
|
||||||
await test.step('Create a new role', async () => {
|
await test.step('Create a new role', async () => {
|
||||||
await adminRolesPage.createRoleButton.click();
|
await adminRolesPage.createRoleButton.click();
|
||||||
|
await adminCreateRolePage.isMounted();
|
||||||
await adminCreateRolePage.nameInput.fill('Delete Role');
|
await adminCreateRolePage.nameInput.fill('Delete Role');
|
||||||
await adminCreateRolePage.createButton.click();
|
await adminCreateRolePage.createButton.click();
|
||||||
await adminCreateRolePage.snackbar.waitFor({
|
await adminCreateRolePage.snackbar.waitFor({
|
||||||
@@ -268,6 +276,7 @@ test.describe('Role management page', () => {
|
|||||||
await adminRolesPage.navigateTo();
|
await adminRolesPage.navigateTo();
|
||||||
await test.step('Create a new role', async () => {
|
await test.step('Create a new role', async () => {
|
||||||
await adminRolesPage.createRoleButton.click();
|
await adminRolesPage.createRoleButton.click();
|
||||||
|
await adminCreateRolePage.isMounted();
|
||||||
await adminCreateRolePage.nameInput.fill('Cannot Delete Role');
|
await adminCreateRolePage.nameInput.fill('Cannot Delete Role');
|
||||||
await adminCreateRolePage.createButton.click();
|
await adminCreateRolePage.createButton.click();
|
||||||
await adminCreateRolePage.snackbar.waitFor({
|
await adminCreateRolePage.snackbar.waitFor({
|
||||||
@@ -282,6 +291,7 @@ test.describe('Role management page', () => {
|
|||||||
await test.step('Create a new user with this role', async () => {
|
await test.step('Create a new user with this role', async () => {
|
||||||
await adminUsersPage.navigateTo();
|
await adminUsersPage.navigateTo();
|
||||||
await adminUsersPage.createUserButton.click();
|
await adminUsersPage.createUserButton.click();
|
||||||
|
await adminCreateUserPage.isMounted();
|
||||||
await adminCreateUserPage.fullNameInput.fill('User Delete Role Test');
|
await adminCreateUserPage.fullNameInput.fill('User Delete Role Test');
|
||||||
await adminCreateUserPage.emailInput.fill(
|
await adminCreateUserPage.emailInput.fill(
|
||||||
'user-delete-role-test@automatisch.io'
|
'user-delete-role-test@automatisch.io'
|
||||||
@@ -306,6 +316,7 @@ test.describe('Role management page', () => {
|
|||||||
const row = await adminUsersPage.findUserPageWithEmail(
|
const row = await adminUsersPage.findUserPageWithEmail(
|
||||||
'user-delete-role-test@automatisch.io'
|
'user-delete-role-test@automatisch.io'
|
||||||
);
|
);
|
||||||
|
// await test.waitForTimeout(10000);
|
||||||
const modal = await adminUsersPage.clickDeleteUser(row);
|
const modal = await adminUsersPage.clickDeleteUser(row);
|
||||||
await modal.deleteButton.click();
|
await modal.deleteButton.click();
|
||||||
await adminUsersPage.snackbar.waitFor({
|
await adminUsersPage.snackbar.waitFor({
|
||||||
@@ -348,6 +359,7 @@ test('Accessibility of role management page', async ({
|
|||||||
await test.step('Create the basic test role', async () => {
|
await test.step('Create the basic test role', async () => {
|
||||||
await adminRolesPage.navigateTo();
|
await adminRolesPage.navigateTo();
|
||||||
await adminRolesPage.createRoleButton.click();
|
await adminRolesPage.createRoleButton.click();
|
||||||
|
await adminCreateRolePage.isMounted();
|
||||||
await adminCreateRolePage.nameInput.fill('Basic Test');
|
await adminCreateRolePage.nameInput.fill('Basic Test');
|
||||||
await adminCreateRolePage.createButton.click();
|
await adminCreateRolePage.createButton.click();
|
||||||
await adminCreateRolePage.snackbar.waitFor({
|
await adminCreateRolePage.snackbar.waitFor({
|
||||||
@@ -363,6 +375,7 @@ test('Accessibility of role management page', async ({
|
|||||||
await test.step('Create a new user with the basic role', async () => {
|
await test.step('Create a new user with the basic role', async () => {
|
||||||
await adminUsersPage.navigateTo();
|
await adminUsersPage.navigateTo();
|
||||||
await adminUsersPage.createUserButton.click();
|
await adminUsersPage.createUserButton.click();
|
||||||
|
await adminCreateUserPage.isMounted();
|
||||||
await adminCreateUserPage.fullNameInput.fill('Role Test');
|
await adminCreateUserPage.fullNameInput.fill('Role Test');
|
||||||
await adminCreateUserPage.emailInput.fill('basic-role-test@automatisch.io');
|
await adminCreateUserPage.emailInput.fill('basic-role-test@automatisch.io');
|
||||||
await adminCreateUserPage.passwordInput.fill('sample');
|
await adminCreateUserPage.passwordInput.fill('sample');
|
||||||
@@ -378,7 +391,7 @@ test('Accessibility of role management page', async ({
|
|||||||
'snackbar-create-user-success'
|
'snackbar-create-user-success'
|
||||||
);
|
);
|
||||||
await expect(snackbar.variant).toBe('success');
|
await expect(snackbar.variant).toBe('success');
|
||||||
await adminCreateRolePage.closeSnackbar();
|
await adminCreateUserPage.closeSnackbar();
|
||||||
});
|
});
|
||||||
|
|
||||||
await test.step('Logout and login to the basic role user', async () => {
|
await test.step('Logout and login to the basic role user', async () => {
|
||||||
@@ -386,6 +399,7 @@ test('Accessibility of role management page', async ({
|
|||||||
await page.getByTestId('logout-item').click();
|
await page.getByTestId('logout-item').click();
|
||||||
// await page.reload({ waitUntil: 'networkidle' });
|
// await page.reload({ waitUntil: 'networkidle' });
|
||||||
const loginPage = new LoginPage(page);
|
const loginPage = new LoginPage(page);
|
||||||
|
// await loginPage.isMounted();
|
||||||
await loginPage.login('basic-role-test@automatisch.io', 'sample');
|
await loginPage.login('basic-role-test@automatisch.io', 'sample');
|
||||||
await expect(loginPage.loginButton).not.toBeVisible();
|
await expect(loginPage.loginButton).not.toBeVisible();
|
||||||
await expect(page).toHaveURL('/flows');
|
await expect(page).toHaveURL('/flows');
|
||||||
@@ -414,6 +428,7 @@ test('Accessibility of role management page', async ({
|
|||||||
await page.getByTestId('profile-menu-button').click();
|
await page.getByTestId('profile-menu-button').click();
|
||||||
await page.getByTestId('logout-item').click();
|
await page.getByTestId('logout-item').click();
|
||||||
const loginPage = new LoginPage(page);
|
const loginPage = new LoginPage(page);
|
||||||
|
await loginPage.isMounted();
|
||||||
await loginPage.login();
|
await loginPage.login();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -423,6 +438,7 @@ test('Accessibility of role management page', async ({
|
|||||||
'basic-role-test@automatisch.io'
|
'basic-role-test@automatisch.io'
|
||||||
);
|
);
|
||||||
await adminUsersPage.clickEditUser(row);
|
await adminUsersPage.clickEditUser(row);
|
||||||
|
await adminEditUserPage.isMounted();
|
||||||
await adminEditUserPage.roleInput.click();
|
await adminEditUserPage.roleInput.click();
|
||||||
await adminEditUserPage.page.getByRole('option', { name: 'Admin' }).click();
|
await adminEditUserPage.page.getByRole('option', { name: 'Admin' }).click();
|
||||||
await adminEditUserPage.updateButton.click();
|
await adminEditUserPage.updateButton.click();
|
||||||
|
2
packages/types/index.d.ts
vendored
2
packages/types/index.d.ts
vendored
@@ -23,6 +23,7 @@ export interface IConnection {
|
|||||||
formattedData?: IJSONObject;
|
formattedData?: IJSONObject;
|
||||||
userId: string;
|
userId: string;
|
||||||
verified: boolean;
|
verified: boolean;
|
||||||
|
shared?: boolean;
|
||||||
count?: number;
|
count?: number;
|
||||||
flowCount?: number;
|
flowCount?: number;
|
||||||
appData?: IApp;
|
appData?: IApp;
|
||||||
@@ -462,6 +463,7 @@ type AppAuthClient = {
|
|||||||
appConfigId: string;
|
appConfigId: string;
|
||||||
authDefaults: string;
|
authDefaults: string;
|
||||||
formattedAuthDefaults: IJSONObject;
|
formattedAuthDefaults: IJSONObject;
|
||||||
|
active: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Notification = {
|
type Notification = {
|
||||||
|
BIN
packages/web/public/fonts/Inter-Bold.ttf
Normal file
BIN
packages/web/public/fonts/Inter-Bold.ttf
Normal file
Binary file not shown.
BIN
packages/web/public/fonts/Inter-Medium.ttf
Normal file
BIN
packages/web/public/fonts/Inter-Medium.ttf
Normal file
Binary file not shown.
BIN
packages/web/public/fonts/Inter-Regular.ttf
Normal file
BIN
packages/web/public/fonts/Inter-Regular.ttf
Normal file
Binary file not shown.
@@ -25,12 +25,52 @@
|
|||||||
-->
|
-->
|
||||||
<title>Automatisch</title>
|
<title>Automatisch</title>
|
||||||
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
||||||
<link
|
<link
|
||||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap"
|
rel="preload"
|
||||||
rel="stylesheet"
|
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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
@@ -0,0 +1,104 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import type { IField } from '@automatisch/types';
|
||||||
|
import LoadingButton from '@mui/lab/LoadingButton';
|
||||||
|
import Alert from '@mui/material/Alert';
|
||||||
|
import Dialog from '@mui/material/Dialog';
|
||||||
|
import DialogContent from '@mui/material/DialogContent';
|
||||||
|
import DialogContentText from '@mui/material/DialogContentText';
|
||||||
|
import DialogTitle from '@mui/material/DialogTitle';
|
||||||
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
|
import { FieldValues, SubmitHandler } from 'react-hook-form';
|
||||||
|
import type { UseFormProps } from 'react-hook-form';
|
||||||
|
import type { ApolloError } from '@apollo/client';
|
||||||
|
|
||||||
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
|
import InputCreator from 'components/InputCreator';
|
||||||
|
import Switch from 'components/Switch';
|
||||||
|
import TextField from 'components/TextField';
|
||||||
|
|
||||||
|
import { Form } from './style';
|
||||||
|
|
||||||
|
type AdminApplicationAuthClientDialogProps = {
|
||||||
|
title: string;
|
||||||
|
authFields?: IField[];
|
||||||
|
defaultValues: UseFormProps['defaultValues'];
|
||||||
|
loading: boolean;
|
||||||
|
submitting: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
error?: ApolloError;
|
||||||
|
submitHandler: SubmitHandler<FieldValues>;
|
||||||
|
onClose: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function AdminApplicationAuthClientDialog(
|
||||||
|
props: AdminApplicationAuthClientDialogProps
|
||||||
|
): React.ReactElement {
|
||||||
|
const {
|
||||||
|
error,
|
||||||
|
onClose,
|
||||||
|
title,
|
||||||
|
loading,
|
||||||
|
submitHandler,
|
||||||
|
authFields,
|
||||||
|
submitting,
|
||||||
|
defaultValues,
|
||||||
|
disabled = false,
|
||||||
|
} = props;
|
||||||
|
const formatMessage = useFormatMessage();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={true} onClose={onClose}>
|
||||||
|
<DialogTitle>{title}</DialogTitle>
|
||||||
|
{error && (
|
||||||
|
<Alert
|
||||||
|
severity="error"
|
||||||
|
sx={{ mt: 1, fontWeight: 500, wordBreak: 'break-all' }}
|
||||||
|
>
|
||||||
|
{error.message}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
<DialogContent>
|
||||||
|
{loading ? (
|
||||||
|
<CircularProgress
|
||||||
|
data-test="search-for-app-loader"
|
||||||
|
sx={{ display: 'block', margin: '20px auto' }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<DialogContentText tabIndex={-1} component="div">
|
||||||
|
<Form
|
||||||
|
onSubmit={submitHandler}
|
||||||
|
defaultValues={defaultValues}
|
||||||
|
render={({ formState: { isDirty } }) => (
|
||||||
|
<>
|
||||||
|
<Switch
|
||||||
|
name="active"
|
||||||
|
label={formatMessage('authClient.inputActive')}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
required={true}
|
||||||
|
name="name"
|
||||||
|
label={formatMessage('authClient.inputName')}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
{authFields?.map((field: IField) => (
|
||||||
|
<InputCreator key={field.key} schema={field} />
|
||||||
|
))}
|
||||||
|
<LoadingButton
|
||||||
|
type="submit"
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
sx={{ boxShadow: 2 }}
|
||||||
|
loading={submitting}
|
||||||
|
disabled={disabled || !isDirty}
|
||||||
|
>
|
||||||
|
{formatMessage('authClient.buttonSubmit')}
|
||||||
|
</LoadingButton>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
></Form>
|
||||||
|
</DialogContentText>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
@@ -0,0 +1,9 @@
|
|||||||
|
import { styled } from '@mui/material/styles';
|
||||||
|
import BaseForm from 'components/Form';
|
||||||
|
|
||||||
|
export const Form = styled(BaseForm)(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: theme.spacing(2),
|
||||||
|
paddingTop: theme.spacing(1),
|
||||||
|
}));
|
@@ -0,0 +1,89 @@
|
|||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
|
import Stack from '@mui/material/Stack';
|
||||||
|
import Card from '@mui/material/Card';
|
||||||
|
import CardActionArea from '@mui/material/CardActionArea';
|
||||||
|
import CardContent from '@mui/material/CardContent';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import Chip from '@mui/material/Chip';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
|
||||||
|
import * as URLS from 'config/urls';
|
||||||
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
|
import useAppAuthClients from 'hooks/useAppAuthClients.ee';
|
||||||
|
|
||||||
|
import NoResultFound from 'components/NoResultFound';
|
||||||
|
|
||||||
|
type AdminApplicationAuthClientsProps = {
|
||||||
|
appKey: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function AdminApplicationAuthClients(
|
||||||
|
props: AdminApplicationAuthClientsProps
|
||||||
|
): React.ReactElement {
|
||||||
|
const { appKey } = props;
|
||||||
|
const formatMessage = useFormatMessage();
|
||||||
|
const { appAuthClients, loading } = useAppAuthClients({ appKey });
|
||||||
|
|
||||||
|
if (loading)
|
||||||
|
return <CircularProgress sx={{ display: 'block', margin: '20px auto' }} />;
|
||||||
|
|
||||||
|
if (!appAuthClients?.length) {
|
||||||
|
return (
|
||||||
|
<NoResultFound
|
||||||
|
to={URLS.ADMIN_APP_AUTH_CLIENTS_CREATE(appKey)}
|
||||||
|
text={formatMessage('adminAppsAuthClients.noAuthClients')}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortedAuthClients = appAuthClients.slice().sort((a, b) => {
|
||||||
|
if (a.id < b.id) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (a.id > b.id) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{sortedAuthClients.map((client) => (
|
||||||
|
<Card sx={{ mb: 1 }} key={client.id}>
|
||||||
|
<CardActionArea
|
||||||
|
component={Link}
|
||||||
|
to={URLS.ADMIN_APP_AUTH_CLIENT(appKey, client.id)}
|
||||||
|
>
|
||||||
|
<CardContent>
|
||||||
|
<Stack direction="row" justifyContent="space-between">
|
||||||
|
<Typography variant="h6" noWrap>
|
||||||
|
{client.name}
|
||||||
|
</Typography>
|
||||||
|
<Chip
|
||||||
|
size="small"
|
||||||
|
color={client?.active ? 'success' : 'info'}
|
||||||
|
variant={client?.active ? 'filled' : 'outlined'}
|
||||||
|
label={formatMessage(
|
||||||
|
client?.active
|
||||||
|
? 'adminAppsAuthClients.statusActive'
|
||||||
|
: 'adminAppsAuthClients.statusInactive'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</CardContent>
|
||||||
|
</CardActionArea>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
<Stack justifyContent="flex-end" direction="row">
|
||||||
|
<Link to={URLS.ADMIN_APP_AUTH_CLIENTS_CREATE(appKey)}>
|
||||||
|
<Button variant="contained" sx={{ mt: 2 }} component="div">
|
||||||
|
{formatMessage('createAuthClient.button')}
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AdminApplicationAuthClients;
|
@@ -0,0 +1,168 @@
|
|||||||
|
import type { IApp, IField, IJSONObject } from '@automatisch/types';
|
||||||
|
import LoadingButton from '@mui/lab/LoadingButton';
|
||||||
|
import Alert from '@mui/material/Alert';
|
||||||
|
import Dialog from '@mui/material/Dialog';
|
||||||
|
import DialogContent from '@mui/material/DialogContent';
|
||||||
|
import DialogContentText from '@mui/material/DialogContentText';
|
||||||
|
import DialogTitle from '@mui/material/DialogTitle';
|
||||||
|
import * as React from 'react';
|
||||||
|
import { FieldValues, SubmitHandler } from 'react-hook-form';
|
||||||
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
import AppAuthClientsDialog from 'components/AppAuthClientsDialog/index.ee';
|
||||||
|
import InputCreator from 'components/InputCreator';
|
||||||
|
import * as URLS from 'config/urls';
|
||||||
|
import useAuthenticateApp from 'hooks/useAuthenticateApp.ee';
|
||||||
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
|
import { generateExternalLink } from '../../helpers/translationValues';
|
||||||
|
import { Form } from './style';
|
||||||
|
|
||||||
|
type AdminApplicationConnectionCreateProps = {
|
||||||
|
onClose: (response: Record<string, unknown>) => void;
|
||||||
|
application: IApp;
|
||||||
|
connectionId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function AdminApplicationConnectionCreate(
|
||||||
|
props: AdminApplicationConnectionCreateProps
|
||||||
|
): React.ReactElement {
|
||||||
|
const { application, connectionId, onClose } = props;
|
||||||
|
const { name, authDocUrl, key, auth } = application;
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const formatMessage = useFormatMessage();
|
||||||
|
const [error, setError] = React.useState<IJSONObject | null>(null);
|
||||||
|
const [inProgress, setInProgress] = React.useState(false);
|
||||||
|
const hasConnection = Boolean(connectionId);
|
||||||
|
const useShared = searchParams.get('shared') === 'true';
|
||||||
|
const appAuthClientId = searchParams.get('appAuthClientId') || undefined;
|
||||||
|
const { authenticate } = useAuthenticateApp({
|
||||||
|
appKey: key,
|
||||||
|
connectionId,
|
||||||
|
appAuthClientId,
|
||||||
|
useShared: !!appAuthClientId,
|
||||||
|
});
|
||||||
|
|
||||||
|
React.useEffect(function relayProviderData() {
|
||||||
|
if (window.opener) {
|
||||||
|
window.opener.postMessage({
|
||||||
|
source: 'automatisch',
|
||||||
|
payload: { search: window.location.search, hash: window.location.hash },
|
||||||
|
});
|
||||||
|
window.close();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
React.useEffect(
|
||||||
|
function initiateSharedAuthenticationForGivenAuthClient() {
|
||||||
|
if (!appAuthClientId) return;
|
||||||
|
if (!authenticate) return;
|
||||||
|
|
||||||
|
const asyncAuthenticate = async () => {
|
||||||
|
await authenticate();
|
||||||
|
|
||||||
|
navigate(URLS.ADMIN_APP_CONNECTIONS(key));
|
||||||
|
};
|
||||||
|
|
||||||
|
asyncAuthenticate();
|
||||||
|
},
|
||||||
|
[appAuthClientId, authenticate]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleClientClick = (appAuthClientId: string) =>
|
||||||
|
navigate(
|
||||||
|
URLS.ADMIN_APP_CONNECTIONS_CREATE_WITH_AUTH_CLIENT_ID(
|
||||||
|
key,
|
||||||
|
appAuthClientId
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleAuthClientsDialogClose = () =>
|
||||||
|
navigate(URLS.ADMIN_APP_CONNECTIONS(key));
|
||||||
|
|
||||||
|
const submitHandler: SubmitHandler<FieldValues> = React.useCallback(
|
||||||
|
async (data) => {
|
||||||
|
if (!authenticate) return;
|
||||||
|
|
||||||
|
setInProgress(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await authenticate({
|
||||||
|
fields: data,
|
||||||
|
});
|
||||||
|
onClose(response as Record<string, unknown>);
|
||||||
|
} catch (err) {
|
||||||
|
const error = err as IJSONObject;
|
||||||
|
console.log(error);
|
||||||
|
setError((error.graphQLErrors as IJSONObject[])?.[0]);
|
||||||
|
} finally {
|
||||||
|
setInProgress(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[authenticate]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (useShared)
|
||||||
|
return (
|
||||||
|
<AppAuthClientsDialog
|
||||||
|
appKey={key}
|
||||||
|
onClose={handleAuthClientsDialogClose}
|
||||||
|
onClientClick={handleClientClick}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (appAuthClientId) return <React.Fragment />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={true} onClose={onClose}>
|
||||||
|
<DialogTitle>
|
||||||
|
{hasConnection
|
||||||
|
? formatMessage('adminAppsConnections.reconnectConnection')
|
||||||
|
: formatMessage('adminAppsConnections.createConnection')}
|
||||||
|
</DialogTitle>
|
||||||
|
|
||||||
|
{authDocUrl && (
|
||||||
|
<Alert severity="info" sx={{ fontWeight: 300 }}>
|
||||||
|
{formatMessage('adminAppsConnections.callToDocs', {
|
||||||
|
appName: name,
|
||||||
|
docsLink: generateExternalLink(authDocUrl),
|
||||||
|
})}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<Alert
|
||||||
|
severity="error"
|
||||||
|
sx={{ mt: 1, fontWeight: 500, wordBreak: 'break-all' }}
|
||||||
|
>
|
||||||
|
{error.message}
|
||||||
|
{error.details && (
|
||||||
|
<pre style={{ whiteSpace: 'pre-wrap' }}>
|
||||||
|
{JSON.stringify(error.details, null, 2)}
|
||||||
|
</pre>
|
||||||
|
)}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<DialogContent>
|
||||||
|
<DialogContentText tabIndex={-1} component="div">
|
||||||
|
<Form onSubmit={submitHandler}>
|
||||||
|
{auth?.fields?.map((field: IField) => (
|
||||||
|
<InputCreator key={field.key} schema={field} />
|
||||||
|
))}
|
||||||
|
|
||||||
|
<LoadingButton
|
||||||
|
type="submit"
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
sx={{ boxShadow: 2 }}
|
||||||
|
loading={inProgress}
|
||||||
|
>
|
||||||
|
{formatMessage('adminAppsConnections.submit')}
|
||||||
|
</LoadingButton>
|
||||||
|
</Form>
|
||||||
|
</DialogContentText>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user