Merge branch 'automatisch:main' into feature/signalwire-integration
This commit is contained in:
@@ -12,6 +12,8 @@ export async function createUser(
|
|||||||
const userParams = {
|
const userParams = {
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
|
fullName: 'Initial admin',
|
||||||
|
role: 'admin',
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@@ -58,6 +58,7 @@
|
|||||||
"oauth-1.0a": "^2.2.6",
|
"oauth-1.0a": "^2.2.6",
|
||||||
"objection": "^3.0.0",
|
"objection": "^3.0.0",
|
||||||
"pg": "^8.7.1",
|
"pg": "^8.7.1",
|
||||||
|
"stripe": "^11.13.0",
|
||||||
"winston": "^3.7.1"
|
"winston": "^3.7.1"
|
||||||
},
|
},
|
||||||
"contributors": [
|
"contributors": [
|
||||||
|
79
packages/backend/src/apps/filter/actions/continue/index.ts
Normal file
79
packages/backend/src/apps/filter/actions/continue/index.ts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import defineAction from '../../../../helpers/define-action';
|
||||||
|
|
||||||
|
type TGroupItem = {
|
||||||
|
key: string;
|
||||||
|
operator: keyof TOperators;
|
||||||
|
value: string;
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type TGroup = Record<'and', TGroupItem[]>;
|
||||||
|
|
||||||
|
const isEqual = (a: string, b: string) => a === b;
|
||||||
|
const isNotEqual = (a: string, b: string) => !isEqual(a, b)
|
||||||
|
const isGreaterThan = (a: string, b: string) => Number(a) > Number(b);
|
||||||
|
const isLessThan = (a: string, b: string) => Number(a) < Number(b);
|
||||||
|
const isGreaterThanOrEqual = (a: string, b: string) => Number(a) >= Number(b);
|
||||||
|
const isLessThanOrEqual = (a: string, b: string) => Number(a) <= Number(b);
|
||||||
|
const contains = (a: string, b: string) => a.includes(b);
|
||||||
|
const doesNotContain = (a: string, b: string) => !contains(a, b);
|
||||||
|
|
||||||
|
type TOperatorFunc = (a: string, b: string) => boolean;
|
||||||
|
|
||||||
|
type TOperators = {
|
||||||
|
equal: TOperatorFunc;
|
||||||
|
not_equal: TOperatorFunc;
|
||||||
|
greater_than: TOperatorFunc;
|
||||||
|
less_than: TOperatorFunc;
|
||||||
|
greater_than_or_equal: TOperatorFunc;
|
||||||
|
less_than_or_equal: TOperatorFunc;
|
||||||
|
contains: TOperatorFunc;
|
||||||
|
not_contains: TOperatorFunc;
|
||||||
|
};
|
||||||
|
|
||||||
|
const operators: TOperators = {
|
||||||
|
'equal': isEqual,
|
||||||
|
'not_equal': isNotEqual,
|
||||||
|
'greater_than': isGreaterThan,
|
||||||
|
'less_than': isLessThan,
|
||||||
|
'greater_than_or_equal': isGreaterThanOrEqual,
|
||||||
|
'less_than_or_equal': isLessThanOrEqual,
|
||||||
|
'contains': contains,
|
||||||
|
'not_contains': doesNotContain,
|
||||||
|
};
|
||||||
|
|
||||||
|
const operate = (operation: keyof TOperators, a: string, b: string) => {
|
||||||
|
return operators[operation](a, b);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default defineAction({
|
||||||
|
name: 'Continue if conditions match',
|
||||||
|
key: 'continueIfMatches',
|
||||||
|
description: 'Let the execution continue if the conditions match',
|
||||||
|
arguments: [],
|
||||||
|
|
||||||
|
async run($) {
|
||||||
|
const orGroups = $.step.parameters.or as TGroup[];
|
||||||
|
|
||||||
|
const matchingGroups = orGroups.reduce((groups, group) => {
|
||||||
|
const matchingConditions = group.and
|
||||||
|
.filter((condition) => operate(condition.operator, condition.key, condition.value));
|
||||||
|
|
||||||
|
if (matchingConditions.length) {
|
||||||
|
return groups.concat([{ and: matchingConditions }]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return groups;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (matchingGroups.length === 0) {
|
||||||
|
$.execution.exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
$.setActionItem({
|
||||||
|
raw: {
|
||||||
|
or: matchingGroups,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
3
packages/backend/src/apps/filter/actions/index.ts
Normal file
3
packages/backend/src/apps/filter/actions/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import continueIfMatches from './continue';
|
||||||
|
|
||||||
|
export default [continueIfMatches];
|
8
packages/backend/src/apps/filter/assets/favicon.svg
Normal file
8
packages/backend/src/apps/filter/assets/favicon.svg
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<svg width="800px" height="800px" viewBox="0 0 512 512" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||||
|
<g id="Shape" fill="#000000" transform="translate(42.666667, 85.333333)">
|
||||||
|
<path d="M3.55271368e-14,1.42108547e-14 L191.565013,234.666667 L192,234.666667 L192,384 L234.666667,384 L234.666667,234.666667 L426.666667,1.42108547e-14 L3.55271368e-14,1.42108547e-14 Z M214.448,192 L211.81248,192 L89.9076267,42.6666667 L336.630187,42.6666667 L214.448,192 Z">
|
||||||
|
</path>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 628 B |
0
packages/backend/src/apps/filter/index.d.ts
vendored
Normal file
0
packages/backend/src/apps/filter/index.d.ts
vendored
Normal file
14
packages/backend/src/apps/filter/index.ts
Normal file
14
packages/backend/src/apps/filter/index.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import defineApp from '../../helpers/define-app';
|
||||||
|
import actions from './actions';
|
||||||
|
|
||||||
|
export default defineApp({
|
||||||
|
name: 'Filter',
|
||||||
|
key: 'filter',
|
||||||
|
iconUrl: '{BASE_URL}/apps/filter/assets/favicon.svg',
|
||||||
|
authDocUrl: 'https://automatisch.io/docs/apps/filter/connection',
|
||||||
|
supportsConnections: false,
|
||||||
|
baseUrl: '',
|
||||||
|
apiBaseUrl: '',
|
||||||
|
primaryColor: '001F52',
|
||||||
|
actions,
|
||||||
|
});
|
@@ -1,8 +1,8 @@
|
|||||||
import { TBeforeRequest } from '@automatisch/types';
|
import { TBeforeRequest } from '@automatisch/types';
|
||||||
|
|
||||||
const addAuthHeader: TBeforeRequest = ($, requestConfig) => {
|
const addAuthHeader: TBeforeRequest = ($, requestConfig) => {
|
||||||
if ($.auth.data.apiBaseUrl) {
|
if ($.auth.data.serverUrl) {
|
||||||
requestConfig.baseURL = $.auth.data.apiBaseUrl as string;
|
requestConfig.baseURL = $.auth.data.serverUrl as string;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($.auth.data?.username && $.auth.data?.password) {
|
if ($.auth.data?.username && $.auth.data?.password) {
|
||||||
|
@@ -51,25 +51,20 @@ export default defineAction({
|
|||||||
value: false,
|
value: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
additionalFields: {
|
||||||
{
|
type: 'query',
|
||||||
label: 'Bot name',
|
name: 'getDynamicFields',
|
||||||
key: 'botName',
|
arguments: [
|
||||||
type: 'string' as const,
|
{
|
||||||
required: true,
|
name: 'key',
|
||||||
value: 'Automatisch',
|
value: 'listFieldsAfterSendAsBot',
|
||||||
description:
|
},
|
||||||
'Specify the bot name which appears as a bold username above the message inside Slack. Defaults to Automatisch.',
|
{
|
||||||
variables: true,
|
name: 'parameters.sendAsBot',
|
||||||
},
|
value: '{parameters.sendAsBot}',
|
||||||
{
|
},
|
||||||
label: 'Bot icon',
|
],
|
||||||
key: 'botIcon',
|
},
|
||||||
type: 'string' as const,
|
|
||||||
required: false,
|
|
||||||
description:
|
|
||||||
'Either an image url or an emoji available to your team (surrounded by :). For example, https://example.com/icon_256.png or :robot_face:',
|
|
||||||
variables: true,
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
|
3
packages/backend/src/apps/slack/dynamic-fields/index.ts
Normal file
3
packages/backend/src/apps/slack/dynamic-fields/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import listFieldsAfterSendAsBot from './send-as-bot';
|
||||||
|
|
||||||
|
export default [listFieldsAfterSendAsBot];
|
@@ -0,0 +1,32 @@
|
|||||||
|
import { IGlobalVariable } from '@automatisch/types';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'List fields after send as bot',
|
||||||
|
key: 'listFieldsAfterSendAsBot',
|
||||||
|
|
||||||
|
async run($: IGlobalVariable) {
|
||||||
|
if ($.step.parameters.sendAsBot) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: 'Bot name',
|
||||||
|
key: 'botName',
|
||||||
|
type: 'string' as const,
|
||||||
|
required: true,
|
||||||
|
value: 'Automatisch',
|
||||||
|
description:
|
||||||
|
'Specify the bot name which appears as a bold username above the message inside Slack. Defaults to Automatisch.',
|
||||||
|
variables: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Bot icon',
|
||||||
|
key: 'botIcon',
|
||||||
|
type: 'string' as const,
|
||||||
|
required: false,
|
||||||
|
description:
|
||||||
|
'Either an image url or an emoji available to your team (surrounded by :). For example, https://example.com/icon_256.png or :robot_face:',
|
||||||
|
variables: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
@@ -3,6 +3,7 @@ import addAuthHeader from './common/add-auth-header';
|
|||||||
import actions from './actions';
|
import actions from './actions';
|
||||||
import auth from './auth';
|
import auth from './auth';
|
||||||
import dynamicData from './dynamic-data';
|
import dynamicData from './dynamic-data';
|
||||||
|
import dynamicFields from './dynamic-fields';
|
||||||
|
|
||||||
export default defineApp({
|
export default defineApp({
|
||||||
name: 'Slack',
|
name: 'Slack',
|
||||||
@@ -17,4 +18,5 @@ export default defineApp({
|
|||||||
auth,
|
auth,
|
||||||
actions,
|
actions,
|
||||||
dynamicData,
|
dynamicData,
|
||||||
|
dynamicFields,
|
||||||
});
|
});
|
||||||
|
@@ -38,6 +38,11 @@ type AppConfig = {
|
|||||||
smtpUser: string;
|
smtpUser: string;
|
||||||
smtpPassword: string;
|
smtpPassword: string;
|
||||||
fromEmail: string;
|
fromEmail: string;
|
||||||
|
isCloud: boolean;
|
||||||
|
stripeSecretKey: string;
|
||||||
|
stripeSigningSecret: string;
|
||||||
|
stripeStarterPriceKey: string;
|
||||||
|
stripeGrowthPriceKey: string;
|
||||||
licenseKey: string;
|
licenseKey: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -104,6 +109,11 @@ const appConfig: AppConfig = {
|
|||||||
smtpUser: process.env.SMTP_USER,
|
smtpUser: process.env.SMTP_USER,
|
||||||
smtpPassword: process.env.SMTP_PASSWORD,
|
smtpPassword: process.env.SMTP_PASSWORD,
|
||||||
fromEmail: process.env.FROM_EMAIL,
|
fromEmail: process.env.FROM_EMAIL,
|
||||||
|
isCloud: process.env.AUTOMATISCH_CLOUD === 'true',
|
||||||
|
stripeSecretKey: process.env.STRIPE_SECRET_KEY,
|
||||||
|
stripeSigningSecret: process.env.STRIPE_SIGNING_SECRET,
|
||||||
|
stripeStarterPriceKey: process.env.STRIPE_STARTER_PRICE_KEY,
|
||||||
|
stripeGrowthPriceKey: process.env.STRIPE_GROWTH_PRICE_KEY,
|
||||||
licenseKey: process.env.LICENSE_KEY,
|
licenseKey: process.env.LICENSE_KEY,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
23
packages/backend/src/controllers/stripe/webhooks.ee.ts
Normal file
23
packages/backend/src/controllers/stripe/webhooks.ee.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { Response } from 'express';
|
||||||
|
import { IRequest } from '@automatisch/types';
|
||||||
|
import Billing from '../../helpers/billing/index.ee';
|
||||||
|
import appConfig from '../../config/app';
|
||||||
|
import logger from '../../helpers/logger';
|
||||||
|
|
||||||
|
export default async (request: IRequest, response: Response) => {
|
||||||
|
const signature = request.headers['stripe-signature'];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const event = Billing.stripe.webhooks.constructEvent(
|
||||||
|
request.rawBody,
|
||||||
|
signature,
|
||||||
|
appConfig.stripeSigningSecret
|
||||||
|
);
|
||||||
|
|
||||||
|
await Billing.handleWebhooks(event);
|
||||||
|
return response.sendStatus(200);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Webhook Error: ${error.message}`);
|
||||||
|
return response.sendStatus(400);
|
||||||
|
}
|
||||||
|
};
|
@@ -0,0 +1,15 @@
|
|||||||
|
import { Knex } from 'knex';
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
return knex.schema.table('users', async (table) => {
|
||||||
|
table.string('full_name');
|
||||||
|
|
||||||
|
await knex('users').update({ full_name: 'Initial admin' });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
return knex.schema.table('users', (table) => {
|
||||||
|
table.dropColumn('full_name');
|
||||||
|
});
|
||||||
|
}
|
@@ -0,0 +1,24 @@
|
|||||||
|
import { Knex } from 'knex';
|
||||||
|
import appConfig from '../../config/app';
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (!appConfig.isCloud) return;
|
||||||
|
|
||||||
|
return knex.schema.createTable('payment_plans', (table) => {
|
||||||
|
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'));
|
||||||
|
table.string('name').notNullable();
|
||||||
|
table.integer('task_count').notNullable();
|
||||||
|
table.uuid('user_id').references('id').inTable('users');
|
||||||
|
table.string('stripe_customer_id');
|
||||||
|
table.string('stripe_subscription_id');
|
||||||
|
table.timestamp('current_period_started_at').nullable();
|
||||||
|
table.timestamp('current_period_ends_at').nullable();
|
||||||
|
table.timestamp('deleted_at').nullable();
|
||||||
|
table.timestamps(true, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
if (!appConfig.isCloud) return;
|
||||||
|
return knex.schema.dropTable('payment_plans');
|
||||||
|
}
|
@@ -0,0 +1,20 @@
|
|||||||
|
import { Knex } from 'knex';
|
||||||
|
import appConfig from '../../config/app';
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (!appConfig.isCloud) return;
|
||||||
|
|
||||||
|
return knex.schema.createTable('usage_data', (table) => {
|
||||||
|
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'));
|
||||||
|
table.uuid('user_id').references('id').inTable('users');
|
||||||
|
table.string('consumed_task_count').notNullable();
|
||||||
|
table.timestamp('next_reset_at').nullable();
|
||||||
|
table.timestamp('deleted_at').nullable();
|
||||||
|
table.timestamps(true, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
if (!appConfig.isCloud) return;
|
||||||
|
return knex.schema.dropTable('usage_data');
|
||||||
|
}
|
@@ -0,0 +1,18 @@
|
|||||||
|
import { Knex } from 'knex';
|
||||||
|
import appConfig from '../../config/app';
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (!appConfig.isCloud) return;
|
||||||
|
|
||||||
|
return knex.schema.alterTable('usage_data', (table) => {
|
||||||
|
table.integer('consumed_task_count').notNullable().alter();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
if (!appConfig.isCloud) return;
|
||||||
|
|
||||||
|
return knex.schema.alterTable('usage_data', (table) => {
|
||||||
|
table.string('consumed_task_count').notNullable().alter();
|
||||||
|
});
|
||||||
|
}
|
3
packages/backend/src/errors/already-processed.ts
Normal file
3
packages/backend/src/errors/already-processed.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import BaseError from './base';
|
||||||
|
|
||||||
|
export default class AlreadyProcessedError extends BaseError { }
|
@@ -13,6 +13,7 @@ import createStep from './mutations/create-step';
|
|||||||
import updateStep from './mutations/update-step';
|
import updateStep from './mutations/update-step';
|
||||||
import deleteStep from './mutations/delete-step';
|
import deleteStep from './mutations/delete-step';
|
||||||
import createUser from './mutations/create-user.ee';
|
import createUser from './mutations/create-user.ee';
|
||||||
|
import deleteUser from './mutations/delete-user.ee';
|
||||||
import updateUser from './mutations/update-user';
|
import updateUser from './mutations/update-user';
|
||||||
import forgotPassword from './mutations/forgot-password.ee';
|
import forgotPassword from './mutations/forgot-password.ee';
|
||||||
import resetPassword from './mutations/reset-password.ee';
|
import resetPassword from './mutations/reset-password.ee';
|
||||||
@@ -34,6 +35,7 @@ const mutationResolvers = {
|
|||||||
updateStep,
|
updateStep,
|
||||||
deleteStep,
|
deleteStep,
|
||||||
createUser,
|
createUser,
|
||||||
|
deleteUser,
|
||||||
updateUser,
|
updateUser,
|
||||||
forgotPassword,
|
forgotPassword,
|
||||||
resetPassword,
|
resetPassword,
|
||||||
|
@@ -1,14 +1,17 @@
|
|||||||
import User from '../../models/user';
|
import User from '../../models/user';
|
||||||
|
import Billing from '../../helpers/billing/index.ee';
|
||||||
|
import appConfig from '../../config/app';
|
||||||
|
|
||||||
type Params = {
|
type Params = {
|
||||||
input: {
|
input: {
|
||||||
|
fullName: string;
|
||||||
email: string;
|
email: string;
|
||||||
password: string;
|
password: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const createUser = async (_parent: unknown, params: Params) => {
|
const createUser = async (_parent: unknown, params: Params) => {
|
||||||
const { email, password } = params.input;
|
const { fullName, email, password } = params.input;
|
||||||
|
|
||||||
const existingUser = await User.query().findOne({ email });
|
const existingUser = await User.query().findOne({ email });
|
||||||
|
|
||||||
@@ -17,11 +20,16 @@ const createUser = async (_parent: unknown, params: Params) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const user = await User.query().insert({
|
const user = await User.query().insert({
|
||||||
|
fullName,
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
role: 'user',
|
role: 'user',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (appConfig.isCloud) {
|
||||||
|
Billing.createSubscription(user);
|
||||||
|
}
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
22
packages/backend/src/graphql/mutations/delete-user.ee.ts
Normal file
22
packages/backend/src/graphql/mutations/delete-user.ee.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import Context from '../../types/express/context';
|
||||||
|
import deleteUserQueue from '../../queues/delete-user.ee';
|
||||||
|
import { Duration } from 'luxon';
|
||||||
|
|
||||||
|
const deleteUser = async (_parent: unknown, params: never, context: Context) => {
|
||||||
|
const id = context.currentUser.id;
|
||||||
|
|
||||||
|
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 deleteUser;
|
@@ -1,3 +1,4 @@
|
|||||||
|
import appConfig from '../../config/app';
|
||||||
import User from '../../models/user';
|
import User from '../../models/user';
|
||||||
import emailQueue from '../../queues/email';
|
import emailQueue from '../../queues/email';
|
||||||
import {
|
import {
|
||||||
@@ -30,6 +31,8 @@ const forgotPassword = async (_parent: unknown, params: Params) => {
|
|||||||
template: 'reset-password-instructions',
|
template: 'reset-password-instructions',
|
||||||
params: {
|
params: {
|
||||||
token: user.resetPasswordToken,
|
token: user.resetPasswordToken,
|
||||||
|
webAppUrl: appConfig.webAppUrl,
|
||||||
|
fullName: user.fullName,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -40,7 +43,7 @@ const forgotPassword = async (_parent: unknown, params: Params) => {
|
|||||||
|
|
||||||
await emailQueue.add(jobName, jobPayload, jobOptions);
|
await emailQueue.add(jobName, jobPayload, jobOptions);
|
||||||
|
|
||||||
return;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default forgotPassword;
|
export default forgotPassword;
|
||||||
|
@@ -24,7 +24,7 @@ const resetPassword = async (_parent: unknown, params: Params) => {
|
|||||||
|
|
||||||
await user.resetPassword(password);
|
await user.resetPassword(password);
|
||||||
|
|
||||||
return;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default resetPassword;
|
export default resetPassword;
|
||||||
|
@@ -0,0 +1,9 @@
|
|||||||
|
import appConfig from '../../config/app';
|
||||||
|
|
||||||
|
const getAutomatischInfo = async () => {
|
||||||
|
return {
|
||||||
|
isCloud: appConfig.isCloud,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getAutomatischInfo;
|
48
packages/backend/src/graphql/queries/get-dynamic-fields.ts
Normal file
48
packages/backend/src/graphql/queries/get-dynamic-fields.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { IDynamicFields, IJSONObject } from '@automatisch/types';
|
||||||
|
import Context from '../../types/express/context';
|
||||||
|
import App from '../../models/app';
|
||||||
|
import globalVariable from '../../helpers/global-variable';
|
||||||
|
|
||||||
|
type Params = {
|
||||||
|
stepId: string;
|
||||||
|
key: string;
|
||||||
|
parameters: IJSONObject;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDynamicFields = async (
|
||||||
|
_parent: unknown,
|
||||||
|
params: Params,
|
||||||
|
context: Context
|
||||||
|
) => {
|
||||||
|
const step = await context.currentUser
|
||||||
|
.$relatedQuery('steps')
|
||||||
|
.withGraphFetched({
|
||||||
|
connection: true,
|
||||||
|
flow: true,
|
||||||
|
})
|
||||||
|
.findById(params.stepId);
|
||||||
|
|
||||||
|
if (!step) return null;
|
||||||
|
|
||||||
|
const connection = step.connection;
|
||||||
|
|
||||||
|
if (!connection || !step.appKey) return null;
|
||||||
|
|
||||||
|
const app = await App.findOneByKey(step.appKey);
|
||||||
|
const $ = await globalVariable({ connection, app, flow: step.flow, step });
|
||||||
|
|
||||||
|
const command = app.dynamicFields.find(
|
||||||
|
(data: IDynamicFields) => data.key === params.key
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const parameterKey in params.parameters) {
|
||||||
|
const parameterValue = params.parameters[parameterKey];
|
||||||
|
$.step.parameters[parameterKey] = parameterValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const additionalFields = await command.run($) || [];
|
||||||
|
|
||||||
|
return additionalFields;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getDynamicFields;
|
@@ -1,11 +0,0 @@
|
|||||||
import checkLicense from '../../helpers/check-license.ee';
|
|
||||||
|
|
||||||
const getLicense = async () => {
|
|
||||||
const license = await checkLicense();
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: license ? 'ee' : 'ce',
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default getLicense;
|
|
@@ -0,0 +1,16 @@
|
|||||||
|
import appConfig from '../../config/app';
|
||||||
|
import Context from '../../types/express/context';
|
||||||
|
import Billing from '../../helpers/billing/index.ee';
|
||||||
|
|
||||||
|
const getPaymentPortalUrl = async (
|
||||||
|
_parent: unknown,
|
||||||
|
_params: unknown,
|
||||||
|
context: Context
|
||||||
|
) => {
|
||||||
|
if (!appConfig.isCloud) return;
|
||||||
|
|
||||||
|
const url = Billing.createPaymentPortalUrl(context.currentUser);
|
||||||
|
return { url };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getPaymentPortalUrl;
|
30
packages/backend/src/graphql/queries/get-usage-data.ee.ts
Normal file
30
packages/backend/src/graphql/queries/get-usage-data.ee.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import appConfig from '../../config/app';
|
||||||
|
import Context from '../../types/express/context';
|
||||||
|
|
||||||
|
const getUsageData = async (
|
||||||
|
_parent: unknown,
|
||||||
|
_params: unknown,
|
||||||
|
context: Context
|
||||||
|
) => {
|
||||||
|
if (!appConfig.isCloud) return;
|
||||||
|
|
||||||
|
const usageData = await context.currentUser
|
||||||
|
.$relatedQuery('usageData')
|
||||||
|
.throwIfNotFound();
|
||||||
|
|
||||||
|
const paymentPlan = await context.currentUser
|
||||||
|
.$relatedQuery('paymentPlan')
|
||||||
|
.throwIfNotFound();
|
||||||
|
|
||||||
|
const computedUsageData = {
|
||||||
|
name: paymentPlan.name,
|
||||||
|
allowedTaskCount: paymentPlan.taskCount,
|
||||||
|
consumedTaskCount: usageData.consumedTaskCount,
|
||||||
|
remainingTaskCount: paymentPlan.taskCount - usageData.consumedTaskCount,
|
||||||
|
nextResetAt: usageData.nextResetAt,
|
||||||
|
};
|
||||||
|
|
||||||
|
return computedUsageData;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getUsageData;
|
@@ -9,8 +9,11 @@ import getExecution from './queries/get-execution';
|
|||||||
import getExecutions from './queries/get-executions';
|
import getExecutions from './queries/get-executions';
|
||||||
import getExecutionSteps from './queries/get-execution-steps';
|
import getExecutionSteps from './queries/get-execution-steps';
|
||||||
import getDynamicData from './queries/get-dynamic-data';
|
import getDynamicData from './queries/get-dynamic-data';
|
||||||
|
import getDynamicFields from './queries/get-dynamic-fields';
|
||||||
import getCurrentUser from './queries/get-current-user';
|
import getCurrentUser from './queries/get-current-user';
|
||||||
import getLicense from './queries/get-license.ee';
|
import getUsageData from './queries/get-usage-data.ee';
|
||||||
|
import getPaymentPortalUrl from './queries/get-payment-portal-url.ee';
|
||||||
|
import getAutomatischInfo from './queries/get-automatisch-info';
|
||||||
import healthcheck from './queries/healthcheck';
|
import healthcheck from './queries/healthcheck';
|
||||||
|
|
||||||
const queryResolvers = {
|
const queryResolvers = {
|
||||||
@@ -25,8 +28,11 @@ const queryResolvers = {
|
|||||||
getExecutions,
|
getExecutions,
|
||||||
getExecutionSteps,
|
getExecutionSteps,
|
||||||
getDynamicData,
|
getDynamicData,
|
||||||
|
getDynamicFields,
|
||||||
getCurrentUser,
|
getCurrentUser,
|
||||||
getLicense,
|
getUsageData,
|
||||||
|
getPaymentPortalUrl,
|
||||||
|
getAutomatischInfo,
|
||||||
healthcheck,
|
healthcheck,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -28,8 +28,15 @@ type Query {
|
|||||||
key: String!
|
key: String!
|
||||||
parameters: JSONObject
|
parameters: JSONObject
|
||||||
): JSONObject
|
): JSONObject
|
||||||
|
getDynamicFields(
|
||||||
|
stepId: String!
|
||||||
|
key: String!
|
||||||
|
parameters: JSONObject
|
||||||
|
): [SubstepArgument]
|
||||||
getCurrentUser: User
|
getCurrentUser: User
|
||||||
getLicense: GetLicense
|
getUsageData: GetUsageData
|
||||||
|
getPaymentPortalUrl: GetPaymentPortalUrl
|
||||||
|
getAutomatischInfo: GetAutomatischInfo
|
||||||
healthcheck: AppHealth
|
healthcheck: AppHealth
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,6 +56,7 @@ type Mutation {
|
|||||||
updateStep(input: UpdateStepInput): Step
|
updateStep(input: UpdateStepInput): Step
|
||||||
deleteStep(input: DeleteStepInput): Step
|
deleteStep(input: DeleteStepInput): Step
|
||||||
createUser(input: CreateUserInput): User
|
createUser(input: CreateUserInput): User
|
||||||
|
deleteUser: Boolean
|
||||||
updateUser(input: UpdateUserInput): User
|
updateUser(input: UpdateUserInput): User
|
||||||
forgotPassword(input: ForgotPasswordInput): Boolean
|
forgotPassword(input: ForgotPasswordInput): Boolean
|
||||||
resetPassword(input: ResetPasswordInput): Boolean
|
resetPassword(input: ResetPasswordInput): Boolean
|
||||||
@@ -65,38 +73,64 @@ directive @specifiedBy(
|
|||||||
url: String!
|
url: String!
|
||||||
) on SCALAR
|
) on SCALAR
|
||||||
|
|
||||||
|
type Trigger {
|
||||||
|
name: String
|
||||||
|
key: String
|
||||||
|
description: String
|
||||||
|
pollInterval: Int
|
||||||
|
type: String
|
||||||
|
substeps: [Substep]
|
||||||
|
}
|
||||||
|
|
||||||
type Action {
|
type Action {
|
||||||
name: String
|
name: String
|
||||||
key: String
|
key: String
|
||||||
description: String
|
description: String
|
||||||
substeps: [ActionSubstep]
|
substeps: [Substep]
|
||||||
}
|
}
|
||||||
|
|
||||||
type ActionSubstep {
|
type Substep {
|
||||||
key: String
|
key: String
|
||||||
name: String
|
name: String
|
||||||
arguments: [ActionSubstepArgument]
|
arguments: [SubstepArgument]
|
||||||
}
|
}
|
||||||
|
|
||||||
type ActionSubstepArgument {
|
type SubstepArgument {
|
||||||
label: String
|
label: String
|
||||||
key: String
|
key: String
|
||||||
type: String
|
type: String
|
||||||
description: String
|
description: String
|
||||||
required: Boolean
|
required: Boolean
|
||||||
variables: Boolean
|
variables: Boolean
|
||||||
options: [ArgumentOption]
|
options: [SubstepArgumentOption]
|
||||||
source: ActionSubstepArgumentSource
|
source: SubstepArgumentSource
|
||||||
|
additionalFields: SubstepArgumentAdditionalFields
|
||||||
dependsOn: [String]
|
dependsOn: [String]
|
||||||
}
|
}
|
||||||
|
|
||||||
type ActionSubstepArgumentSource {
|
type SubstepArgumentOption {
|
||||||
type: String
|
label: String
|
||||||
name: String
|
value: JSONObject
|
||||||
arguments: [ActionSubstepArgumentSourceArgument]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ActionSubstepArgumentSourceArgument {
|
type SubstepArgumentSource {
|
||||||
|
type: String
|
||||||
|
name: String
|
||||||
|
arguments: [SubstepArgumentSourceArgument]
|
||||||
|
}
|
||||||
|
|
||||||
|
type SubstepArgumentSourceArgument {
|
||||||
|
name: String
|
||||||
|
value: String
|
||||||
|
}
|
||||||
|
|
||||||
|
type SubstepArgumentAdditionalFields {
|
||||||
|
type: String
|
||||||
|
name: String
|
||||||
|
arguments: [SubstepArgumentAdditionalFieldsArgument]
|
||||||
|
}
|
||||||
|
|
||||||
|
type SubstepArgumentAdditionalFieldsArgument {
|
||||||
name: String
|
name: String
|
||||||
value: String
|
value: String
|
||||||
}
|
}
|
||||||
@@ -198,7 +232,7 @@ type Field {
|
|||||||
description: String
|
description: String
|
||||||
docUrl: String
|
docUrl: String
|
||||||
clickToCopy: Boolean
|
clickToCopy: Boolean
|
||||||
options: [ArgumentOption]
|
options: [SubstepArgumentOption]
|
||||||
}
|
}
|
||||||
|
|
||||||
type FlowConnection {
|
type FlowConnection {
|
||||||
@@ -304,6 +338,7 @@ input DeleteStepInput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
input CreateUserInput {
|
input CreateUserInput {
|
||||||
|
fullName: String!
|
||||||
email: String!
|
email: String!
|
||||||
password: String!
|
password: String!
|
||||||
}
|
}
|
||||||
@@ -394,52 +429,11 @@ input StepInput {
|
|||||||
previousStep: PreviousStepInput
|
previousStep: PreviousStepInput
|
||||||
}
|
}
|
||||||
|
|
||||||
type Trigger {
|
|
||||||
name: String
|
|
||||||
key: String
|
|
||||||
description: String
|
|
||||||
pollInterval: Int
|
|
||||||
type: String
|
|
||||||
substeps: [TriggerSubstep]
|
|
||||||
}
|
|
||||||
|
|
||||||
type TriggerSubstep {
|
|
||||||
key: String
|
|
||||||
name: String
|
|
||||||
arguments: [TriggerSubstepArgument]
|
|
||||||
}
|
|
||||||
|
|
||||||
type TriggerSubstepArgument {
|
|
||||||
label: String
|
|
||||||
key: String
|
|
||||||
type: String
|
|
||||||
description: String
|
|
||||||
required: Boolean
|
|
||||||
variables: Boolean
|
|
||||||
source: TriggerSubstepArgumentSource
|
|
||||||
dependsOn: [String]
|
|
||||||
options: [ArgumentOption]
|
|
||||||
}
|
|
||||||
|
|
||||||
type TriggerSubstepArgumentSource {
|
|
||||||
type: String
|
|
||||||
name: String
|
|
||||||
arguments: [TriggerSubstepArgumentSourceArgument]
|
|
||||||
}
|
|
||||||
|
|
||||||
type ArgumentOption {
|
|
||||||
label: String
|
|
||||||
value: JSONObject
|
|
||||||
}
|
|
||||||
|
|
||||||
type TriggerSubstepArgumentSourceArgument {
|
|
||||||
name: String
|
|
||||||
value: String
|
|
||||||
}
|
|
||||||
|
|
||||||
type User {
|
type User {
|
||||||
id: String
|
id: String
|
||||||
|
fullName: String
|
||||||
email: String
|
email: String
|
||||||
|
role: String
|
||||||
createdAt: String
|
createdAt: String
|
||||||
updatedAt: String
|
updatedAt: String
|
||||||
}
|
}
|
||||||
@@ -471,8 +465,20 @@ type AppHealth {
|
|||||||
version: String
|
version: String
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetLicense {
|
type GetAutomatischInfo {
|
||||||
type: String
|
isCloud: Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetUsageData {
|
||||||
|
name: String
|
||||||
|
allowedTaskCount: Int
|
||||||
|
consumedTaskCount: Int
|
||||||
|
remainingTaskCount: Int
|
||||||
|
nextResetAt: String
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetPaymentPortalUrl {
|
||||||
|
url: String
|
||||||
}
|
}
|
||||||
|
|
||||||
schema {
|
schema {
|
||||||
|
@@ -24,6 +24,7 @@ const authentication = shield(
|
|||||||
{
|
{
|
||||||
Query: {
|
Query: {
|
||||||
'*': isAuthenticated,
|
'*': isAuthenticated,
|
||||||
|
getAutomatischInfo: allow,
|
||||||
healthcheck: allow,
|
healthcheck: allow,
|
||||||
},
|
},
|
||||||
Mutation: {
|
Mutation: {
|
||||||
@@ -31,6 +32,7 @@ const authentication = shield(
|
|||||||
login: allow,
|
login: allow,
|
||||||
createUser: allow,
|
createUser: allow,
|
||||||
forgotPassword: allow,
|
forgotPassword: allow,
|
||||||
|
resetPassword: allow,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
100
packages/backend/src/helpers/billing/index.ee.ts
Normal file
100
packages/backend/src/helpers/billing/index.ee.ts
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import Stripe from 'stripe';
|
||||||
|
import User from '../../models/user';
|
||||||
|
import PaymentPlan from '../../models/payment-plan.ee';
|
||||||
|
import UsageData from '../../models/usage-data.ee';
|
||||||
|
import appConfig from '../../config/app';
|
||||||
|
import handleWebhooks from './webhooks.ee';
|
||||||
|
|
||||||
|
const plans = [
|
||||||
|
{
|
||||||
|
price: appConfig.stripeStarterPriceKey,
|
||||||
|
name: 'Starter',
|
||||||
|
taskCount: 1000,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
price: appConfig.stripeGrowthPriceKey,
|
||||||
|
name: 'Growth',
|
||||||
|
taskCount: 10000,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const stripe = new Stripe(appConfig.stripeSecretKey, {
|
||||||
|
apiVersion: '2022-11-15',
|
||||||
|
});
|
||||||
|
|
||||||
|
const createStripeCustomer = async (user: User) => {
|
||||||
|
const params: Stripe.CustomerCreateParams = {
|
||||||
|
email: user.email,
|
||||||
|
name: user.fullName,
|
||||||
|
description: `User ID: ${user.id}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
return await stripe.customers.create(params);
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultPlan = plans.find((plan) => plan.default);
|
||||||
|
|
||||||
|
const createStripeSubscription = async (
|
||||||
|
user: User,
|
||||||
|
stripeCustomer: Stripe.Customer
|
||||||
|
) => {
|
||||||
|
const params: Stripe.SubscriptionCreateParams = {
|
||||||
|
customer: stripeCustomer.id,
|
||||||
|
items: [{ price: defaultPlan.price }],
|
||||||
|
};
|
||||||
|
|
||||||
|
return await stripe.subscriptions.create(params);
|
||||||
|
};
|
||||||
|
|
||||||
|
const createSubscription = async (user: User) => {
|
||||||
|
const stripeCustomer = await createStripeCustomer(user);
|
||||||
|
const stripeSubscription = await createStripeSubscription(
|
||||||
|
user,
|
||||||
|
stripeCustomer
|
||||||
|
);
|
||||||
|
|
||||||
|
await PaymentPlan.query().insert({
|
||||||
|
name: defaultPlan.name,
|
||||||
|
taskCount: defaultPlan.taskCount,
|
||||||
|
userId: user.id,
|
||||||
|
stripeCustomerId: stripeCustomer.id,
|
||||||
|
stripeSubscriptionId: stripeSubscription.id,
|
||||||
|
currentPeriodStartedAt: new Date(
|
||||||
|
stripeSubscription.current_period_start * 1000
|
||||||
|
).toISOString(),
|
||||||
|
currentPeriodEndsAt: new Date(
|
||||||
|
stripeSubscription.current_period_end * 1000
|
||||||
|
).toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
await UsageData.query().insert({
|
||||||
|
userId: user.id,
|
||||||
|
consumedTaskCount: 0,
|
||||||
|
nextResetAt: new Date(
|
||||||
|
stripeSubscription.current_period_end * 1000
|
||||||
|
).toISOString(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const createPaymentPortalUrl = async (user: User) => {
|
||||||
|
const paymentPlan = await user.$relatedQuery('paymentPlan');
|
||||||
|
|
||||||
|
const userSession = await stripe.billingPortal.sessions.create({
|
||||||
|
customer: paymentPlan.stripeCustomerId,
|
||||||
|
return_url: 'https://cloud.automatisch.io',
|
||||||
|
});
|
||||||
|
|
||||||
|
return userSession.url;
|
||||||
|
};
|
||||||
|
|
||||||
|
const billing = {
|
||||||
|
createSubscription,
|
||||||
|
createPaymentPortalUrl,
|
||||||
|
handleWebhooks,
|
||||||
|
stripe,
|
||||||
|
plans,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default billing;
|
42
packages/backend/src/helpers/billing/webhooks.ee.ts
Normal file
42
packages/backend/src/helpers/billing/webhooks.ee.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import Stripe from 'stripe';
|
||||||
|
import PaymentPlan from '../../models/payment-plan.ee';
|
||||||
|
import Billing from './index.ee';
|
||||||
|
|
||||||
|
const handleWebhooks = async (event: Stripe.Event) => {
|
||||||
|
const trackedWebhookTypes = [
|
||||||
|
'customer.subscription.created',
|
||||||
|
'customer.subscription.updated',
|
||||||
|
'customer.subscription.deleted',
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!trackedWebhookTypes.includes(event.type)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await updatePaymentPlan(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatePaymentPlan = async (event: Stripe.Event) => {
|
||||||
|
const subscription = event.data.object as Stripe.Subscription;
|
||||||
|
const priceKey = subscription.items.data[0].plan.id;
|
||||||
|
const plan = Billing.plans.find((plan) => plan.price === priceKey);
|
||||||
|
|
||||||
|
const paymentPlan = await PaymentPlan.query().findOne({
|
||||||
|
stripe_customer_id: subscription.customer,
|
||||||
|
});
|
||||||
|
|
||||||
|
await paymentPlan.$query().patchAndFetch({
|
||||||
|
name: plan.name,
|
||||||
|
taskCount: plan.taskCount,
|
||||||
|
stripeSubscriptionId: subscription.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const user = await paymentPlan.$relatedQuery('user');
|
||||||
|
const usageData = await user.$relatedQuery('usageData');
|
||||||
|
|
||||||
|
await usageData.$query().patchAndFetch({
|
||||||
|
nextResetAt: new Date(subscription.current_period_end * 1000).toISOString(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default handleWebhooks;
|
@@ -38,6 +38,13 @@ export default function computeParameters(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
[key]: value.map(item => computeParameters(item, executionSteps)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...result,
|
...result,
|
||||||
[key]: value,
|
[key]: value,
|
||||||
|
@@ -4,6 +4,8 @@ import { BullMQAdapter } from '@bull-board/api/bullMQAdapter';
|
|||||||
import flowQueue from '../queues/flow';
|
import flowQueue from '../queues/flow';
|
||||||
import triggerQueue from '../queues/trigger';
|
import triggerQueue from '../queues/trigger';
|
||||||
import actionQueue from '../queues/action';
|
import actionQueue from '../queues/action';
|
||||||
|
import emailQueue from '../queues/email';
|
||||||
|
import deleteUserQueue from '../queues/delete-user.ee';
|
||||||
import appConfig from '../config/app';
|
import appConfig from '../config/app';
|
||||||
|
|
||||||
const serverAdapter = new ExpressAdapter();
|
const serverAdapter = new ExpressAdapter();
|
||||||
@@ -21,6 +23,8 @@ const createBullBoardHandler = async (serverAdapter: ExpressAdapter) => {
|
|||||||
new BullMQAdapter(flowQueue),
|
new BullMQAdapter(flowQueue),
|
||||||
new BullMQAdapter(triggerQueue),
|
new BullMQAdapter(triggerQueue),
|
||||||
new BullMQAdapter(actionQueue),
|
new BullMQAdapter(actionQueue),
|
||||||
|
new BullMQAdapter(emailQueue),
|
||||||
|
new BullMQAdapter(deleteUserQueue),
|
||||||
],
|
],
|
||||||
serverAdapter: serverAdapter,
|
serverAdapter: serverAdapter,
|
||||||
});
|
});
|
||||||
|
@@ -13,6 +13,7 @@ import {
|
|||||||
IRequest,
|
IRequest,
|
||||||
} from '@automatisch/types';
|
} from '@automatisch/types';
|
||||||
import EarlyExitError from '../errors/early-exit';
|
import EarlyExitError from '../errors/early-exit';
|
||||||
|
import AlreadyProcessedError from '../errors/already-processed';
|
||||||
|
|
||||||
type GlobalVariableOptions = {
|
type GlobalVariableOptions = {
|
||||||
connection?: Connection;
|
connection?: Connection;
|
||||||
@@ -77,6 +78,9 @@ const globalVariable = async (
|
|||||||
execution: {
|
execution: {
|
||||||
id: execution?.id,
|
id: execution?.id,
|
||||||
testRun,
|
testRun,
|
||||||
|
exit: () => {
|
||||||
|
throw new EarlyExitError();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
lastExecutionStep: (await step?.getLastExecutionStep())?.toJSON(),
|
lastExecutionStep: (await step?.getLastExecutionStep())?.toJSON(),
|
||||||
triggerOutput: {
|
triggerOutput: {
|
||||||
@@ -93,7 +97,7 @@ const globalVariable = async (
|
|||||||
!$.execution.testRun
|
!$.execution.testRun
|
||||||
) {
|
) {
|
||||||
// early exit as we do not want to process duplicate items in actual executions
|
// early exit as we do not want to process duplicate items in actual executions
|
||||||
throw new EarlyExitError();
|
throw new AlreadyProcessedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
$.triggerOutput.data.push(triggerItem);
|
$.triggerOutput.data.push(triggerItem);
|
||||||
|
@@ -28,7 +28,7 @@ const graphQLInstance = graphqlHTTP({
|
|||||||
delete (error.originalError as HttpError).response;
|
delete (error.originalError as HttpError).response;
|
||||||
}
|
}
|
||||||
|
|
||||||
return error.originalError;
|
return error;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -40,8 +40,9 @@ export default function createHttpClient({
|
|||||||
instance.interceptors.response.use(
|
instance.interceptors.response.use(
|
||||||
(response) => response,
|
(response) => response,
|
||||||
async (error) => {
|
async (error) => {
|
||||||
const { config } = error;
|
const { config, response } = error;
|
||||||
const { status } = error.response;
|
// Do not destructure `status` from `error.response` because it might not exist
|
||||||
|
const status = response?.status;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
// TODO: provide a `shouldRefreshToken` function in the app
|
// TODO: provide a `shouldRefreshToken` function in the app
|
||||||
|
@@ -1,9 +1,10 @@
|
|||||||
import type { QueryContext } from 'objection';
|
import type { QueryContext } from 'objection';
|
||||||
|
import { IJSONObject } from '@automatisch/types';
|
||||||
|
import appConfig from '../config/app';
|
||||||
import Base from './base';
|
import Base from './base';
|
||||||
import Execution from './execution';
|
import Execution from './execution';
|
||||||
import Step from './step';
|
import Step from './step';
|
||||||
import Telemetry from '../helpers/telemetry';
|
import Telemetry from '../helpers/telemetry';
|
||||||
import { IJSONObject } from '@automatisch/types';
|
|
||||||
|
|
||||||
class ExecutionStep extends Base {
|
class ExecutionStep extends Base {
|
||||||
id!: string;
|
id!: string;
|
||||||
@@ -14,6 +15,7 @@ class ExecutionStep extends Base {
|
|||||||
errorDetails: IJSONObject;
|
errorDetails: IJSONObject;
|
||||||
status: 'success' | 'failure';
|
status: 'success' | 'failure';
|
||||||
step: Step;
|
step: Step;
|
||||||
|
execution?: Execution;
|
||||||
|
|
||||||
static tableName = 'execution_steps';
|
static tableName = 'execution_steps';
|
||||||
|
|
||||||
@@ -57,6 +59,18 @@ class ExecutionStep extends Base {
|
|||||||
async $afterInsert(queryContext: QueryContext) {
|
async $afterInsert(queryContext: QueryContext) {
|
||||||
await super.$afterInsert(queryContext);
|
await super.$afterInsert(queryContext);
|
||||||
Telemetry.executionStepCreated(this);
|
Telemetry.executionStepCreated(this);
|
||||||
|
|
||||||
|
if (appConfig.isCloud) {
|
||||||
|
const execution = await this.$relatedQuery('execution');
|
||||||
|
|
||||||
|
if (!execution.testRun && !this.isFailed) {
|
||||||
|
const flow = await execution.$relatedQuery('flow');
|
||||||
|
const user = await flow.$relatedQuery('user');
|
||||||
|
const usageData = await user.$relatedQuery('usageData');
|
||||||
|
|
||||||
|
await usageData.increaseConsumedTaskCountByOne();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -10,6 +10,7 @@ class Execution extends Base {
|
|||||||
testRun: boolean;
|
testRun: boolean;
|
||||||
internalId: string;
|
internalId: string;
|
||||||
executionSteps: ExecutionStep[];
|
executionSteps: ExecutionStep[];
|
||||||
|
flow?: Flow;
|
||||||
|
|
||||||
static tableName = 'executions';
|
static tableName = 'executions';
|
||||||
|
|
||||||
|
@@ -3,6 +3,7 @@ import type { ModelOptions, QueryContext } from 'objection';
|
|||||||
import ExtendedQueryBuilder from './query-builder';
|
import ExtendedQueryBuilder from './query-builder';
|
||||||
import Base from './base';
|
import Base from './base';
|
||||||
import Step from './step';
|
import Step from './step';
|
||||||
|
import User from './user';
|
||||||
import Execution from './execution';
|
import Execution from './execution';
|
||||||
import Telemetry from '../helpers/telemetry';
|
import Telemetry from '../helpers/telemetry';
|
||||||
|
|
||||||
@@ -15,6 +16,7 @@ class Flow extends Base {
|
|||||||
published_at: string;
|
published_at: string;
|
||||||
remoteWebhookId: string;
|
remoteWebhookId: string;
|
||||||
executions?: Execution[];
|
executions?: Execution[];
|
||||||
|
user?: User;
|
||||||
|
|
||||||
static tableName = 'flows';
|
static tableName = 'flows';
|
||||||
|
|
||||||
@@ -51,6 +53,14 @@ class Flow extends Base {
|
|||||||
to: 'executions.flow_id',
|
to: 'executions.flow_id',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
user: {
|
||||||
|
relation: Base.HasOneRelation,
|
||||||
|
modelClass: User,
|
||||||
|
join: {
|
||||||
|
from: 'flows.user_id',
|
||||||
|
to: 'users.id',
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
async lastInternalId() {
|
async lastInternalId() {
|
||||||
|
53
packages/backend/src/models/payment-plan.ee.ts
Normal file
53
packages/backend/src/models/payment-plan.ee.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import Base from './base';
|
||||||
|
import User from './user';
|
||||||
|
|
||||||
|
class PaymentPlan extends Base {
|
||||||
|
id!: string;
|
||||||
|
name!: string;
|
||||||
|
taskCount: number;
|
||||||
|
userId!: string;
|
||||||
|
stripeCustomerId!: string;
|
||||||
|
stripeSubscriptionId!: string;
|
||||||
|
currentPeriodStartedAt!: string;
|
||||||
|
currentPeriodEndsAt!: string;
|
||||||
|
user?: User;
|
||||||
|
|
||||||
|
static tableName = 'payment_plans';
|
||||||
|
|
||||||
|
static jsonSchema = {
|
||||||
|
type: 'object',
|
||||||
|
required: [
|
||||||
|
'name',
|
||||||
|
'taskCount',
|
||||||
|
'userId',
|
||||||
|
'stripeCustomerId',
|
||||||
|
'stripeSubscriptionId',
|
||||||
|
'currentPeriodStartedAt',
|
||||||
|
'currentPeriodEndsAt',
|
||||||
|
],
|
||||||
|
|
||||||
|
properties: {
|
||||||
|
id: { type: 'string', format: 'uuid' },
|
||||||
|
name: { type: 'string' },
|
||||||
|
taskCount: { type: 'integer' },
|
||||||
|
userId: { type: 'string', format: 'uuid' },
|
||||||
|
stripeCustomerId: { type: 'string' },
|
||||||
|
stripeSubscriptionId: { type: 'string' },
|
||||||
|
currentPeriodStartedAt: { type: 'string' },
|
||||||
|
currentPeriodEndsAt: { type: 'string' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
static relationMappings = () => ({
|
||||||
|
user: {
|
||||||
|
relation: Base.BelongsToOneRelation,
|
||||||
|
modelClass: User,
|
||||||
|
join: {
|
||||||
|
from: 'payment_plans.user_id',
|
||||||
|
to: 'users.id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PaymentPlan;
|
@@ -149,6 +149,22 @@ class Step extends Base {
|
|||||||
|
|
||||||
return command;
|
return command;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getSetupFields() {
|
||||||
|
let setupSupsteps;
|
||||||
|
|
||||||
|
if (this.isTrigger) {
|
||||||
|
setupSupsteps = (await this.getTriggerCommand()).substeps;
|
||||||
|
} else {
|
||||||
|
setupSupsteps = (await this.getActionCommand()).substeps;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingArguments = setupSupsteps.find(
|
||||||
|
(substep) => substep.key === 'chooseTrigger'
|
||||||
|
).arguments;
|
||||||
|
|
||||||
|
return existingArguments;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Step;
|
export default Step;
|
||||||
|
41
packages/backend/src/models/usage-data.ee.ts
Normal file
41
packages/backend/src/models/usage-data.ee.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { raw } from 'objection';
|
||||||
|
import Base from './base';
|
||||||
|
import User from './user';
|
||||||
|
|
||||||
|
class UsageData extends Base {
|
||||||
|
id!: string;
|
||||||
|
userId!: string;
|
||||||
|
consumedTaskCount!: number;
|
||||||
|
nextResetAt!: string;
|
||||||
|
|
||||||
|
static tableName = 'usage_data';
|
||||||
|
|
||||||
|
static jsonSchema = {
|
||||||
|
type: 'object',
|
||||||
|
required: ['userId', 'consumedTaskCount', 'nextResetAt'],
|
||||||
|
|
||||||
|
properties: {
|
||||||
|
id: { type: 'string', format: 'uuid' },
|
||||||
|
userId: { type: 'string', format: 'uuid' },
|
||||||
|
consumedTaskCount: { type: 'integer' },
|
||||||
|
nextResetAt: { type: 'string' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
static relationMappings = () => ({
|
||||||
|
user: {
|
||||||
|
relation: Base.BelongsToOneRelation,
|
||||||
|
modelClass: User,
|
||||||
|
join: {
|
||||||
|
from: 'usage_data.user_id',
|
||||||
|
to: 'users.id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
async increaseConsumedTaskCountByOne() {
|
||||||
|
return await this.$query().patch({ consumedTaskCount: raw('consumed_task_count + 1') });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UsageData;
|
@@ -6,9 +6,12 @@ import Step from './step';
|
|||||||
import Execution from './execution';
|
import Execution from './execution';
|
||||||
import bcrypt from 'bcrypt';
|
import bcrypt from 'bcrypt';
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
|
import PaymentPlan from './payment-plan.ee';
|
||||||
|
import UsageData from './usage-data.ee';
|
||||||
|
|
||||||
class User extends Base {
|
class User extends Base {
|
||||||
id!: string;
|
id!: string;
|
||||||
|
fullName!: string;
|
||||||
email!: string;
|
email!: string;
|
||||||
password!: string;
|
password!: string;
|
||||||
role: string;
|
role: string;
|
||||||
@@ -18,15 +21,18 @@ class User extends Base {
|
|||||||
flows?: Flow[];
|
flows?: Flow[];
|
||||||
steps?: Step[];
|
steps?: Step[];
|
||||||
executions?: Execution[];
|
executions?: Execution[];
|
||||||
|
paymentPlan?: PaymentPlan;
|
||||||
|
usageData?: UsageData;
|
||||||
|
|
||||||
static tableName = 'users';
|
static tableName = 'users';
|
||||||
|
|
||||||
static jsonSchema = {
|
static jsonSchema = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
required: ['email', 'password'],
|
required: ['fullName', 'email', 'password'],
|
||||||
|
|
||||||
properties: {
|
properties: {
|
||||||
id: { type: 'string', format: 'uuid' },
|
id: { type: 'string', format: 'uuid' },
|
||||||
|
fullName: { type: 'string', minLength: 1 },
|
||||||
email: { type: 'string', format: 'email', minLength: 1, maxLength: 255 },
|
email: { type: 'string', format: 'email', minLength: 1, maxLength: 255 },
|
||||||
password: { type: 'string', minLength: 1, maxLength: 255 },
|
password: { type: 'string', minLength: 1, maxLength: 255 },
|
||||||
role: { type: 'string', enum: ['admin', 'user'] },
|
role: { type: 'string', enum: ['admin', 'user'] },
|
||||||
@@ -74,6 +80,22 @@ class User extends Base {
|
|||||||
to: 'executions.flow_id',
|
to: 'executions.flow_id',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
paymentPlan: {
|
||||||
|
relation: Base.HasOneRelation,
|
||||||
|
modelClass: PaymentPlan,
|
||||||
|
join: {
|
||||||
|
from: 'payment_plans.user_id',
|
||||||
|
to: 'users.id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
usageData: {
|
||||||
|
relation: Base.HasOneRelation,
|
||||||
|
modelClass: UsageData,
|
||||||
|
join: {
|
||||||
|
from: 'usage_data.user_id',
|
||||||
|
to: 'users.id',
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
login(password: string) {
|
login(password: string) {
|
||||||
|
25
packages/backend/src/queues/delete-user.ee.ts
Normal file
25
packages/backend/src/queues/delete-user.ee.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import process from 'process';
|
||||||
|
import { Queue } from 'bullmq';
|
||||||
|
import redisConfig from '../config/redis';
|
||||||
|
import logger from '../helpers/logger';
|
||||||
|
|
||||||
|
const CONNECTION_REFUSED = 'ECONNREFUSED';
|
||||||
|
|
||||||
|
const redisConnection = {
|
||||||
|
connection: redisConfig,
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteUserQueue = new Queue('delete-user', redisConnection);
|
||||||
|
|
||||||
|
process.on('SIGTERM', async () => {
|
||||||
|
await deleteUserQueue.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
deleteUserQueue.on('error', (err) => {
|
||||||
|
if ((err as any).code === CONNECTION_REFUSED) {
|
||||||
|
logger.error('Make sure you have installed Redis and it is running.', err);
|
||||||
|
process.exit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default deleteUserQueue;
|
@@ -1,10 +1,12 @@
|
|||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
import graphQLInstance from '../helpers/graphql-instance';
|
import graphQLInstance from '../helpers/graphql-instance';
|
||||||
import webhooksRouter from './webhooks';
|
import webhooksRouter from './webhooks';
|
||||||
|
import stripeRouter from './stripe.ee';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
router.use('/graphql', graphQLInstance);
|
router.use('/graphql', graphQLInstance);
|
||||||
router.use('/webhooks', webhooksRouter);
|
router.use('/webhooks', webhooksRouter);
|
||||||
|
router.use('/stripe', stripeRouter);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
23
packages/backend/src/routes/stripe.ee.ts
Normal file
23
packages/backend/src/routes/stripe.ee.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import express, { Router } from 'express';
|
||||||
|
import multer from 'multer';
|
||||||
|
import { IRequest } from '@automatisch/types';
|
||||||
|
import appConfig from '../config/app';
|
||||||
|
import stripeWebhooksAction from '../controllers/stripe/webhooks.ee';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
const upload = multer();
|
||||||
|
|
||||||
|
router.use(upload.none());
|
||||||
|
|
||||||
|
router.use(
|
||||||
|
express.text({
|
||||||
|
limit: appConfig.requestBodySizeLimit,
|
||||||
|
verify(req, res, buf) {
|
||||||
|
(req as IRequest).rawBody = buf;
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post('/webhooks', stripeWebhooksAction);
|
||||||
|
|
||||||
|
export default router;
|
@@ -5,6 +5,8 @@ import ExecutionStep from '../models/execution-step';
|
|||||||
import computeParameters from '../helpers/compute-parameters';
|
import computeParameters from '../helpers/compute-parameters';
|
||||||
import globalVariable from '../helpers/global-variable';
|
import globalVariable from '../helpers/global-variable';
|
||||||
import HttpError from '../errors/http';
|
import HttpError from '../errors/http';
|
||||||
|
import EarlyExitError from '../errors/early-exit';
|
||||||
|
import AlreadyProcessedError from '../errors/already-processed';
|
||||||
|
|
||||||
type ProcessActionOptions = {
|
type ProcessActionOptions = {
|
||||||
flowId: string;
|
flowId: string;
|
||||||
@@ -44,13 +46,19 @@ export const processAction = async (options: ProcessActionOptions) => {
|
|||||||
try {
|
try {
|
||||||
await actionCommand.run($);
|
await actionCommand.run($);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof HttpError) {
|
const shouldEarlyExit = error instanceof EarlyExitError;
|
||||||
$.actionOutput.error = error.details;
|
const shouldNotProcess = error instanceof AlreadyProcessedError;
|
||||||
} else {
|
const shouldNotConsiderAsError = shouldEarlyExit || shouldNotProcess;
|
||||||
try {
|
|
||||||
$.actionOutput.error = JSON.parse(error.message);
|
if (!shouldNotConsiderAsError) {
|
||||||
} catch {
|
if (error instanceof HttpError) {
|
||||||
$.actionOutput.error = { error: error.message };
|
$.actionOutput.error = error.details;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
$.actionOutput.error = JSON.parse(error.message);
|
||||||
|
} catch {
|
||||||
|
$.actionOutput.error = { error: error.message };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import Flow from '../models/flow';
|
import Flow from '../models/flow';
|
||||||
import globalVariable from '../helpers/global-variable';
|
import globalVariable from '../helpers/global-variable';
|
||||||
import EarlyExitError from '../errors/early-exit';
|
import EarlyExitError from '../errors/early-exit';
|
||||||
|
import AlreadyProcessedError from '../errors/already-processed';
|
||||||
import HttpError from '../errors/http';
|
import HttpError from '../errors/http';
|
||||||
|
|
||||||
type ProcessFlowOptions = {
|
type ProcessFlowOptions = {
|
||||||
@@ -29,7 +30,11 @@ export const processFlow = async (options: ProcessFlowOptions) => {
|
|||||||
await triggerCommand.run($);
|
await triggerCommand.run($);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof EarlyExitError === false) {
|
const shouldEarlyExit = error instanceof EarlyExitError;
|
||||||
|
const shouldNotProcess = error instanceof AlreadyProcessedError;
|
||||||
|
const shouldNotConsiderAsError = shouldEarlyExit || shouldNotProcess;
|
||||||
|
|
||||||
|
if (!shouldNotConsiderAsError) {
|
||||||
if (error instanceof HttpError) {
|
if (error instanceof HttpError) {
|
||||||
$.triggerOutput.error = error.details;
|
$.triggerOutput.error = error.details;
|
||||||
} else {
|
} else {
|
||||||
|
@@ -1,16 +1,23 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>Title</title>
|
<title>Reset password instructions</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
Hello {{ email }}
|
<p>
|
||||||
|
Hello {{ fullName }},
|
||||||
|
</p>
|
||||||
|
|
||||||
Someone has requested a link to change your password, and you can do this through the link below.
|
<p>
|
||||||
|
Someone has requested a link to change your password, and you can do this through the link below.
|
||||||
|
</p>
|
||||||
|
|
||||||
<a href="/reset-password">Change my password</a>
|
<p>
|
||||||
|
<a href="{{ webAppUrl }}/reset-password?token={{ token }}">Change my password</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
If you didn't request this, please ignore this email.
|
<p>
|
||||||
Your password won't change until you access the link above and create a new one.
|
If you didn't request this, please ignore this email. Your password won't change until you access the link above and create a new one.
|
||||||
|
</p>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@@ -4,6 +4,7 @@ import './workers/flow';
|
|||||||
import './workers/trigger';
|
import './workers/trigger';
|
||||||
import './workers/action';
|
import './workers/action';
|
||||||
import './workers/email';
|
import './workers/email';
|
||||||
|
import './workers/delete-user.ee';
|
||||||
import telemetry from './helpers/telemetry';
|
import telemetry from './helpers/telemetry';
|
||||||
|
|
||||||
telemetry.setServiceType('worker');
|
telemetry.setServiceType('worker');
|
||||||
|
@@ -21,7 +21,7 @@ const DEFAULT_DELAY_DURATION = 0;
|
|||||||
export const worker = new Worker(
|
export const worker = new Worker(
|
||||||
'action',
|
'action',
|
||||||
async (job) => {
|
async (job) => {
|
||||||
const { stepId, flowId, executionId, computedParameters } = await processAction(
|
const { stepId, flowId, executionId, computedParameters, executionStep } = await processAction(
|
||||||
job.data as JobData
|
job.data as JobData
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -48,6 +48,10 @@ export const worker = new Worker(
|
|||||||
jobOptions.delay = delayAsMilliseconds(step.key, computedParameters);
|
jobOptions.delay = delayAsMilliseconds(step.key, computedParameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (step.appKey === 'filter' && !executionStep.dataOut) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await actionQueue.add(jobName, jobPayload, jobOptions);
|
await actionQueue.add(jobName, jobPayload, jobOptions);
|
||||||
},
|
},
|
||||||
{ connection: redisConfig }
|
{ connection: redisConfig }
|
||||||
|
44
packages/backend/src/workers/delete-user.ee.ts
Normal file
44
packages/backend/src/workers/delete-user.ee.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { Worker } from 'bullmq';
|
||||||
|
import redisConfig from '../config/redis';
|
||||||
|
import logger from '../helpers/logger';
|
||||||
|
import User from '../models/user';
|
||||||
|
import Execution from '../models/execution';
|
||||||
|
import ExecutionStep from '../models/execution-step';
|
||||||
|
|
||||||
|
export const worker = new Worker(
|
||||||
|
'delete-user',
|
||||||
|
async (job) => {
|
||||||
|
const { id } = job.data;
|
||||||
|
|
||||||
|
const user = await User.query().findById(id).throwIfNotFound();
|
||||||
|
|
||||||
|
const executionIds = (
|
||||||
|
await user.$relatedQuery('executions').select('executions.id')
|
||||||
|
).map((execution: Execution) => execution.id);
|
||||||
|
|
||||||
|
await ExecutionStep.query().hardDelete().whereIn('execution_id', executionIds);
|
||||||
|
await user.$relatedQuery('executions').hardDelete();
|
||||||
|
await user.$relatedQuery('steps').hardDelete();
|
||||||
|
await user.$relatedQuery('flows').hardDelete();
|
||||||
|
await user.$relatedQuery('connections').hardDelete();
|
||||||
|
|
||||||
|
await user.$query().hardDelete();
|
||||||
|
},
|
||||||
|
{ connection: redisConfig }
|
||||||
|
);
|
||||||
|
|
||||||
|
worker.on('completed', (job) => {
|
||||||
|
logger.info(
|
||||||
|
`JOB ID: ${job.id} - The user with the ID of '${job.data.id}' has been deleted!`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
worker.on('failed', (job, err) => {
|
||||||
|
logger.info(
|
||||||
|
`JOB ID: ${job.id} - The user with the ID of '${job.data.id}' has failed to be deleted! ${err.message}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('SIGTERM', async () => {
|
||||||
|
await worker.close();
|
||||||
|
});
|
@@ -8,13 +8,13 @@ import appConfig from '../config/app';
|
|||||||
export const worker = new Worker(
|
export const worker = new Worker(
|
||||||
'email',
|
'email',
|
||||||
async (job) => {
|
async (job) => {
|
||||||
const { email, subject, templateName, params } = job.data;
|
const { email, subject, template, params } = job.data;
|
||||||
|
|
||||||
await mailer.sendMail({
|
await mailer.sendMail({
|
||||||
to: email,
|
to: email,
|
||||||
from: appConfig.fromEmail,
|
from: appConfig.fromEmail,
|
||||||
subject: subject,
|
subject: subject,
|
||||||
html: compileEmail(templateName, params),
|
html: compileEmail(template, params),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
{ connection: redisConfig }
|
{ connection: redisConfig }
|
||||||
|
16
packages/types/index.d.ts
vendored
16
packages/types/index.d.ts
vendored
@@ -82,6 +82,7 @@ export interface IFlow {
|
|||||||
|
|
||||||
export interface IUser {
|
export interface IUser {
|
||||||
id: string;
|
id: string;
|
||||||
|
fullName: string;
|
||||||
email: string;
|
email: string;
|
||||||
password: string;
|
password: string;
|
||||||
connections: IConnection[];
|
connections: IConnection[];
|
||||||
@@ -104,6 +105,7 @@ export interface IFieldDropdown {
|
|||||||
dependsOn?: string[];
|
dependsOn?: string[];
|
||||||
options?: IFieldDropdownOption[];
|
options?: IFieldDropdownOption[];
|
||||||
source?: IFieldDropdownSource;
|
source?: IFieldDropdownSource;
|
||||||
|
additionalFields?: IFieldDropdownAdditionalFields;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IFieldDropdownSource {
|
export interface IFieldDropdownSource {
|
||||||
@@ -114,6 +116,14 @@ export interface IFieldDropdownSource {
|
|||||||
value: string;
|
value: string;
|
||||||
}[];
|
}[];
|
||||||
}
|
}
|
||||||
|
export interface IFieldDropdownAdditionalFields {
|
||||||
|
type: string;
|
||||||
|
name: string;
|
||||||
|
arguments: {
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface IFieldDropdownOption {
|
export interface IFieldDropdownOption {
|
||||||
label: string;
|
label: string;
|
||||||
@@ -167,6 +177,7 @@ export interface IApp {
|
|||||||
flowCount?: number;
|
flowCount?: number;
|
||||||
beforeRequest?: TBeforeRequest[];
|
beforeRequest?: TBeforeRequest[];
|
||||||
dynamicData?: IDynamicData;
|
dynamicData?: IDynamicData;
|
||||||
|
dynamicFields?: IDynamicFields;
|
||||||
triggers?: ITrigger[];
|
triggers?: ITrigger[];
|
||||||
actions?: IAction[];
|
actions?: IAction[];
|
||||||
connections?: IConnection[];
|
connections?: IConnection[];
|
||||||
@@ -180,6 +191,10 @@ export interface IDynamicData {
|
|||||||
[index: string]: any;
|
[index: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IDynamicFields {
|
||||||
|
[index: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IAuth {
|
export interface IAuth {
|
||||||
generateAuthUrl?($: IGlobalVariable): Promise<void>;
|
generateAuthUrl?($: IGlobalVariable): Promise<void>;
|
||||||
verifyCredentials?($: IGlobalVariable): Promise<void>;
|
verifyCredentials?($: IGlobalVariable): Promise<void>;
|
||||||
@@ -296,6 +311,7 @@ export type IGlobalVariable = {
|
|||||||
execution?: {
|
execution?: {
|
||||||
id: string;
|
id: string;
|
||||||
testRun: boolean;
|
testRun: boolean;
|
||||||
|
exit: () => void;
|
||||||
};
|
};
|
||||||
lastExecutionStep?: IExecutionStep;
|
lastExecutionStep?: IExecutionStep;
|
||||||
webhookUrl?: string;
|
webhookUrl?: string;
|
||||||
|
@@ -9,9 +9,9 @@
|
|||||||
"@emotion/react": "^11.4.1",
|
"@emotion/react": "^11.4.1",
|
||||||
"@emotion/styled": "^11.3.0",
|
"@emotion/styled": "^11.3.0",
|
||||||
"@hookform/resolvers": "^2.8.8",
|
"@hookform/resolvers": "^2.8.8",
|
||||||
"@mui/icons-material": "^5.0.1",
|
"@mui/icons-material": "^5.11.9",
|
||||||
"@mui/lab": "^5.0.0-alpha.60",
|
"@mui/lab": "^5.0.0-alpha.120",
|
||||||
"@mui/material": "^5.0.2",
|
"@mui/material": "^5.11.10",
|
||||||
"@testing-library/jest-dom": "^5.11.4",
|
"@testing-library/jest-dom": "^5.11.4",
|
||||||
"@testing-library/react": "^11.1.0",
|
"@testing-library/react": "^11.1.0",
|
||||||
"@testing-library/user-event": "^12.1.10",
|
"@testing-library/user-event": "^12.1.10",
|
||||||
@@ -21,6 +21,7 @@
|
|||||||
"@types/node": "^12.0.0",
|
"@types/node": "^12.0.0",
|
||||||
"@types/react": "^17.0.0",
|
"@types/react": "^17.0.0",
|
||||||
"@types/react-dom": "^17.0.0",
|
"@types/react-dom": "^17.0.0",
|
||||||
|
"@types/uuid": "^9.0.0",
|
||||||
"clipboard-copy": "^4.0.1",
|
"clipboard-copy": "^4.0.1",
|
||||||
"compare-versions": "^4.1.3",
|
"compare-versions": "^4.1.3",
|
||||||
"graphql": "^15.6.0",
|
"graphql": "^15.6.0",
|
||||||
@@ -38,6 +39,7 @@
|
|||||||
"slate-history": "^0.66.0",
|
"slate-history": "^0.66.0",
|
||||||
"slate-react": "^0.72.9",
|
"slate-react": "^0.72.9",
|
||||||
"typescript": "^4.6.3",
|
"typescript": "^4.6.3",
|
||||||
|
"uuid": "^9.0.0",
|
||||||
"web-vitals": "^1.0.1",
|
"web-vitals": "^1.0.1",
|
||||||
"yup": "^0.32.11"
|
"yup": "^0.32.11"
|
||||||
},
|
},
|
||||||
|
@@ -27,7 +27,7 @@ function ControlledAutocomplete(
|
|||||||
required = false,
|
required = false,
|
||||||
name,
|
name,
|
||||||
defaultValue,
|
defaultValue,
|
||||||
shouldUnregister,
|
shouldUnregister = true,
|
||||||
onBlur,
|
onBlur,
|
||||||
onChange,
|
onChange,
|
||||||
description,
|
description,
|
||||||
@@ -69,7 +69,7 @@ function ControlledAutocomplete(
|
|||||||
},
|
},
|
||||||
fieldState,
|
fieldState,
|
||||||
}) => (
|
}) => (
|
||||||
<div>
|
<div style={{ width:'100%' }}>
|
||||||
{/* encapsulated with an element such as div to vertical spacing delegated from parent */}
|
{/* encapsulated with an element such as div to vertical spacing delegated from parent */}
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
{...autocompleteProps}
|
{...autocompleteProps}
|
||||||
|
56
packages/web/src/components/DeleteAccountDialog/index.ee.tsx
Normal file
56
packages/web/src/components/DeleteAccountDialog/index.ee.tsx
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { useMutation } from '@apollo/client';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import Dialog from '@mui/material/Dialog';
|
||||||
|
import DialogActions from '@mui/material/DialogActions';
|
||||||
|
import DialogContent from '@mui/material/DialogContent';
|
||||||
|
import DialogContentText from '@mui/material/DialogContentText';
|
||||||
|
import DialogTitle from '@mui/material/DialogTitle';
|
||||||
|
|
||||||
|
import * as URLS from 'config/urls';
|
||||||
|
import apolloClient from 'graphql/client';
|
||||||
|
import { DELETE_USER } from 'graphql/mutations/delete-user.ee';
|
||||||
|
import useAuthentication from 'hooks/useAuthentication';
|
||||||
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
|
import useCurrentUser from 'hooks/useCurrentUser';
|
||||||
|
|
||||||
|
type DeleteAccountDialogProps = {
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DeleteAccountDialog(props: DeleteAccountDialogProps) {
|
||||||
|
const [deleteUser] = useMutation(DELETE_USER);
|
||||||
|
const formatMessage = useFormatMessage();
|
||||||
|
const currentUser = useCurrentUser();
|
||||||
|
const authentication = useAuthentication();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const handleConfirm = React.useCallback(async () => {
|
||||||
|
await deleteUser();
|
||||||
|
|
||||||
|
authentication.updateToken('');
|
||||||
|
await apolloClient.clearStore();
|
||||||
|
|
||||||
|
navigate(URLS.LOGIN);
|
||||||
|
}, [deleteUser, currentUser]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open onClose={props.onClose}>
|
||||||
|
<DialogTitle >
|
||||||
|
{formatMessage('deleteAccountDialog.title')}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogContentText id="alert-dialog-description">
|
||||||
|
{formatMessage('deleteAccountDialog.description')}
|
||||||
|
</DialogContentText>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={props.onClose}>{formatMessage('deleteAccountDialog.cancel')}</Button>
|
||||||
|
<Button onClick={handleConfirm} color="error">
|
||||||
|
{formatMessage('deleteAccountDialog.confirm')}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
@@ -85,7 +85,6 @@ export default function FlowRow(props: FlowRowProps): React.ReactElement {
|
|||||||
|
|
||||||
<IconButton
|
<IconButton
|
||||||
size="large"
|
size="large"
|
||||||
edge="start"
|
|
||||||
color="inherit"
|
color="inherit"
|
||||||
aria-label="open context menu"
|
aria-label="open context menu"
|
||||||
ref={contextButtonRef}
|
ref={contextButtonRef}
|
||||||
|
@@ -30,11 +30,11 @@ export const Title = styled(MuiStack)(() => ({
|
|||||||
gridArea: 'title',
|
gridArea: 'title',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const ContextMenu = styled(MuiBox)(() => ({
|
export const ContextMenu = styled(MuiBox)(({ theme }) => ({
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: 10,
|
gap: theme.spacing(0.625),
|
||||||
gridArea: 'menu',
|
gridArea: 'menu',
|
||||||
}));
|
}));
|
||||||
export const Typography = styled(MuiTypography)(() => ({
|
export const Typography = styled(MuiTypography)(() => ({
|
||||||
|
@@ -0,0 +1,213 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
import { useFormContext, useWatch } from 'react-hook-form';
|
||||||
|
import Stack from '@mui/material/Stack';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import Divider from '@mui/material/Divider';
|
||||||
|
import IconButton from '@mui/material/IconButton';
|
||||||
|
import RemoveIcon from '@mui/icons-material/Remove';
|
||||||
|
import AddIcon from '@mui/icons-material/Add';
|
||||||
|
import type { IField, IFieldText, IFieldDropdown } from '@automatisch/types';
|
||||||
|
|
||||||
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
|
import InputCreator from 'components/InputCreator';
|
||||||
|
import { EditorContext } from 'contexts/Editor';
|
||||||
|
|
||||||
|
type TGroupItem = {
|
||||||
|
key: string;
|
||||||
|
operator: string;
|
||||||
|
value: string;
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type TGroup = Record<'and', TGroupItem[]>;
|
||||||
|
|
||||||
|
const createGroupItem = (): TGroupItem => ({
|
||||||
|
key: '',
|
||||||
|
operator: operators[0].value,
|
||||||
|
value: '',
|
||||||
|
id: uuidv4(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const createGroup = (): TGroup => ({
|
||||||
|
and: [createGroupItem()]
|
||||||
|
});
|
||||||
|
|
||||||
|
const operators = [
|
||||||
|
{
|
||||||
|
label: 'Equal',
|
||||||
|
value: 'equal',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Not Equal',
|
||||||
|
value: 'not_equal',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Greater Than',
|
||||||
|
value: 'greater_than',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Less Than',
|
||||||
|
value: 'less_than',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Greater Than Or Equal',
|
||||||
|
value: 'greater_than_or_equal',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Less Than Or Equal',
|
||||||
|
value: 'less_than_or_equal',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Contains',
|
||||||
|
value: 'contains',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Not Contains',
|
||||||
|
value: 'not_contains',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const createStringArgument = (argumentOptions: Omit<IFieldText, 'type' | 'required' | 'variables'>): IField => {
|
||||||
|
return {
|
||||||
|
...argumentOptions,
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
variables: true,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const createDropdownArgument = (argumentOptions: Omit<IFieldDropdown, 'type' | 'required'>): IField => {
|
||||||
|
return {
|
||||||
|
...argumentOptions,
|
||||||
|
required: true,
|
||||||
|
type: 'dropdown',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type FilterConditionsProps = {
|
||||||
|
stepId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function FilterConditions(props: FilterConditionsProps): React.ReactElement {
|
||||||
|
const {
|
||||||
|
stepId
|
||||||
|
} = props;
|
||||||
|
const formatMessage = useFormatMessage();
|
||||||
|
const { control, setValue, getValues } = useFormContext();
|
||||||
|
const groups = useWatch({ control, name: 'parameters.or' });
|
||||||
|
const editorContext = React.useContext(EditorContext);
|
||||||
|
|
||||||
|
React.useEffect(function addInitialGroupWhenEmpty() {
|
||||||
|
const groups = getValues('parameters.or');
|
||||||
|
|
||||||
|
if (!groups) {
|
||||||
|
setValue('parameters.or', [createGroup()]);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const appendGroup = React.useCallback(() => {
|
||||||
|
const values = getValues('parameters.or');
|
||||||
|
|
||||||
|
setValue('parameters.or', values.concat(createGroup()))
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const appendGroupItem = React.useCallback((index) => {
|
||||||
|
const group = getValues(`parameters.or.${index}.and`);
|
||||||
|
setValue(`parameters.or.${index}.and`, group.concat(createGroupItem()));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const removeGroupItem = React.useCallback((groupIndex, groupItemIndex) => {
|
||||||
|
const group: TGroupItem[] = getValues(`parameters.or.${groupIndex}.and`);
|
||||||
|
|
||||||
|
if (group.length === 1) {
|
||||||
|
const groups: TGroup[] = getValues('parameters.or');
|
||||||
|
|
||||||
|
setValue('parameters.or', groups.filter((group, index) => index !== groupIndex));
|
||||||
|
} else {
|
||||||
|
setValue(`parameters.or.${groupIndex}.and`, group.filter((groupItem, index) => index !== groupItemIndex));
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<Stack sx={{ width: "100%" }} direction="column" spacing={2} mt={2}>
|
||||||
|
{groups?.map((group: TGroup, groupIndex: number) => (
|
||||||
|
<>
|
||||||
|
{groupIndex !== 0 && <Divider />}
|
||||||
|
|
||||||
|
<Typography variant="subtitle2" gutterBottom>
|
||||||
|
{groupIndex === 0 && formatMessage('filterConditions.onlyContinueIf')}
|
||||||
|
{groupIndex !== 0 && formatMessage('filterConditions.orContinueIf')}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{group?.and?.map((groupItem: TGroupItem, groupItemIndex: number) => (
|
||||||
|
<Stack direction="row" spacing={2} key={`item-${groupItem.id}`}>
|
||||||
|
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={{ xs: 2}} sx={{ display: 'flex', flex: 1 }}>
|
||||||
|
<Box sx={{ display: 'flex', flex: '1 0 0px', maxWidth: ['100%', '33%'] }}>
|
||||||
|
<InputCreator
|
||||||
|
schema={createStringArgument({ key: `or.${groupIndex}.and.${groupItemIndex}.key`, label: 'Choose field' })}
|
||||||
|
namePrefix="parameters"
|
||||||
|
stepId={stepId}
|
||||||
|
disabled={editorContext.readOnly}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{ display: 'flex', flex: '1 0 0px', maxWidth: ['100%', '33%'] }}>
|
||||||
|
<InputCreator
|
||||||
|
schema={createDropdownArgument({ key: `or.${groupIndex}.and.${groupItemIndex}.operator`, options: operators, label: 'Choose condition' })}
|
||||||
|
namePrefix="parameters"
|
||||||
|
stepId={stepId}
|
||||||
|
disabled={editorContext.readOnly}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{ display: 'flex', flex: '1 0 0px', maxWidth: ['100%', '33%'] }}>
|
||||||
|
<InputCreator
|
||||||
|
schema={createStringArgument({ key: `or.${groupIndex}.and.${groupItemIndex}.value`, label: 'Enter text' })}
|
||||||
|
namePrefix="parameters"
|
||||||
|
stepId={stepId}
|
||||||
|
disabled={editorContext.readOnly}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
edge="start"
|
||||||
|
onClick={() => removeGroupItem(groupIndex, groupItemIndex)}
|
||||||
|
sx={{ width: 61, height: 61 }}
|
||||||
|
>
|
||||||
|
<RemoveIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Stack>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<Stack spacing={1} direction="row">
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
edge="start"
|
||||||
|
sx={{ width: 61, height: 61 }}
|
||||||
|
onClick={() => appendGroupItem(groupIndex)}
|
||||||
|
>
|
||||||
|
<AddIcon /> And
|
||||||
|
</IconButton>
|
||||||
|
|
||||||
|
{(groups.length - 1) === groupIndex && <IconButton
|
||||||
|
size="small"
|
||||||
|
edge="start"
|
||||||
|
onClick={appendGroup}
|
||||||
|
sx={{ width: 61, height: 61 }}
|
||||||
|
>
|
||||||
|
<AddIcon /> Or
|
||||||
|
</IconButton>}
|
||||||
|
</Stack>
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FilterConditions;
|
@@ -4,11 +4,12 @@ import Collapse from '@mui/material/Collapse';
|
|||||||
import ListItem from '@mui/material/ListItem';
|
import ListItem from '@mui/material/ListItem';
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
import Stack from '@mui/material/Stack';
|
import Stack from '@mui/material/Stack';
|
||||||
|
import type { IField, IStep, ISubstep } from '@automatisch/types';
|
||||||
|
|
||||||
import { EditorContext } from 'contexts/Editor';
|
import { EditorContext } from 'contexts/Editor';
|
||||||
import FlowSubstepTitle from 'components/FlowSubstepTitle';
|
import FlowSubstepTitle from 'components/FlowSubstepTitle';
|
||||||
import InputCreator from 'components/InputCreator';
|
import InputCreator from 'components/InputCreator';
|
||||||
import type { IField, IStep, ISubstep } from '@automatisch/types';
|
import FilterConditions from './FilterConditions';
|
||||||
|
|
||||||
type FlowSubstepProps = {
|
type FlowSubstepProps = {
|
||||||
substep: ISubstep;
|
substep: ISubstep;
|
||||||
@@ -84,20 +85,25 @@ function FlowSubstep(props: FlowSubstepProps): React.ReactElement {
|
|||||||
pb: 3,
|
pb: 3,
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
alignItems: 'flex-start',
|
alignItems: 'flex-start',
|
||||||
|
position: 'relative'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Stack width="100%" spacing={2}>
|
{!!args?.length && (
|
||||||
{args?.map((argument) => (
|
<Stack width="100%" spacing={2}>
|
||||||
<InputCreator
|
{args.map((argument) => (
|
||||||
key={argument.key}
|
<InputCreator
|
||||||
schema={argument}
|
key={argument.key}
|
||||||
namePrefix="parameters"
|
schema={argument}
|
||||||
stepId={step.id}
|
namePrefix="parameters"
|
||||||
disabled={editorContext.readOnly}
|
stepId={step.id}
|
||||||
showOptionValue={true}
|
disabled={editorContext.readOnly}
|
||||||
/>
|
showOptionValue={true}
|
||||||
))}
|
/>
|
||||||
</Stack>
|
))}
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{step.appKey === 'filter' && <FilterConditions stepId={step.id} />}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
fullWidth
|
fullWidth
|
||||||
|
68
packages/web/src/components/ForgotPasswordForm/index.ee.tsx
Normal file
68
packages/web/src/components/ForgotPasswordForm/index.ee.tsx
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { useMutation } from '@apollo/client';
|
||||||
|
import Paper from '@mui/material/Paper';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import LoadingButton from '@mui/lab/LoadingButton';
|
||||||
|
|
||||||
|
import { FORGOT_PASSWORD } from 'graphql/mutations/forgot-password.ee';
|
||||||
|
import Form from 'components/Form';
|
||||||
|
import TextField from 'components/TextField';
|
||||||
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
|
|
||||||
|
export default function ForgotPasswordForm() {
|
||||||
|
const formatMessage = useFormatMessage();
|
||||||
|
const [forgotPassword, { data, loading }] = useMutation(FORGOT_PASSWORD);
|
||||||
|
|
||||||
|
const handleSubmit = async (values: any) => {
|
||||||
|
await forgotPassword({
|
||||||
|
variables: {
|
||||||
|
input: values,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper sx={{ px: 2, py: 4 }}>
|
||||||
|
<Typography
|
||||||
|
variant="h3"
|
||||||
|
align="center"
|
||||||
|
sx={{
|
||||||
|
borderBottom: '1px solid',
|
||||||
|
borderColor: (theme) => theme.palette.text.disabled,
|
||||||
|
pb: 2,
|
||||||
|
mb: 2,
|
||||||
|
}}
|
||||||
|
gutterBottom
|
||||||
|
>
|
||||||
|
{formatMessage('forgotPasswordForm.title')}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Form onSubmit={handleSubmit}>
|
||||||
|
<TextField
|
||||||
|
label={formatMessage('forgotPasswordForm.emailFieldLabel')}
|
||||||
|
name="email"
|
||||||
|
required
|
||||||
|
fullWidth
|
||||||
|
margin="dense"
|
||||||
|
autoComplete="username"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LoadingButton
|
||||||
|
type="submit"
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
sx={{ boxShadow: 2, my: 3 }}
|
||||||
|
loading={loading}
|
||||||
|
disabled={data}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
{formatMessage('forgotPasswordForm.submit')}
|
||||||
|
</LoadingButton>
|
||||||
|
|
||||||
|
{data && <Typography variant="body1" sx={{ color: (theme) => theme.palette.success.main }}>
|
||||||
|
{formatMessage('forgotPasswordForm.instructionsSent')}
|
||||||
|
</Typography>}
|
||||||
|
</Form>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
}
|
@@ -1,7 +1,9 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import MuiTextField from '@mui/material/TextField';
|
import MuiTextField from '@mui/material/TextField';
|
||||||
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
import type { IField, IFieldDropdownOption } from '@automatisch/types';
|
import type { IField, IFieldDropdownOption } from '@automatisch/types';
|
||||||
|
|
||||||
|
import useDynamicFields from 'hooks/useDynamicFields';
|
||||||
import useDynamicData from 'hooks/useDynamicData';
|
import useDynamicData from 'hooks/useDynamicData';
|
||||||
import PowerInput from 'components/PowerInput';
|
import PowerInput from 'components/PowerInput';
|
||||||
import TextField from 'components/TextField';
|
import TextField from 'components/TextField';
|
||||||
@@ -52,58 +54,111 @@ export default function InputCreator(
|
|||||||
} = schema;
|
} = schema;
|
||||||
|
|
||||||
const { data, loading } = useDynamicData(stepId, schema);
|
const { data, loading } = useDynamicData(stepId, schema);
|
||||||
|
const {
|
||||||
|
data: additionalFields,
|
||||||
|
loading: additionalFieldsLoading
|
||||||
|
} = useDynamicFields(stepId, schema);
|
||||||
const computedName = namePrefix ? `${namePrefix}.${name}` : name;
|
const computedName = namePrefix ? `${namePrefix}.${name}` : name;
|
||||||
|
|
||||||
if (type === 'dropdown') {
|
if (type === 'dropdown') {
|
||||||
const preparedOptions = schema.options || optionGenerator(data);
|
const preparedOptions = schema.options || optionGenerator(data);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ControlledAutocomplete
|
<React.Fragment>
|
||||||
name={computedName}
|
<ControlledAutocomplete
|
||||||
dependsOn={dependsOn}
|
name={computedName}
|
||||||
fullWidth
|
dependsOn={dependsOn}
|
||||||
disablePortal
|
fullWidth
|
||||||
disableClearable={required}
|
disablePortal
|
||||||
options={preparedOptions}
|
disableClearable={required}
|
||||||
renderInput={(params) => <MuiTextField {...params} label={label} />}
|
options={preparedOptions}
|
||||||
defaultValue={value as string}
|
renderInput={(params) => <MuiTextField {...params} label={label} />}
|
||||||
onChange={console.log}
|
defaultValue={value as string}
|
||||||
description={description}
|
description={description}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
showOptionValue={showOptionValue}
|
showOptionValue={showOptionValue}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{(additionalFieldsLoading && !additionalFields?.length) && <div>
|
||||||
|
<CircularProgress sx={{ display: 'block', margin: '20px auto' }} />
|
||||||
|
</div>}
|
||||||
|
|
||||||
|
{additionalFields?.map((field) => (
|
||||||
|
<InputCreator
|
||||||
|
key={field.key}
|
||||||
|
schema={field}
|
||||||
|
namePrefix="parameters"
|
||||||
|
stepId={stepId}
|
||||||
|
disabled={disabled}
|
||||||
|
showOptionValue={true}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'string') {
|
if (type === 'string') {
|
||||||
if (variables) {
|
if (variables) {
|
||||||
return (
|
return (
|
||||||
<PowerInput
|
<React.Fragment>
|
||||||
label={label}
|
<PowerInput
|
||||||
description={description}
|
label={label}
|
||||||
name={computedName}
|
description={description}
|
||||||
required={required}
|
name={computedName}
|
||||||
disabled={disabled}
|
required={required}
|
||||||
/>
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{(additionalFieldsLoading && !additionalFields?.length) && <div>
|
||||||
|
<CircularProgress sx={{ display: 'block', margin: '20px auto' }} />
|
||||||
|
</div>}
|
||||||
|
|
||||||
|
{additionalFields?.map((field) => (
|
||||||
|
<InputCreator
|
||||||
|
key={field.key}
|
||||||
|
schema={field}
|
||||||
|
namePrefix="parameters"
|
||||||
|
stepId={stepId}
|
||||||
|
disabled={disabled}
|
||||||
|
showOptionValue={true}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TextField
|
<React.Fragment>
|
||||||
defaultValue={value}
|
<TextField
|
||||||
required={required}
|
defaultValue={value}
|
||||||
placeholder=""
|
required={required}
|
||||||
readOnly={readOnly || disabled}
|
placeholder=""
|
||||||
onChange={onChange}
|
readOnly={readOnly || disabled}
|
||||||
onBlur={onBlur}
|
onChange={onChange}
|
||||||
name={computedName}
|
onBlur={onBlur}
|
||||||
size="small"
|
name={computedName}
|
||||||
label={label}
|
label={label}
|
||||||
fullWidth
|
fullWidth
|
||||||
helperText={description}
|
helperText={description}
|
||||||
clickToCopy={clickToCopy}
|
clickToCopy={clickToCopy}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{(additionalFieldsLoading && !additionalFields?.length) && <div>
|
||||||
|
<CircularProgress sx={{ display: 'block', margin: '20px auto' }} />
|
||||||
|
</div>}
|
||||||
|
|
||||||
|
{additionalFields?.map((field) => (
|
||||||
|
<InputCreator
|
||||||
|
key={field.key}
|
||||||
|
schema={field}
|
||||||
|
namePrefix="parameters"
|
||||||
|
stepId={stepId}
|
||||||
|
disabled={disabled}
|
||||||
|
showOptionValue={true}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
41
packages/web/src/components/LiveChat/Chatwoot.ee.tsx
Normal file
41
packages/web/src/components/LiveChat/Chatwoot.ee.tsx
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import appConfig from 'config/app';
|
||||||
|
import useCurrentUser from 'hooks/useCurrentUser';
|
||||||
|
|
||||||
|
type ChatwootProps = {
|
||||||
|
ready: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Chatwoot = ({ ready }: ChatwootProps) => {
|
||||||
|
const currentUser = useCurrentUser();
|
||||||
|
|
||||||
|
React.useEffect(function initiateChatwoot() {
|
||||||
|
window.chatwootSDK.run({
|
||||||
|
websiteToken: 'EFyq5MTsvS7XwUrwSH36VskT',
|
||||||
|
baseUrl: appConfig.chatwootBaseUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
return function removeChatwoot() {
|
||||||
|
window.$chatwoot.reset();
|
||||||
|
window.$chatwoot.toggleBubbleVisibility('hide');
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
React.useEffect(function initiateUser() {
|
||||||
|
if (!currentUser?.id || !ready) return;
|
||||||
|
|
||||||
|
window.$chatwoot.setUser(currentUser.id, {
|
||||||
|
email: currentUser.email,
|
||||||
|
name: currentUser.fullName,
|
||||||
|
});
|
||||||
|
|
||||||
|
window.$chatwoot.toggleBubbleVisibility("show");
|
||||||
|
}, [currentUser, ready]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment />
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Chatwoot;
|
62
packages/web/src/components/LiveChat/index.ee.tsx
Normal file
62
packages/web/src/components/LiveChat/index.ee.tsx
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import appConfig from 'config/app';
|
||||||
|
import useAuthentication from 'hooks/useAuthentication';
|
||||||
|
import useCloud from 'hooks/useCloud';
|
||||||
|
import Chatwoot from './Chatwoot.ee';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
chatwootSDK: any;
|
||||||
|
$chatwoot: any;
|
||||||
|
chatwootSettings: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const LiveChat = () => {
|
||||||
|
const isCloud = useCloud();
|
||||||
|
const { isAuthenticated } = useAuthentication();
|
||||||
|
const [isLoaded, setLoaded] = React.useState(false);
|
||||||
|
const [isReady, setReady] = React.useState(false);
|
||||||
|
|
||||||
|
const shouldShow = isCloud && isAuthenticated;
|
||||||
|
|
||||||
|
React.useLayoutEffect(() => {
|
||||||
|
window.addEventListener("chatwoot:ready", function () {
|
||||||
|
setReady(true);
|
||||||
|
}, { once: true });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
React.useLayoutEffect(function addChatwootScript() {
|
||||||
|
if (!shouldShow) return;
|
||||||
|
|
||||||
|
window.chatwootSettings = {
|
||||||
|
hideMessageBubble: true,
|
||||||
|
position: 'right',
|
||||||
|
type: 'standard',
|
||||||
|
launcherTitle: 'Give us feedback'
|
||||||
|
};
|
||||||
|
|
||||||
|
const g = document.createElement('script')
|
||||||
|
const s = document.getElementsByTagName('script')[0];
|
||||||
|
g.src = appConfig.chatwootBaseUrl + '/packs/js/sdk.js';
|
||||||
|
g.defer = true;
|
||||||
|
g.async = true;
|
||||||
|
|
||||||
|
if (s.parentNode) {
|
||||||
|
s.parentNode.insertBefore(g, s);
|
||||||
|
}
|
||||||
|
|
||||||
|
g.onload = function () {
|
||||||
|
setLoaded(true);
|
||||||
|
}
|
||||||
|
}, [shouldShow]);
|
||||||
|
|
||||||
|
if (!shouldShow || !isLoaded) return (<React.Fragment />);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Chatwoot ready={isReady} />
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LiveChat;
|
@@ -1,7 +1,8 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate, Link as RouterLink } from 'react-router-dom';
|
||||||
import { useMutation } from '@apollo/client';
|
import { useMutation } from '@apollo/client';
|
||||||
import Paper from '@mui/material/Paper';
|
import Paper from '@mui/material/Paper';
|
||||||
|
import Link from '@mui/material/Link';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import LoadingButton from '@mui/lab/LoadingButton';
|
import LoadingButton from '@mui/lab/LoadingButton';
|
||||||
|
|
||||||
@@ -10,52 +11,11 @@ import * as URLS from 'config/urls';
|
|||||||
import { LOGIN } from 'graphql/mutations/login';
|
import { LOGIN } from 'graphql/mutations/login';
|
||||||
import Form from 'components/Form';
|
import Form from 'components/Form';
|
||||||
import TextField from 'components/TextField';
|
import TextField from 'components/TextField';
|
||||||
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
function renderFields(props: { loading: boolean }) {
|
|
||||||
const { loading = false } = props;
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<TextField
|
|
||||||
label="Email"
|
|
||||||
name="email"
|
|
||||||
required
|
|
||||||
fullWidth
|
|
||||||
margin="dense"
|
|
||||||
autoComplete="username"
|
|
||||||
data-test="email-text-field"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextField
|
|
||||||
label="Password"
|
|
||||||
name="password"
|
|
||||||
type="password"
|
|
||||||
required
|
|
||||||
fullWidth
|
|
||||||
margin="dense"
|
|
||||||
autoComplete="current-password"
|
|
||||||
data-test="password-text-field"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<LoadingButton
|
|
||||||
type="submit"
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
sx={{ boxShadow: 2, mt: 3 }}
|
|
||||||
loading={loading}
|
|
||||||
fullWidth
|
|
||||||
data-test="login-button"
|
|
||||||
>
|
|
||||||
Login
|
|
||||||
</LoadingButton>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function LoginForm() {
|
function LoginForm() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const formatMessage = useFormatMessage();
|
||||||
const authentication = useAuthentication();
|
const authentication = useAuthentication();
|
||||||
const [login, { loading }] = useMutation(LOGIN);
|
const [login, { loading }] = useMutation(LOGIN);
|
||||||
|
|
||||||
@@ -77,8 +37,6 @@ function LoginForm() {
|
|||||||
authentication.updateToken(token);
|
authentication.updateToken(token);
|
||||||
};
|
};
|
||||||
|
|
||||||
const render = React.useMemo(() => renderFields({ loading }), [loading]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper sx={{ px: 2, py: 4 }}>
|
<Paper sx={{ px: 2, py: 4 }}>
|
||||||
<Typography
|
<Typography
|
||||||
@@ -92,10 +50,60 @@ function LoginForm() {
|
|||||||
}}
|
}}
|
||||||
gutterBottom
|
gutterBottom
|
||||||
>
|
>
|
||||||
Login
|
{formatMessage('loginForm.title')}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Form onSubmit={handleSubmit} render={render} />
|
<Form onSubmit={handleSubmit}>
|
||||||
|
<TextField
|
||||||
|
label={formatMessage('loginForm.emailFieldLabel')}
|
||||||
|
name="email"
|
||||||
|
required
|
||||||
|
fullWidth
|
||||||
|
margin="dense"
|
||||||
|
autoComplete="username"
|
||||||
|
data-test="email-text-field"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
label={formatMessage('loginForm.passwordFieldLabel')}
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
fullWidth
|
||||||
|
margin="dense"
|
||||||
|
autoComplete="current-password"
|
||||||
|
data-test="password-text-field"
|
||||||
|
sx={{ mb: 1 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
component={RouterLink}
|
||||||
|
to={URLS.FORGOT_PASSWORD}
|
||||||
|
underline="none"
|
||||||
|
>
|
||||||
|
{formatMessage('loginForm.forgotPasswordText')}
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<LoadingButton
|
||||||
|
type="submit"
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
sx={{ boxShadow: 2, mt: 3 }}
|
||||||
|
loading={loading}
|
||||||
|
fullWidth
|
||||||
|
data-test="login-button"
|
||||||
|
>
|
||||||
|
{formatMessage('loginForm.submit')}
|
||||||
|
</LoadingButton>
|
||||||
|
|
||||||
|
<Typography variant="body1" align="center" mt={3}>
|
||||||
|
{formatMessage('loginForm.noAccount')}
|
||||||
|
|
||||||
|
<Link component={RouterLink} to={URLS.SIGNUP} underline="none">
|
||||||
|
{formatMessage('loginForm.signUp')}
|
||||||
|
</Link>
|
||||||
|
</Typography>
|
||||||
|
</Form>
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
28
packages/web/src/components/PaymentInformation/index.ee.tsx
Normal file
28
packages/web/src/components/PaymentInformation/index.ee.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
|
||||||
|
import PageTitle from 'components/PageTitle';
|
||||||
|
import { generateExternalLink } from 'helpers/translation-values';
|
||||||
|
import usePaymentPortalUrl from 'hooks/usePaymentPortalUrl.ee';
|
||||||
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
|
|
||||||
|
export default function PaymentInformation() {
|
||||||
|
const paymentPortal = usePaymentPortalUrl();
|
||||||
|
const formatMessage = useFormatMessage();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<PageTitle
|
||||||
|
gutterBottom
|
||||||
|
>
|
||||||
|
{formatMessage('billingAndUsageSettings.paymentInformation')}
|
||||||
|
</PageTitle>
|
||||||
|
|
||||||
|
<Typography>
|
||||||
|
{formatMessage(
|
||||||
|
'billingAndUsageSettings.paymentPortalInformation',
|
||||||
|
{ link: generateExternalLink(paymentPortal.url) })}
|
||||||
|
</Typography>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
@@ -81,7 +81,7 @@ const PowerInput = (props: PowerInputProps) => {
|
|||||||
name={name}
|
name={name}
|
||||||
control={control}
|
control={control}
|
||||||
defaultValue={defaultValue}
|
defaultValue={defaultValue}
|
||||||
shouldUnregister={false}
|
shouldUnregister={true}
|
||||||
render={({
|
render={({
|
||||||
field: {
|
field: {
|
||||||
value,
|
value,
|
||||||
@@ -130,7 +130,7 @@ const PowerInput = (props: PowerInputProps) => {
|
|||||||
/>
|
/>
|
||||||
</FakeInput>
|
</FakeInput>
|
||||||
{/* ghost placer for the variables popover */}
|
{/* ghost placer for the variables popover */}
|
||||||
<div ref={editorRef} style={{ width: '100%' }} />
|
<div ref={editorRef} style={{ position: 'absolute', right: 16, left: 16 }} />
|
||||||
|
|
||||||
<FormHelperText variant="outlined">{description}</FormHelperText>
|
<FormHelperText variant="outlined">{description}</FormHelperText>
|
||||||
|
|
||||||
|
122
packages/web/src/components/ResetPasswordForm/index.ee.tsx
Normal file
122
packages/web/src/components/ResetPasswordForm/index.ee.tsx
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { useSearchParams, useNavigate } from 'react-router-dom';
|
||||||
|
import { useMutation } from '@apollo/client';
|
||||||
|
import Paper from '@mui/material/Paper';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import LoadingButton from '@mui/lab/LoadingButton';
|
||||||
|
import { useSnackbar } from 'notistack';
|
||||||
|
import * as yup from 'yup';
|
||||||
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
|
|
||||||
|
import * as URLS from 'config/urls';
|
||||||
|
import Form from 'components/Form';
|
||||||
|
import TextField from 'components/TextField';
|
||||||
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
|
import { RESET_PASSWORD } from 'graphql/mutations/reset-password.ee';
|
||||||
|
|
||||||
|
const validationSchema = yup.object().shape({
|
||||||
|
password: yup.string().required('resetPasswordForm.mandatoryInput'),
|
||||||
|
confirmPassword: yup
|
||||||
|
.string()
|
||||||
|
.required('resetPasswordForm.mandatoryInput')
|
||||||
|
.oneOf([yup.ref('password')], 'resetPasswordForm.passwordsMustMatch'),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function ResetPasswordForm() {
|
||||||
|
const { enqueueSnackbar } = useSnackbar();
|
||||||
|
const formatMessage = useFormatMessage();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const [resetPassword, { data, loading }] = useMutation(RESET_PASSWORD);
|
||||||
|
|
||||||
|
const token = searchParams.get('token');
|
||||||
|
|
||||||
|
const handleSubmit = async (values: any) => {
|
||||||
|
await resetPassword({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
password: values.password,
|
||||||
|
token,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
enqueueSnackbar(formatMessage('resetPasswordForm.passwordUpdated'), { variant: 'success' });
|
||||||
|
|
||||||
|
navigate(URLS.LOGIN);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper sx={{ px: 2, py: 4 }}>
|
||||||
|
<Typography
|
||||||
|
variant="h3"
|
||||||
|
align="center"
|
||||||
|
sx={{
|
||||||
|
borderBottom: '1px solid',
|
||||||
|
borderColor: (theme) => theme.palette.text.disabled,
|
||||||
|
pb: 2,
|
||||||
|
mb: 2,
|
||||||
|
}}
|
||||||
|
gutterBottom
|
||||||
|
>
|
||||||
|
{formatMessage('resetPasswordForm.title')}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Form
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
resolver={yupResolver(validationSchema)}
|
||||||
|
mode="onChange"
|
||||||
|
render={({ formState: { errors, touchedFields } }) => (
|
||||||
|
<>
|
||||||
|
<TextField
|
||||||
|
label={formatMessage('resetPasswordForm.passwordFieldLabel')}
|
||||||
|
name="password"
|
||||||
|
fullWidth
|
||||||
|
margin="dense"
|
||||||
|
type="password"
|
||||||
|
error={touchedFields.password && !!errors?.password}
|
||||||
|
helperText={
|
||||||
|
touchedFields.password && errors?.password?.message
|
||||||
|
? formatMessage(errors?.password?.message, {
|
||||||
|
inputName: formatMessage('resetPasswordForm.passwordFieldLabel'),
|
||||||
|
})
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
label={formatMessage('resetPasswordForm.confirmPasswordFieldLabel')}
|
||||||
|
name="confirmPassword"
|
||||||
|
fullWidth
|
||||||
|
margin="dense"
|
||||||
|
type="password"
|
||||||
|
error={touchedFields.confirmPassword && !!errors?.confirmPassword}
|
||||||
|
helperText={
|
||||||
|
touchedFields.confirmPassword &&
|
||||||
|
errors?.confirmPassword?.message
|
||||||
|
? formatMessage(errors?.confirmPassword?.message, {
|
||||||
|
inputName: formatMessage(
|
||||||
|
'resetPasswordForm.confirmPasswordFieldLabel'
|
||||||
|
),
|
||||||
|
})
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LoadingButton
|
||||||
|
type="submit"
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
sx={{ boxShadow: 2, my: 3 }}
|
||||||
|
loading={loading}
|
||||||
|
disabled={data || !token}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
{formatMessage('resetPasswordForm.submit')}
|
||||||
|
</LoadingButton>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
}
|
@@ -5,8 +5,10 @@ import { useTheme } from '@mui/material/styles';
|
|||||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||||
import AccountCircleIcon from '@mui/icons-material/AccountCircle';
|
import AccountCircleIcon from '@mui/icons-material/AccountCircle';
|
||||||
import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew';
|
import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew';
|
||||||
|
import PaymentIcon from '@mui/icons-material/Payment';
|
||||||
|
|
||||||
import * as URLS from 'config/urls';
|
import * as URLS from 'config/urls';
|
||||||
|
import useAutomatischInfo from 'hooks/useAutomatischInfo';
|
||||||
import AppBar from 'components/AppBar';
|
import AppBar from 'components/AppBar';
|
||||||
import Drawer from 'components/Drawer';
|
import Drawer from 'components/Drawer';
|
||||||
|
|
||||||
@@ -14,13 +16,25 @@ type SettingsLayoutProps = {
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
const drawerLinks = [
|
function createDrawerLinks({ isCloud }: { isCloud: boolean }) {
|
||||||
{
|
const items = [
|
||||||
Icon: AccountCircleIcon,
|
{
|
||||||
primary: 'settingsDrawer.myProfile',
|
Icon: AccountCircleIcon,
|
||||||
to: URLS.SETTINGS_PROFILE,
|
primary: 'settingsDrawer.myProfile',
|
||||||
},
|
to: URLS.SETTINGS_PROFILE,
|
||||||
];
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
if (isCloud) {
|
||||||
|
items.push({
|
||||||
|
Icon: PaymentIcon,
|
||||||
|
primary: 'settingsDrawer.billingAndUsage',
|
||||||
|
to: URLS.SETTINGS_BILLING_AND_USAGE,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
const drawerBottomLinks = [
|
const drawerBottomLinks = [
|
||||||
{
|
{
|
||||||
@@ -33,6 +47,7 @@ const drawerBottomLinks = [
|
|||||||
export default function SettingsLayout({
|
export default function SettingsLayout({
|
||||||
children,
|
children,
|
||||||
}: SettingsLayoutProps): React.ReactElement {
|
}: SettingsLayoutProps): React.ReactElement {
|
||||||
|
const { isCloud } = useAutomatischInfo();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('lg'), {
|
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('lg'), {
|
||||||
noSsr: true,
|
noSsr: true,
|
||||||
@@ -41,6 +56,7 @@ export default function SettingsLayout({
|
|||||||
|
|
||||||
const openDrawer = () => setDrawerOpen(true);
|
const openDrawer = () => setDrawerOpen(true);
|
||||||
const closeDrawer = () => setDrawerOpen(false);
|
const closeDrawer = () => setDrawerOpen(false);
|
||||||
|
const drawerLinks = createDrawerLinks({ isCloud });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
181
packages/web/src/components/SignUpForm/index.ee.tsx
Normal file
181
packages/web/src/components/SignUpForm/index.ee.tsx
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { useMutation } from '@apollo/client';
|
||||||
|
import Paper from '@mui/material/Paper';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import LoadingButton from '@mui/lab/LoadingButton';
|
||||||
|
import * as yup from 'yup';
|
||||||
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
|
|
||||||
|
import useAuthentication from 'hooks/useAuthentication';
|
||||||
|
import * as URLS from 'config/urls';
|
||||||
|
import { CREATE_USER } from 'graphql/mutations/create-user.ee';
|
||||||
|
import Form from 'components/Form';
|
||||||
|
import TextField from 'components/TextField';
|
||||||
|
import { LOGIN } from 'graphql/mutations/login';
|
||||||
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
|
|
||||||
|
const validationSchema = yup.object().shape({
|
||||||
|
fullName: yup.string().trim().required('signupForm.mandatoryInput'),
|
||||||
|
email: yup
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.email('signupForm.validateEmail')
|
||||||
|
.required('signupForm.mandatoryInput'),
|
||||||
|
password: yup.string().required('signupForm.mandatoryInput'),
|
||||||
|
confirmPassword: yup
|
||||||
|
.string()
|
||||||
|
.required('signupForm.mandatoryInput')
|
||||||
|
.oneOf([yup.ref('password')], 'signupForm.passwordsMustMatch'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const initialValues = {
|
||||||
|
fullName: '',
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
confirmPassword: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
function SignUpForm() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const authentication = useAuthentication();
|
||||||
|
const formatMessage = useFormatMessage();
|
||||||
|
const [createUser] = useMutation(CREATE_USER);
|
||||||
|
const [login, { loading }] = useMutation(LOGIN);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (authentication.isAuthenticated) {
|
||||||
|
navigate(URLS.DASHBOARD);
|
||||||
|
}
|
||||||
|
}, [authentication.isAuthenticated]);
|
||||||
|
|
||||||
|
const handleSubmit = async (values: any) => {
|
||||||
|
const { fullName, email, password } = values;
|
||||||
|
await createUser({
|
||||||
|
variables: {
|
||||||
|
input: { fullName, email, password },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data } = await login({
|
||||||
|
variables: {
|
||||||
|
input: { email, password },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { token } = data.login;
|
||||||
|
|
||||||
|
authentication.updateToken(token);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper sx={{ px: 2, py: 4 }}>
|
||||||
|
<Typography
|
||||||
|
variant="h3"
|
||||||
|
align="center"
|
||||||
|
sx={{
|
||||||
|
borderBottom: '1px solid',
|
||||||
|
borderColor: (theme) => theme.palette.text.disabled,
|
||||||
|
pb: 2,
|
||||||
|
mb: 2,
|
||||||
|
}}
|
||||||
|
gutterBottom
|
||||||
|
>
|
||||||
|
{formatMessage('signupForm.title')}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Form
|
||||||
|
defaultValues={initialValues}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
resolver={yupResolver(validationSchema)}
|
||||||
|
mode="onChange"
|
||||||
|
render={({ formState: { errors, touchedFields } }) => (
|
||||||
|
<>
|
||||||
|
<TextField
|
||||||
|
label={formatMessage('signupForm.fullNameFieldLabel')}
|
||||||
|
name="fullName"
|
||||||
|
fullWidth
|
||||||
|
margin="dense"
|
||||||
|
autoComplete="fullName"
|
||||||
|
data-test="fullName-text-field"
|
||||||
|
error={touchedFields.fullName && !!errors?.fullName}
|
||||||
|
helperText={
|
||||||
|
touchedFields.fullName && errors?.fullName?.message
|
||||||
|
? formatMessage(errors?.fullName?.message, {
|
||||||
|
inputName: formatMessage('signupForm.fullNameFieldLabel'),
|
||||||
|
})
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
label={formatMessage('signupForm.emailFieldLabel')}
|
||||||
|
name="email"
|
||||||
|
fullWidth
|
||||||
|
margin="dense"
|
||||||
|
autoComplete="email"
|
||||||
|
data-test="email-text-field"
|
||||||
|
error={touchedFields.email && !!errors?.email}
|
||||||
|
helperText={
|
||||||
|
touchedFields.email && errors?.email?.message
|
||||||
|
? formatMessage(errors?.email?.message, {
|
||||||
|
inputName: formatMessage('signupForm.emailFieldLabel'),
|
||||||
|
})
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
label={formatMessage('signupForm.passwordFieldLabel')}
|
||||||
|
name="password"
|
||||||
|
fullWidth
|
||||||
|
margin="dense"
|
||||||
|
type="password"
|
||||||
|
error={touchedFields.password && !!errors?.password}
|
||||||
|
helperText={
|
||||||
|
touchedFields.password && errors?.password?.message
|
||||||
|
? formatMessage(errors?.password?.message, {
|
||||||
|
inputName: formatMessage('signupForm.passwordFieldLabel'),
|
||||||
|
})
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
label={formatMessage('signupForm.confirmPasswordFieldLabel')}
|
||||||
|
name="confirmPassword"
|
||||||
|
fullWidth
|
||||||
|
margin="dense"
|
||||||
|
type="password"
|
||||||
|
error={touchedFields.confirmPassword && !!errors?.confirmPassword}
|
||||||
|
helperText={
|
||||||
|
touchedFields.confirmPassword &&
|
||||||
|
errors?.confirmPassword?.message
|
||||||
|
? formatMessage(errors?.confirmPassword?.message, {
|
||||||
|
inputName: formatMessage(
|
||||||
|
'signupForm.confirmPasswordFieldLabel'
|
||||||
|
),
|
||||||
|
})
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LoadingButton
|
||||||
|
type="submit"
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
sx={{ boxShadow: 2, mt: 3 }}
|
||||||
|
loading={loading}
|
||||||
|
fullWidth
|
||||||
|
data-test="signUp-button"
|
||||||
|
>
|
||||||
|
{formatMessage('signupForm.submit')}
|
||||||
|
</LoadingButton>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SignUpForm;
|
@@ -38,9 +38,10 @@ export default function TextField(props: TextFieldProps): React.ReactElement {
|
|||||||
required,
|
required,
|
||||||
name,
|
name,
|
||||||
defaultValue,
|
defaultValue,
|
||||||
shouldUnregister,
|
shouldUnregister = true,
|
||||||
clickToCopy,
|
clickToCopy = false,
|
||||||
readOnly,
|
readOnly = false,
|
||||||
|
disabled = false,
|
||||||
onBlur,
|
onBlur,
|
||||||
onChange,
|
onChange,
|
||||||
...textFieldProps
|
...textFieldProps
|
||||||
@@ -64,6 +65,7 @@ export default function TextField(props: TextFieldProps): React.ReactElement {
|
|||||||
<MuiTextField
|
<MuiTextField
|
||||||
{...textFieldProps}
|
{...textFieldProps}
|
||||||
{...field}
|
{...field}
|
||||||
|
disabled={disabled}
|
||||||
onChange={(...args) => {
|
onChange={(...args) => {
|
||||||
controllerOnChange(...args);
|
controllerOnChange(...args);
|
||||||
onChange?.(...args);
|
onChange?.(...args);
|
||||||
@@ -85,10 +87,3 @@ export default function TextField(props: TextFieldProps): React.ReactElement {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
TextField.defaultProps = {
|
|
||||||
readOnly: false,
|
|
||||||
disabled: false,
|
|
||||||
clickToCopy: false,
|
|
||||||
shouldUnregister: false,
|
|
||||||
};
|
|
||||||
|
@@ -0,0 +1,57 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
|
|
||||||
|
import Paper from '@mui/material/Paper';
|
||||||
|
import Table from '@mui/material/Table';
|
||||||
|
import TableBody from '@mui/material/TableBody';
|
||||||
|
import TableCell from '@mui/material/TableCell';
|
||||||
|
import TableContainer from '@mui/material/TableContainer';
|
||||||
|
import TableRow from '@mui/material/TableRow';
|
||||||
|
|
||||||
|
import useUsageData from 'hooks/useUsageData.ee';
|
||||||
|
|
||||||
|
export default function UsageDataInformation() {
|
||||||
|
const usageData = useUsageData();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<TableContainer component={Paper}>
|
||||||
|
<Table>
|
||||||
|
<TableBody>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell component="td" scope="row">
|
||||||
|
Current plan
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
|
<TableCell align="right" sx={{ fontWeight: 500 }}>{usageData.name}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
|
||||||
|
<TableRow>
|
||||||
|
<TableCell component="td" scope="row">
|
||||||
|
Total allowed task count
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
|
<TableCell align="right" sx={{ fontWeight: 500 }}>{usageData.allowedTaskCount}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
|
||||||
|
<TableRow>
|
||||||
|
<TableCell component="td" scope="row">
|
||||||
|
Consumed task count
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
|
<TableCell align="right" sx={{ fontWeight: 500 }}>{usageData.consumedTaskCount}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
|
||||||
|
<TableRow sx={{ 'td': { border: 0 } }}>
|
||||||
|
<TableCell component="td" scope="row">
|
||||||
|
Next billing date
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
|
<TableCell align="right" sx={{ fontWeight: 500 }}>{usageData.nextResetAt?.toLocaleString(DateTime.DATE_FULL)}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
@@ -6,6 +6,7 @@ const config: Config = {
|
|||||||
baseUrl: process.env.REACT_APP_BASE_URL as string,
|
baseUrl: process.env.REACT_APP_BASE_URL as string,
|
||||||
graphqlUrl: process.env.REACT_APP_GRAPHQL_URL as string,
|
graphqlUrl: process.env.REACT_APP_GRAPHQL_URL as string,
|
||||||
notificationsUrl: process.env.REACT_APP_NOTIFICATIONS_URL as string,
|
notificationsUrl: process.env.REACT_APP_NOTIFICATIONS_URL as string,
|
||||||
|
chatwootBaseUrl: 'https://app.chatwoot.com',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
@@ -5,6 +5,9 @@ export const EXECUTION = (executionId: string): string =>
|
|||||||
`/executions/${executionId}`;
|
`/executions/${executionId}`;
|
||||||
|
|
||||||
export const LOGIN = '/login';
|
export const LOGIN = '/login';
|
||||||
|
export const SIGNUP = '/sign-up';
|
||||||
|
export const FORGOT_PASSWORD = '/forgot-password';
|
||||||
|
export const RESET_PASSWORD = '/reset-password';
|
||||||
|
|
||||||
export const APPS = '/apps';
|
export const APPS = '/apps';
|
||||||
export const NEW_APP_CONNECTION = '/apps/new';
|
export const NEW_APP_CONNECTION = '/apps/new';
|
||||||
@@ -61,8 +64,10 @@ export const FLOW_PATTERN = '/flows/:flowId';
|
|||||||
export const SETTINGS = '/settings';
|
export const SETTINGS = '/settings';
|
||||||
export const SETTINGS_DASHBOARD = SETTINGS;
|
export const SETTINGS_DASHBOARD = SETTINGS;
|
||||||
export const PROFILE = 'profile';
|
export const PROFILE = 'profile';
|
||||||
|
export const BILLING_AND_USAGE = 'billing';
|
||||||
export const UPDATES = '/updates';
|
export const UPDATES = '/updates';
|
||||||
export const SETTINGS_PROFILE = `${SETTINGS}/${PROFILE}`;
|
export const SETTINGS_PROFILE = `${SETTINGS}/${PROFILE}`;
|
||||||
|
export const SETTINGS_BILLING_AND_USAGE = `${SETTINGS}/${BILLING_AND_USAGE}`;
|
||||||
|
|
||||||
export const DASHBOARD = FLOWS;
|
export const DASHBOARD = FLOWS;
|
||||||
|
|
||||||
|
39
packages/web/src/contexts/AutomatischInfo.tsx
Normal file
39
packages/web/src/contexts/AutomatischInfo.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { useQuery } from '@apollo/client';
|
||||||
|
|
||||||
|
import { GET_AUTOMATISCH_INFO } from 'graphql/queries/get-automatisch-info';
|
||||||
|
|
||||||
|
export type AutomatischInfoContextParams = {
|
||||||
|
isCloud: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AutomatischInfoContext =
|
||||||
|
React.createContext<AutomatischInfoContextParams>({
|
||||||
|
isCloud: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
type AutomatischInfoProviderProps = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AutomatischInfoProvider = (
|
||||||
|
props: AutomatischInfoProviderProps
|
||||||
|
): React.ReactElement => {
|
||||||
|
const { children } = props;
|
||||||
|
const { data, loading } = useQuery(GET_AUTOMATISCH_INFO);
|
||||||
|
|
||||||
|
const isCloud = data?.getAutomatischInfo?.isCloud;
|
||||||
|
|
||||||
|
const value = React.useMemo(() => {
|
||||||
|
return {
|
||||||
|
isCloud,
|
||||||
|
loading
|
||||||
|
};
|
||||||
|
}, [isCloud, loading]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AutomatischInfoContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</AutomatischInfoContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
@@ -33,13 +33,16 @@ const createErrorLink = (callback: CreateLinkOptions['onError']): ApolloLink =>
|
|||||||
callback?.(message);
|
callback?.(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(
|
console.error(
|
||||||
`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
|
`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
|
||||||
);
|
);
|
||||||
|
|
||||||
if (message === NOT_AUTHORISED) {
|
if (message === NOT_AUTHORISED) {
|
||||||
setItem('token', '');
|
setItem('token', '');
|
||||||
window.location.href = URLS.LOGIN;
|
|
||||||
|
if (window.location.pathname !== URLS.LOGIN) {
|
||||||
|
window.location.href = URLS.LOGIN;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -47,12 +50,13 @@ const createErrorLink = (callback: CreateLinkOptions['onError']): ApolloLink =>
|
|||||||
if (autoSnackbar) {
|
if (autoSnackbar) {
|
||||||
callback?.(networkError.toString());
|
callback?.(networkError.toString());
|
||||||
}
|
}
|
||||||
console.log(`[Network error]: ${networkError}`);
|
|
||||||
|
console.error(`[Network error]: ${networkError}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
const noop = () => {};
|
const noop = () => { };
|
||||||
|
|
||||||
const createLink = (options: CreateLinkOptions): ApolloLink => {
|
const createLink = (options: CreateLinkOptions): ApolloLink => {
|
||||||
const { uri, onError = noop, token } = options;
|
const { uri, onError = noop, token } = options;
|
||||||
|
10
packages/web/src/graphql/mutations/create-user.ee.ts
Normal file
10
packages/web/src/graphql/mutations/create-user.ee.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { gql } from '@apollo/client';
|
||||||
|
|
||||||
|
export const CREATE_USER = gql`
|
||||||
|
mutation CreateUser($input: CreateUserInput) {
|
||||||
|
createUser(input: $input) {
|
||||||
|
email
|
||||||
|
fullName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
7
packages/web/src/graphql/mutations/delete-user.ee.ts
Normal file
7
packages/web/src/graphql/mutations/delete-user.ee.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { gql } from '@apollo/client';
|
||||||
|
|
||||||
|
export const DELETE_USER = gql`
|
||||||
|
mutation DeleteUser {
|
||||||
|
deleteUser
|
||||||
|
}
|
||||||
|
`;
|
7
packages/web/src/graphql/mutations/forgot-password.ee.ts
Normal file
7
packages/web/src/graphql/mutations/forgot-password.ee.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { gql } from '@apollo/client';
|
||||||
|
|
||||||
|
export const FORGOT_PASSWORD = gql`
|
||||||
|
mutation ForgotPassword($input: ForgotPasswordInput) {
|
||||||
|
forgotPassword(input: $input)
|
||||||
|
}
|
||||||
|
`;
|
7
packages/web/src/graphql/mutations/reset-password.ee.ts
Normal file
7
packages/web/src/graphql/mutations/reset-password.ee.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { gql } from '@apollo/client';
|
||||||
|
|
||||||
|
export const RESET_PASSWORD = gql`
|
||||||
|
mutation ResetPassword($input: ResetPasswordInput) {
|
||||||
|
resetPassword(input: $input)
|
||||||
|
}
|
||||||
|
`;
|
@@ -122,6 +122,14 @@ export const GET_APPS = gql`
|
|||||||
value
|
value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
additionalFields {
|
||||||
|
type
|
||||||
|
name
|
||||||
|
arguments {
|
||||||
|
name
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
10
packages/web/src/graphql/queries/get-automatisch-info.ts
Normal file
10
packages/web/src/graphql/queries/get-automatisch-info.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { gql } from '@apollo/client';
|
||||||
|
|
||||||
|
export const GET_AUTOMATISCH_INFO = gql`
|
||||||
|
query GetAutomatischInfo {
|
||||||
|
getAutomatischInfo {
|
||||||
|
isCloud
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
@@ -4,6 +4,7 @@ export const GET_CURRENT_USER = gql`
|
|||||||
query GetCurrentUser {
|
query GetCurrentUser {
|
||||||
getCurrentUser {
|
getCurrentUser {
|
||||||
id
|
id
|
||||||
|
fullName
|
||||||
email
|
email
|
||||||
createdAt
|
createdAt
|
||||||
updatedAt
|
updatedAt
|
||||||
|
21
packages/web/src/graphql/queries/get-dynamic-fields.ts
Normal file
21
packages/web/src/graphql/queries/get-dynamic-fields.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { gql } from '@apollo/client';
|
||||||
|
|
||||||
|
export const GET_DYNAMIC_FIELDS = gql`
|
||||||
|
query GetDynamicFields(
|
||||||
|
$stepId: String!
|
||||||
|
$key: String!
|
||||||
|
$parameters: JSONObject
|
||||||
|
) {
|
||||||
|
getDynamicFields(stepId: $stepId, key: $key, parameters: $parameters) {
|
||||||
|
label
|
||||||
|
key
|
||||||
|
type
|
||||||
|
required
|
||||||
|
description
|
||||||
|
options {
|
||||||
|
label
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
@@ -0,0 +1,10 @@
|
|||||||
|
import { gql } from '@apollo/client';
|
||||||
|
|
||||||
|
export const GET_PAYMENT_PORTAL_URL = gql`
|
||||||
|
query GetPaymentPortalUrl {
|
||||||
|
getPaymentPortalUrl {
|
||||||
|
url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
14
packages/web/src/graphql/queries/get-usage-data.ee.ts
Normal file
14
packages/web/src/graphql/queries/get-usage-data.ee.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { gql } from '@apollo/client';
|
||||||
|
|
||||||
|
export const GET_USAGE_DATA = gql`
|
||||||
|
query GetUsageData {
|
||||||
|
getUsageData {
|
||||||
|
name
|
||||||
|
allowedTaskCount
|
||||||
|
consumedTaskCount
|
||||||
|
remainingTaskCount
|
||||||
|
nextResetAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
@@ -1,6 +1,8 @@
|
|||||||
|
import Link from '@mui/material/Link';
|
||||||
|
|
||||||
export const generateExternalLink = (link: string) => (str: string) =>
|
export const generateExternalLink = (link: string) => (str: string) =>
|
||||||
(
|
(
|
||||||
<a href={link} target="_blank">
|
<Link href={link} target="_blank">
|
||||||
{str}
|
{str}
|
||||||
</a>
|
</Link>
|
||||||
);
|
);
|
||||||
|
14
packages/web/src/hooks/useAutomatischInfo.ts
Normal file
14
packages/web/src/hooks/useAutomatischInfo.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { AutomatischInfoContext } from 'contexts/AutomatischInfo';
|
||||||
|
|
||||||
|
type UseAutomatischInfoReturn = {
|
||||||
|
isCloud: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function useAutomatischInfo(): UseAutomatischInfoReturn {
|
||||||
|
const automatischInfoContext = React.useContext(AutomatischInfoContext);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isCloud: automatischInfoContext.isCloud,
|
||||||
|
};
|
||||||
|
}
|
7
packages/web/src/hooks/useCloud.ts
Normal file
7
packages/web/src/hooks/useCloud.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import useAutomatischInfo from './useAutomatischInfo';
|
||||||
|
|
||||||
|
export default function useCloud(): boolean {
|
||||||
|
const { isCloud } = useAutomatischInfo();
|
||||||
|
|
||||||
|
return isCloud;
|
||||||
|
}
|
112
packages/web/src/hooks/useDynamicFields.ts
Normal file
112
packages/web/src/hooks/useDynamicFields.ts
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { useLazyQuery } from '@apollo/client';
|
||||||
|
import type { UseFormReturn } from 'react-hook-form';
|
||||||
|
import { useFormContext } from 'react-hook-form';
|
||||||
|
import set from 'lodash/set';
|
||||||
|
import isEqual from 'lodash/isEqual';
|
||||||
|
import type {
|
||||||
|
IField,
|
||||||
|
IFieldDropdownAdditionalFields,
|
||||||
|
IJSONObject,
|
||||||
|
} from '@automatisch/types';
|
||||||
|
|
||||||
|
import { GET_DYNAMIC_FIELDS } from 'graphql/queries/get-dynamic-fields';
|
||||||
|
|
||||||
|
const variableRegExp = /({.*?})/g;
|
||||||
|
|
||||||
|
// TODO: extract this function to a separate file
|
||||||
|
function computeArguments(
|
||||||
|
args: IFieldDropdownAdditionalFields['arguments'],
|
||||||
|
getValues: UseFormReturn['getValues']
|
||||||
|
): IJSONObject {
|
||||||
|
const initialValue = {};
|
||||||
|
return args.reduce((result, { name, value }) => {
|
||||||
|
const isVariable = variableRegExp.test(value);
|
||||||
|
|
||||||
|
if (isVariable) {
|
||||||
|
const sanitizedFieldPath = value.replace(/{|}/g, '');
|
||||||
|
const computedValue = getValues(sanitizedFieldPath);
|
||||||
|
|
||||||
|
if (computedValue === undefined)
|
||||||
|
throw new Error(`The ${sanitizedFieldPath} field is required.`);
|
||||||
|
|
||||||
|
set(result, name, computedValue);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
set(result, name, value);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}, initialValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the dynamic fields for the given step.
|
||||||
|
* This hook must be within a react-hook-form context.
|
||||||
|
*
|
||||||
|
* @param stepId - the id of the step
|
||||||
|
* @param schema - the field schema that needs the dynamic fields
|
||||||
|
*/
|
||||||
|
function useDynamicFields(stepId: string | undefined, schema: IField) {
|
||||||
|
const lastComputedVariables = React.useRef({});
|
||||||
|
const [getDynamicFields, { called, data, loading }] =
|
||||||
|
useLazyQuery(GET_DYNAMIC_FIELDS);
|
||||||
|
const { getValues } = useFormContext();
|
||||||
|
const formValues = getValues();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return `null` when even a field is missing value.
|
||||||
|
*
|
||||||
|
* This must return the same reference if no computed variable is changed.
|
||||||
|
* Otherwise, it causes redundant network request!
|
||||||
|
*/
|
||||||
|
const computedVariables = React.useMemo(() => {
|
||||||
|
if (schema.type === 'dropdown' && schema.additionalFields) {
|
||||||
|
try {
|
||||||
|
const variables = computeArguments(schema.additionalFields.arguments, getValues);
|
||||||
|
|
||||||
|
// if computed variables are the same, return the last computed variables.
|
||||||
|
if (isEqual(variables, lastComputedVariables.current)) {
|
||||||
|
return lastComputedVariables.current;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastComputedVariables.current = variables;
|
||||||
|
|
||||||
|
return variables;
|
||||||
|
} catch (err) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
/**
|
||||||
|
* `formValues` is to trigger recomputation when form is updated.
|
||||||
|
* `getValues` is for convenience as it supports paths for fields like `getValues('foo.bar.baz')`.
|
||||||
|
*/
|
||||||
|
}, [schema, formValues, getValues]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (
|
||||||
|
schema.type === 'dropdown' &&
|
||||||
|
stepId &&
|
||||||
|
schema.additionalFields &&
|
||||||
|
computedVariables
|
||||||
|
) {
|
||||||
|
getDynamicFields({
|
||||||
|
variables: {
|
||||||
|
stepId,
|
||||||
|
...computedVariables,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [getDynamicFields, stepId, schema, computedVariables]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
called,
|
||||||
|
data: data?.getDynamicFields as IField[] | undefined,
|
||||||
|
loading,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useDynamicFields;
|
16
packages/web/src/hooks/usePaymentPortalUrl.ee.ts
Normal file
16
packages/web/src/hooks/usePaymentPortalUrl.ee.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { useQuery } from '@apollo/client';
|
||||||
|
import { GET_PAYMENT_PORTAL_URL } from 'graphql/queries/get-payment-portal-url.ee';
|
||||||
|
|
||||||
|
type UsePaymentPortalUrlReturn = {
|
||||||
|
url: string;
|
||||||
|
loading: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function usePaymentPortalUrl(): UsePaymentPortalUrlReturn {
|
||||||
|
const { data, loading } = useQuery(GET_PAYMENT_PORTAL_URL);
|
||||||
|
|
||||||
|
return {
|
||||||
|
url: data?.getPaymentPortalUrl?.url,
|
||||||
|
loading
|
||||||
|
};
|
||||||
|
}
|
30
packages/web/src/hooks/useUsageData.ee.ts
Normal file
30
packages/web/src/hooks/useUsageData.ee.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { useQuery } from '@apollo/client';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
|
|
||||||
|
import { GET_USAGE_DATA } from 'graphql/queries/get-usage-data.ee';
|
||||||
|
|
||||||
|
type UseUsageDataReturn = {
|
||||||
|
name: string;
|
||||||
|
allowedTaskCount: number;
|
||||||
|
consumedTaskCount: number;
|
||||||
|
remainingTaskCount: number;
|
||||||
|
nextResetAt: DateTime;
|
||||||
|
loading: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function useUsageData(): UseUsageDataReturn {
|
||||||
|
const { data, loading } = useQuery(GET_USAGE_DATA);
|
||||||
|
|
||||||
|
const usageData = data?.getUsageData;
|
||||||
|
const nextResetAt = usageData?.nextResetAt;
|
||||||
|
const nextResetAtDateTimeObject = nextResetAt && DateTime.fromMillis(Number(nextResetAt));
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: usageData?.name,
|
||||||
|
allowedTaskCount: usageData?.allowedTaskCount,
|
||||||
|
consumedTaskCount: usageData?.consumedTaskCount,
|
||||||
|
remainingTaskCount: usageData?.remainingTaskCount,
|
||||||
|
nextResetAt: nextResetAtDateTimeObject,
|
||||||
|
loading
|
||||||
|
};
|
||||||
|
}
|
@@ -5,7 +5,9 @@ import IntlProvider from 'components/IntlProvider';
|
|||||||
import ApolloProvider from 'components/ApolloProvider';
|
import ApolloProvider from 'components/ApolloProvider';
|
||||||
import SnackbarProvider from 'components/SnackbarProvider';
|
import SnackbarProvider from 'components/SnackbarProvider';
|
||||||
import { AuthenticationProvider } from 'contexts/Authentication';
|
import { AuthenticationProvider } from 'contexts/Authentication';
|
||||||
|
import { AutomatischInfoProvider } from 'contexts/AutomatischInfo';
|
||||||
import Router from 'components/Router';
|
import Router from 'components/Router';
|
||||||
|
import LiveChat from 'components/LiveChat/index.ee';
|
||||||
import routes from 'routes';
|
import routes from 'routes';
|
||||||
import reportWebVitals from './reportWebVitals';
|
import reportWebVitals from './reportWebVitals';
|
||||||
|
|
||||||
@@ -13,11 +15,15 @@ ReactDOM.render(
|
|||||||
<SnackbarProvider>
|
<SnackbarProvider>
|
||||||
<AuthenticationProvider>
|
<AuthenticationProvider>
|
||||||
<ApolloProvider>
|
<ApolloProvider>
|
||||||
<IntlProvider>
|
<AutomatischInfoProvider>
|
||||||
<ThemeProvider>
|
<IntlProvider>
|
||||||
<Router>{routes}</Router>
|
<ThemeProvider>
|
||||||
</ThemeProvider>
|
<Router>{routes}</Router>
|
||||||
</IntlProvider>
|
|
||||||
|
<LiveChat />
|
||||||
|
</ThemeProvider>
|
||||||
|
</IntlProvider>
|
||||||
|
</AutomatischInfoProvider>
|
||||||
</ApolloProvider>
|
</ApolloProvider>
|
||||||
</AuthenticationProvider>
|
</AuthenticationProvider>
|
||||||
</SnackbarProvider>,
|
</SnackbarProvider>,
|
||||||
|
@@ -11,6 +11,7 @@
|
|||||||
"settingsDrawer.myProfile": "My Profile",
|
"settingsDrawer.myProfile": "My Profile",
|
||||||
"settingsDrawer.goBack": "Go to the dashboard",
|
"settingsDrawer.goBack": "Go to the dashboard",
|
||||||
"settingsDrawer.notifications": "Notifications",
|
"settingsDrawer.notifications": "Notifications",
|
||||||
|
"settingsDrawer.billingAndUsage": "Billing and usage",
|
||||||
"app.connectionCount": "{count} connections",
|
"app.connectionCount": "{count} connections",
|
||||||
"app.flowCount": "{count} flows",
|
"app.flowCount": "{count} flows",
|
||||||
"app.addConnection": "Add connection",
|
"app.addConnection": "Add connection",
|
||||||
@@ -58,6 +59,8 @@
|
|||||||
"flowEditor.triggerEvent": "Trigger event",
|
"flowEditor.triggerEvent": "Trigger event",
|
||||||
"flowEditor.actionEvent": "Action event",
|
"flowEditor.actionEvent": "Action event",
|
||||||
"flowEditor.instantTriggerType": "Instant",
|
"flowEditor.instantTriggerType": "Instant",
|
||||||
|
"filterConditions.onlyContinueIf": "Only continue if…",
|
||||||
|
"filterConditions.orContinueIf": "OR continue if…",
|
||||||
"chooseConnectionSubstep.continue": "Continue",
|
"chooseConnectionSubstep.continue": "Continue",
|
||||||
"chooseConnectionSubstep.addNewConnection": "Add new connection",
|
"chooseConnectionSubstep.addNewConnection": "Add new connection",
|
||||||
"chooseConnectionSubstep.chooseConnection": "Choose connection",
|
"chooseConnectionSubstep.chooseConnection": "Choose connection",
|
||||||
@@ -87,10 +90,51 @@
|
|||||||
"profileSettings.updatedEmail": "Your email has been updated.",
|
"profileSettings.updatedEmail": "Your email has been updated.",
|
||||||
"profileSettings.updatedPassword": "Your password has been updated.",
|
"profileSettings.updatedPassword": "Your password has been updated.",
|
||||||
"profileSettings.updatePassword": "Update password",
|
"profileSettings.updatePassword": "Update password",
|
||||||
|
"profileSettings.deleteMyAccount": "Delete my account",
|
||||||
|
"profileSettings.deleteAccount": "Delete account",
|
||||||
|
"profileSettings.deleteAccountSubtitle": "This will permanently delete...",
|
||||||
|
"profileSettings.deleteAccountResult1": "Your account",
|
||||||
|
"profileSettings.deleteAccountResult2": "All your flows",
|
||||||
|
"profileSettings.deleteAccountResult3": "All your connections",
|
||||||
|
"profileSettings.deleteAccountResult4": "All execution history",
|
||||||
|
"billingAndUsageSettings.title": "Billing and usage",
|
||||||
|
"billingAndUsageSettings.paymentInformation": "Payment information",
|
||||||
|
"billingAndUsageSettings.paymentPortalInformation": "To manage your subscription, click <link>here</link> to go to the payment portal.",
|
||||||
|
"deleteAccountDialog.title": "Delete account?",
|
||||||
|
"deleteAccountDialog.description": "This will permanently delete your account and all the associated data with it.",
|
||||||
|
"deleteAccountDialog.cancel": "Cancel?",
|
||||||
|
"deleteAccountDialog.confirm": "Yes, delete it",
|
||||||
"notifications.title": "Notifications",
|
"notifications.title": "Notifications",
|
||||||
"notification.releasedAt": "Released {relativeDate}",
|
"notification.releasedAt": "Released {relativeDate}",
|
||||||
"webhookUrlInfo.title": "Your webhook URL",
|
"webhookUrlInfo.title": "Your webhook URL",
|
||||||
"webhookUrlInfo.description": "You'll need to configure your application with this webhook URL.",
|
"webhookUrlInfo.description": "You'll need to configure your application with this webhook URL.",
|
||||||
"webhookUrlInfo.helperText": "We've generated a custom webhook URL for you to send requests to. <link>Learn more about webhooks</link>.",
|
"webhookUrlInfo.helperText": "We've generated a custom webhook URL for you to send requests to. <link>Learn more about webhooks</link>.",
|
||||||
"webhookUrlInfo.copy": "Copy"
|
"webhookUrlInfo.copy": "Copy",
|
||||||
|
"signupForm.title": "Sign up",
|
||||||
|
"signupForm.fullNameFieldLabel": "Full name",
|
||||||
|
"signupForm.emailFieldLabel": "Email",
|
||||||
|
"signupForm.passwordFieldLabel": "Password",
|
||||||
|
"signupForm.confirmPasswordFieldLabel": "Confirm password",
|
||||||
|
"signupForm.submit": "Sign up",
|
||||||
|
"signupForm.validateEmail": "Email must be valid.",
|
||||||
|
"signupForm.passwordsMustMatch": "Passwords must match.",
|
||||||
|
"signupForm.mandatoryInput": "{inputName} is required.",
|
||||||
|
"loginForm.title": "Login",
|
||||||
|
"loginForm.emailFieldLabel": "Email",
|
||||||
|
"loginForm.passwordFieldLabel": "Password",
|
||||||
|
"loginForm.forgotPasswordText": "Forgot password?",
|
||||||
|
"loginForm.submit": "Login",
|
||||||
|
"loginForm.noAccount": "Don't have an Automatisch account yet?",
|
||||||
|
"loginForm.signUp": "Sign up",
|
||||||
|
"forgotPasswordForm.title": "Forgot password",
|
||||||
|
"forgotPasswordForm.submit": "Send reset instructions",
|
||||||
|
"forgotPasswordForm.instructionsSent": "The instructions have been sent!",
|
||||||
|
"forgotPasswordForm.emailFieldLabel": "Email",
|
||||||
|
"resetPasswordForm.passwordsMustMatch": "Passwords must match.",
|
||||||
|
"resetPasswordForm.mandatoryInput": "{inputName} is required.",
|
||||||
|
"resetPasswordForm.title": "Reset password",
|
||||||
|
"resetPasswordForm.submit": "Reset password",
|
||||||
|
"resetPasswordForm.passwordFieldLabel": "Password",
|
||||||
|
"resetPasswordForm.confirmPasswordFieldLabel": "Confirm password",
|
||||||
|
"resetPasswordForm.passwordUpdated": "The password has been updated. Now, you can login."
|
||||||
}
|
}
|
45
packages/web/src/pages/BillingAndUsageSettings/index.ee.tsx
Normal file
45
packages/web/src/pages/BillingAndUsageSettings/index.ee.tsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { Navigate } from 'react-router-dom';
|
||||||
|
import Grid from '@mui/material/Grid';
|
||||||
|
|
||||||
|
import * as URLS from 'config/urls'
|
||||||
|
import PaymentInformation from 'components/PaymentInformation/index.ee';
|
||||||
|
import UsageDataInformation from 'components/UsageDataInformation/index.ee';
|
||||||
|
import PageTitle from 'components/PageTitle';
|
||||||
|
import Container from 'components/Container';
|
||||||
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
|
import useCloud from 'hooks/useCloud';
|
||||||
|
|
||||||
|
function BillingAndUsageSettings() {
|
||||||
|
const isCloud = useCloud();
|
||||||
|
const formatMessage = useFormatMessage();
|
||||||
|
|
||||||
|
// redirect to the initial settings page
|
||||||
|
if (isCloud === false) {
|
||||||
|
return (<Navigate to={URLS.SETTINGS} replace={true} />)
|
||||||
|
}
|
||||||
|
|
||||||
|
// render nothing until we know if it's cloud or not
|
||||||
|
// here, `isCloud` is not `false`, but `undefined`
|
||||||
|
if (!isCloud) return <React.Fragment />
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container sx={{ py: 3, display: 'flex', justifyContent: 'center' }}>
|
||||||
|
<Grid container item xs={12} sm={9} md={8} lg={6}>
|
||||||
|
<Grid item xs={12} sx={{ mb: [2, 5] }}>
|
||||||
|
<PageTitle>{formatMessage('billingAndUsageSettings.title')}</PageTitle>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={12} sx={{ mb: 6 }}>
|
||||||
|
<UsageDataInformation />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<PaymentInformation />
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BillingAndUsageSettings;
|
14
packages/web/src/pages/ForgotPassword/index.ee.tsx
Normal file
14
packages/web/src/pages/ForgotPassword/index.ee.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Container from 'components/Container';
|
||||||
|
import ForgotPasswordForm from 'components/ForgotPasswordForm/index.ee';
|
||||||
|
|
||||||
|
export default function ForgotPassword(): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<Box sx={{ display: 'flex', flex: 1, alignItems: 'center' }}>
|
||||||
|
<Container maxWidth="sm">
|
||||||
|
<ForgotPasswordForm />
|
||||||
|
</Container>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user