Compare commits
3 Commits
AUT-157-AU
...
AUT-1024
Author | SHA1 | Date | |
---|---|---|---|
![]() |
6062cfafaf | ||
![]() |
5263e774d2 | ||
![]() |
22b4a04567 |
7
packages/backend/src/apps/eventbrite/assets/favicon.svg
Normal file
7
packages/backend/src/apps/eventbrite/assets/favicon.svg
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="256px" height="256px" viewBox="0 0 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
|
||||||
|
<g>
|
||||||
|
<circle fill="#F05537" cx="128" cy="128" r="128"></circle>
|
||||||
|
<path d="M117.475323,82.7290398 C136.772428,78.4407943 156.069532,86.3025777 166.790146,101.311437 L81.5017079,120.608542 C84.3605382,102.26438 98.1782181,87.0172853 117.475323,82.7290398 Z M167.266618,153.48509 C160.596014,163.252761 150.351872,170.161601 138.678314,172.782195 C119.38121,177.070441 99.8458692,169.208657 89.1252554,153.961562 L174.651929,134.664457 L188.469609,131.567391 L215.152026,125.611495 C214.91379,119.893834 214.199082,114.176173 213.007903,108.696749 C202.287289,62.7172275 155.354825,33.8906884 108.42236,44.6113021 C61.4898956,55.3319159 32.1868848,101.073201 43.1457344,147.290958 C54.1045839,193.508715 100.798813,222.097018 147.731277,211.376404 C175.366637,205.182272 196.807864,186.599875 207.766714,163.014525 L167.266618,153.48509 L167.266618,153.48509 Z" fill="#FFFFFF" fill-rule="nonzero"></path>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,19 @@
|
|||||||
|
import { URLSearchParams } from 'url';
|
||||||
|
|
||||||
|
export default async function generateAuthUrl($) {
|
||||||
|
const oauthRedirectUrlField = $.app.auth.fields.find(
|
||||||
|
(field) => field.key == 'oAuthRedirectUrl'
|
||||||
|
);
|
||||||
|
const redirectUri = oauthRedirectUrlField.value;
|
||||||
|
const searchParams = new URLSearchParams({
|
||||||
|
response_type: 'code',
|
||||||
|
client_id: $.auth.data.clientId,
|
||||||
|
redirect_uri: redirectUri,
|
||||||
|
});
|
||||||
|
|
||||||
|
const url = `https://www.eventbrite.com/oauth/authorize?${searchParams.toString()}`;
|
||||||
|
|
||||||
|
await $.auth.set({
|
||||||
|
url,
|
||||||
|
});
|
||||||
|
}
|
46
packages/backend/src/apps/eventbrite/auth/index.js
Normal file
46
packages/backend/src/apps/eventbrite/auth/index.js
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import generateAuthUrl from './generate-auth-url.js';
|
||||||
|
import verifyCredentials from './verify-credentials.js';
|
||||||
|
import isStillVerified from './is-still-verified.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'oAuthRedirectUrl',
|
||||||
|
label: 'OAuth Redirect URL',
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
readOnly: true,
|
||||||
|
value: '{WEB_APP_URL}/app/eventbrite/connections/add',
|
||||||
|
placeholder: null,
|
||||||
|
description:
|
||||||
|
'When asked to input a redirect URL in Eventbrite, enter the URL above.',
|
||||||
|
clickToCopy: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'clientId',
|
||||||
|
label: 'API Key',
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
readOnly: false,
|
||||||
|
value: null,
|
||||||
|
placeholder: null,
|
||||||
|
description: null,
|
||||||
|
clickToCopy: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'clientSecret',
|
||||||
|
label: 'Client Secret',
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
readOnly: false,
|
||||||
|
value: null,
|
||||||
|
placeholder: null,
|
||||||
|
description: null,
|
||||||
|
clickToCopy: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
generateAuthUrl,
|
||||||
|
verifyCredentials,
|
||||||
|
isStillVerified,
|
||||||
|
};
|
@@ -0,0 +1,8 @@
|
|||||||
|
import getCurrentUser from '../common/get-current-user.js';
|
||||||
|
|
||||||
|
const isStillVerified = async ($) => {
|
||||||
|
const currentUser = await getCurrentUser($);
|
||||||
|
return !!currentUser.id;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default isStillVerified;
|
@@ -0,0 +1,42 @@
|
|||||||
|
import getCurrentUser from '../common/get-current-user.js';
|
||||||
|
|
||||||
|
const verifyCredentials = async ($) => {
|
||||||
|
const oauthRedirectUrlField = $.app.auth.fields.find(
|
||||||
|
(field) => field.key == 'oAuthRedirectUrl'
|
||||||
|
);
|
||||||
|
const redirectUri = oauthRedirectUrlField.value;
|
||||||
|
const { data } = await $.http.post(
|
||||||
|
'https://www.eventbrite.com/oauth/token',
|
||||||
|
{
|
||||||
|
grant_type: 'authorization_code',
|
||||||
|
client_id: $.auth.data.clientId,
|
||||||
|
client_secret: $.auth.data.clientSecret,
|
||||||
|
code: $.auth.data.code,
|
||||||
|
redirect_uri: redirectUri,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
await $.auth.set({
|
||||||
|
accessToken: data.access_token,
|
||||||
|
tokenType: data.token_type,
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentUser = await getCurrentUser($);
|
||||||
|
|
||||||
|
const screenName = [currentUser.name, currentUser.emails[0].email]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' @ ');
|
||||||
|
|
||||||
|
await $.auth.set({
|
||||||
|
clientId: $.auth.data.clientId,
|
||||||
|
clientSecret: $.auth.data.clientSecret,
|
||||||
|
screenName,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default verifyCredentials;
|
@@ -0,0 +1,9 @@
|
|||||||
|
const addAuthHeader = ($, requestConfig) => {
|
||||||
|
if ($.auth.data?.accessToken) {
|
||||||
|
requestConfig.headers.Authorization = `Bearer ${$.auth.data.accessToken}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return requestConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default addAuthHeader;
|
@@ -0,0 +1,6 @@
|
|||||||
|
const getCurrentUser = async ($) => {
|
||||||
|
const { data: currentUser } = await $.http.get('/v3/users/me');
|
||||||
|
return currentUser;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getCurrentUser;
|
@@ -0,0 +1,4 @@
|
|||||||
|
import listEvents from './list-events/index.js';
|
||||||
|
import listOrganizations from './list-organizations/index.js';
|
||||||
|
|
||||||
|
export default [listEvents, listOrganizations];
|
@@ -0,0 +1,44 @@
|
|||||||
|
export default {
|
||||||
|
name: 'List events',
|
||||||
|
key: 'listEvents',
|
||||||
|
|
||||||
|
async run($) {
|
||||||
|
const events = {
|
||||||
|
data: [],
|
||||||
|
};
|
||||||
|
const organizationId = $.step.parameters.organizationId;
|
||||||
|
|
||||||
|
if (!organizationId) {
|
||||||
|
return events;
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
continuation: undefined,
|
||||||
|
order_by: 'created_desc',
|
||||||
|
};
|
||||||
|
|
||||||
|
do {
|
||||||
|
const { data } = await $.http.get(
|
||||||
|
`/v3/organizations/${organizationId}/events/`,
|
||||||
|
{
|
||||||
|
params,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (data.pagination.has_more_items) {
|
||||||
|
params.continuation = data.pagination.continuation;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.events) {
|
||||||
|
for (const event of data.events) {
|
||||||
|
events.data.push({
|
||||||
|
value: event.id,
|
||||||
|
name: `${event.name.text} (${event.status})`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} while (params.continuation);
|
||||||
|
|
||||||
|
return events;
|
||||||
|
},
|
||||||
|
};
|
@@ -0,0 +1,35 @@
|
|||||||
|
export default {
|
||||||
|
name: 'List organizations',
|
||||||
|
key: 'listOrganizations',
|
||||||
|
|
||||||
|
async run($) {
|
||||||
|
const organizations = {
|
||||||
|
data: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
continuation: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
do {
|
||||||
|
const { data } = await $.http.get('/v3/users/me/organizations', {
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data.pagination.has_more_items) {
|
||||||
|
params.continuation = data.pagination.continuation;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.organizations) {
|
||||||
|
for (const organization of data.organizations) {
|
||||||
|
organizations.data.push({
|
||||||
|
value: organization.id,
|
||||||
|
name: organization.name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} while (params.continuation);
|
||||||
|
|
||||||
|
return organizations;
|
||||||
|
},
|
||||||
|
};
|
20
packages/backend/src/apps/eventbrite/index.js
Normal file
20
packages/backend/src/apps/eventbrite/index.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import defineApp from '../../helpers/define-app.js';
|
||||||
|
import addAuthHeader from './common/add-auth-header.js';
|
||||||
|
import auth from './auth/index.js';
|
||||||
|
import dynamicData from './dynamic-data/index.js';
|
||||||
|
import triggers from './triggers/index.js';
|
||||||
|
|
||||||
|
export default defineApp({
|
||||||
|
name: 'Eventbrite',
|
||||||
|
key: 'eventbrite',
|
||||||
|
baseUrl: 'https://www.eventbrite.com',
|
||||||
|
apiBaseUrl: 'https://www.eventbriteapi.com',
|
||||||
|
iconUrl: '{BASE_URL}/apps/eventbrite/assets/favicon.svg',
|
||||||
|
authDocUrl: '{DOCS_URL}/apps/eventbrite/connection',
|
||||||
|
primaryColor: 'F05537',
|
||||||
|
supportsConnections: true,
|
||||||
|
beforeRequest: [addAuthHeader],
|
||||||
|
auth,
|
||||||
|
dynamicData,
|
||||||
|
triggers,
|
||||||
|
});
|
4
packages/backend/src/apps/eventbrite/triggers/index.js
Normal file
4
packages/backend/src/apps/eventbrite/triggers/index.js
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import newAttendeeCheckIn from './new-attendee-check-in/index.js';
|
||||||
|
import newEvents from './new-events/index.js';
|
||||||
|
|
||||||
|
export default [newAttendeeCheckIn, newEvents];
|
@@ -0,0 +1,120 @@
|
|||||||
|
import Crypto from 'crypto';
|
||||||
|
import defineTrigger from '../../../../helpers/define-trigger.js';
|
||||||
|
|
||||||
|
export default defineTrigger({
|
||||||
|
name: 'New attendee check in',
|
||||||
|
key: 'newAttendeeCheckIn',
|
||||||
|
type: 'webhook',
|
||||||
|
description: "Triggers when an attendee's barcode is scanned in.",
|
||||||
|
arguments: [
|
||||||
|
{
|
||||||
|
label: 'Organization',
|
||||||
|
key: 'organizationId',
|
||||||
|
type: 'dropdown',
|
||||||
|
required: true,
|
||||||
|
description: '',
|
||||||
|
variables: true,
|
||||||
|
source: {
|
||||||
|
type: 'query',
|
||||||
|
name: 'getDynamicData',
|
||||||
|
arguments: [
|
||||||
|
{
|
||||||
|
name: 'key',
|
||||||
|
value: 'listOrganizations',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Event',
|
||||||
|
key: 'eventId',
|
||||||
|
type: 'dropdown',
|
||||||
|
required: false,
|
||||||
|
description: '',
|
||||||
|
variables: true,
|
||||||
|
source: {
|
||||||
|
type: 'query',
|
||||||
|
name: 'getDynamicData',
|
||||||
|
arguments: [
|
||||||
|
{
|
||||||
|
name: 'key',
|
||||||
|
value: 'listEvents',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'parameters.organizationId',
|
||||||
|
value: '{parameters.organizationId}',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
async run($) {
|
||||||
|
const dataItem = {
|
||||||
|
raw: $.request.body,
|
||||||
|
meta: {
|
||||||
|
internalId: Crypto.randomUUID(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
$.pushTriggerItem(dataItem);
|
||||||
|
},
|
||||||
|
|
||||||
|
async testRun($) {
|
||||||
|
const eventId = $.step.parameters.eventId;
|
||||||
|
const organizationId = $.step.parameters.organizationId;
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
event_id: eventId,
|
||||||
|
};
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: { orders },
|
||||||
|
} = await $.http.get(`/v3/events/${eventId}/orders/`, params);
|
||||||
|
|
||||||
|
if (orders.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const computedWebhookEvent = {
|
||||||
|
config: {
|
||||||
|
action: 'barcode.checked_in',
|
||||||
|
user_id: organizationId,
|
||||||
|
webhook_id: '11111111',
|
||||||
|
endpoint_url: $.webhookUrl,
|
||||||
|
},
|
||||||
|
api_url: orders[0].resource_uri,
|
||||||
|
};
|
||||||
|
|
||||||
|
const dataItem = {
|
||||||
|
raw: computedWebhookEvent,
|
||||||
|
meta: {
|
||||||
|
internalId: computedWebhookEvent.user_id,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
$.pushTriggerItem(dataItem);
|
||||||
|
},
|
||||||
|
|
||||||
|
async registerHook($) {
|
||||||
|
const organizationId = $.step.parameters.organizationId;
|
||||||
|
const eventId = $.step.parameters.eventId;
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
endpoint_url: $.webhookUrl,
|
||||||
|
actions: 'attendee.checked_in',
|
||||||
|
event_id: eventId,
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data } = await $.http.post(
|
||||||
|
`/v3/organizations/${organizationId}/webhooks/`,
|
||||||
|
payload
|
||||||
|
);
|
||||||
|
|
||||||
|
await $.flow.setRemoteWebhookId(data.id);
|
||||||
|
},
|
||||||
|
|
||||||
|
async unregisterHook($) {
|
||||||
|
await $.http.delete(`/v3/webhooks/${$.flow.remoteWebhookId}/`);
|
||||||
|
},
|
||||||
|
});
|
@@ -0,0 +1,98 @@
|
|||||||
|
import Crypto from 'crypto';
|
||||||
|
import defineTrigger from '../../../../helpers/define-trigger.js';
|
||||||
|
|
||||||
|
export default defineTrigger({
|
||||||
|
name: 'New events',
|
||||||
|
key: 'newEvents',
|
||||||
|
type: 'webhook',
|
||||||
|
description:
|
||||||
|
'Triggers when a new event is published and live within an organization.',
|
||||||
|
arguments: [
|
||||||
|
{
|
||||||
|
label: 'Organization',
|
||||||
|
key: 'organizationId',
|
||||||
|
type: 'dropdown',
|
||||||
|
required: true,
|
||||||
|
description: '',
|
||||||
|
variables: true,
|
||||||
|
source: {
|
||||||
|
type: 'query',
|
||||||
|
name: 'getDynamicData',
|
||||||
|
arguments: [
|
||||||
|
{
|
||||||
|
name: 'key',
|
||||||
|
value: 'listOrganizations',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
async run($) {
|
||||||
|
const dataItem = {
|
||||||
|
raw: $.request.body,
|
||||||
|
meta: {
|
||||||
|
internalId: Crypto.randomUUID(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
$.pushTriggerItem(dataItem);
|
||||||
|
},
|
||||||
|
|
||||||
|
async testRun($) {
|
||||||
|
const organizationId = $.step.parameters.organizationId;
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
orderBy: 'created_desc',
|
||||||
|
status: 'all',
|
||||||
|
};
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: { events },
|
||||||
|
} = await $.http.get(`/v3/organizations/${organizationId}/events/`, params);
|
||||||
|
|
||||||
|
if (events.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const computedWebhookEvent = {
|
||||||
|
config: {
|
||||||
|
action: 'event.published',
|
||||||
|
user_id: events[0].organization_id,
|
||||||
|
webhook_id: '11111111',
|
||||||
|
endpoint_url: $.webhookUrl,
|
||||||
|
},
|
||||||
|
api_url: events[0].resource_uri,
|
||||||
|
};
|
||||||
|
|
||||||
|
const dataItem = {
|
||||||
|
raw: computedWebhookEvent,
|
||||||
|
meta: {
|
||||||
|
internalId: computedWebhookEvent.user_id,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
$.pushTriggerItem(dataItem);
|
||||||
|
},
|
||||||
|
|
||||||
|
async registerHook($) {
|
||||||
|
const organizationId = $.step.parameters.organizationId;
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
endpoint_url: $.webhookUrl,
|
||||||
|
actions: 'event.published',
|
||||||
|
event_id: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data } = await $.http.post(
|
||||||
|
`/v3/organizations/${organizationId}/webhooks/`,
|
||||||
|
payload
|
||||||
|
);
|
||||||
|
|
||||||
|
await $.flow.setRemoteWebhookId(data.id);
|
||||||
|
},
|
||||||
|
|
||||||
|
async unregisterHook($) {
|
||||||
|
await $.http.delete(`/v3/webhooks/${$.flow.remoteWebhookId}/`);
|
||||||
|
},
|
||||||
|
});
|
@@ -33,8 +33,8 @@ class User extends Base {
|
|||||||
fullName: { type: 'string', minLength: 1 },
|
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' },
|
password: { type: 'string' },
|
||||||
resetPasswordToken: { type: ['string', 'null'] },
|
resetPasswordToken: { type: 'string' },
|
||||||
resetPasswordTokenSentAt: { type: ['string', 'null'], format: 'date-time' },
|
resetPasswordTokenSentAt: { type: 'string' },
|
||||||
trialExpiryDate: { type: 'string' },
|
trialExpiryDate: { type: 'string' },
|
||||||
roleId: { type: 'string', format: 'uuid' },
|
roleId: { type: 'string', format: 'uuid' },
|
||||||
deletedAt: { type: 'string' },
|
deletedAt: { type: 'string' },
|
||||||
|
@@ -40,7 +40,6 @@ export const worker = new Worker(
|
|||||||
await user.$relatedQuery('usageData').withSoftDeleted().hardDelete();
|
await user.$relatedQuery('usageData').withSoftDeleted().hardDelete();
|
||||||
}
|
}
|
||||||
|
|
||||||
await user.$relatedQuery('accessTokens').withSoftDeleted().hardDelete();
|
|
||||||
await user.$query().withSoftDeleted().hardDelete();
|
await user.$query().withSoftDeleted().hardDelete();
|
||||||
},
|
},
|
||||||
{ connection: redisConfig }
|
{ connection: redisConfig }
|
||||||
|
@@ -113,6 +113,15 @@ export default defineConfig({
|
|||||||
{ text: 'Connection', link: '/apps/dropbox/connection' },
|
{ text: 'Connection', link: '/apps/dropbox/connection' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: 'Eventbrite',
|
||||||
|
collapsible: true,
|
||||||
|
collapsed: true,
|
||||||
|
items: [
|
||||||
|
{ text: 'Triggers', link: '/apps/eventbrite/triggers' },
|
||||||
|
{ text: 'Connection', link: '/apps/eventbrite/connection' },
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
text: 'Filter',
|
text: 'Filter',
|
||||||
collapsible: true,
|
collapsible: true,
|
||||||
|
18
packages/docs/pages/apps/eventbrite/connection.md
Normal file
18
packages/docs/pages/apps/eventbrite/connection.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Eventbrite
|
||||||
|
|
||||||
|
:::info
|
||||||
|
This page explains the steps you need to follow to set up the Eventbrite
|
||||||
|
connection in Automatisch. If any of the steps are outdated, please let us know!
|
||||||
|
:::
|
||||||
|
|
||||||
|
1. Go to your Eventbrite account settings.
|
||||||
|
2. Click on the **Developer Links**, and click on the **API Keys** button.
|
||||||
|
3. Click on the **Create API Key** button.
|
||||||
|
4. Fill the form.
|
||||||
|
5. Copy **OAuth Redirect URL** from Automatisch to **OAuth Redirect URI** field in the form.
|
||||||
|
6. After filling the form, click on the **Create Key** button.
|
||||||
|
7. Click on the **Show API key, client secret and tokens** in the middle of the page.
|
||||||
|
8. Copy the **API Key** value to the `API Key` field on Automatisch.
|
||||||
|
9. Copy the **Client secret** value to the `Client Secret` field on Automatisch.
|
||||||
|
10. Click **Submit** button on Automatisch.
|
||||||
|
11. Congrats! Start using your new Eventbrite connection within the flows.
|
14
packages/docs/pages/apps/eventbrite/triggers.md
Normal file
14
packages/docs/pages/apps/eventbrite/triggers.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
favicon: /favicons/eventbrite.svg
|
||||||
|
items:
|
||||||
|
- name: New attendee check in
|
||||||
|
desc: Triggers when an attendee's barcode is scanned in.
|
||||||
|
- name: New events
|
||||||
|
desc: Triggers when a new event is published and live within an organization.
|
||||||
|
---
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import CustomListing from '../../components/CustomListing.vue'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<CustomListing />
|
@@ -6,12 +6,16 @@ We use `lerna` with `yarn workspaces` to manage the mono repository. We have the
|
|||||||
.
|
.
|
||||||
├── packages
|
├── packages
|
||||||
│ ├── backend
|
│ ├── backend
|
||||||
|
│ ├── cli
|
||||||
│ ├── docs
|
│ ├── docs
|
||||||
│ ├── e2e-tests
|
│ ├── e2e-tests
|
||||||
|
│ ├── types
|
||||||
│ └── web
|
│ └── web
|
||||||
```
|
```
|
||||||
|
|
||||||
- `backend` - The backend package contains the backend application and all integrations.
|
- `backend` - The backend package contains the backend application and all integrations.
|
||||||
|
- `cli` - The cli package contains the CLI application of Automatisch.
|
||||||
- `docs` - The docs package contains the documentation website.
|
- `docs` - The docs package contains the documentation website.
|
||||||
- `e2e-tests` - The e2e-tests package contains the end-to-end tests for the internal usage.
|
- `e2e-tests` - The e2e-tests package contains the end-to-end tests for the internal usage.
|
||||||
|
- `types` - The types package contains the shared types for both the backend and web packages.
|
||||||
- `web` - The web package contains the frontend application of Automatisch.
|
- `web` - The web package contains the frontend application of Automatisch.
|
||||||
|
@@ -11,6 +11,7 @@ The following integrations are currently supported by Automatisch.
|
|||||||
- [Discord](/apps/discord/actions)
|
- [Discord](/apps/discord/actions)
|
||||||
- [Disqus](/apps/disqus/triggers)
|
- [Disqus](/apps/disqus/triggers)
|
||||||
- [Dropbox](/apps/dropbox/actions)
|
- [Dropbox](/apps/dropbox/actions)
|
||||||
|
- [Eventbrite](/apps/eventbrite/triggers)
|
||||||
- [Filter](/apps/filter/actions)
|
- [Filter](/apps/filter/actions)
|
||||||
- [Flickr](/apps/flickr/triggers)
|
- [Flickr](/apps/flickr/triggers)
|
||||||
- [Formatter](/apps/formatter/actions)
|
- [Formatter](/apps/formatter/actions)
|
||||||
|
7
packages/docs/pages/public/favicons/eventbrite.svg
Normal file
7
packages/docs/pages/public/favicons/eventbrite.svg
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="256px" height="256px" viewBox="0 0 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
|
||||||
|
<g>
|
||||||
|
<circle fill="#F05537" cx="128" cy="128" r="128"></circle>
|
||||||
|
<path d="M117.475323,82.7290398 C136.772428,78.4407943 156.069532,86.3025777 166.790146,101.311437 L81.5017079,120.608542 C84.3605382,102.26438 98.1782181,87.0172853 117.475323,82.7290398 Z M167.266618,153.48509 C160.596014,163.252761 150.351872,170.161601 138.678314,172.782195 C119.38121,177.070441 99.8458692,169.208657 89.1252554,153.961562 L174.651929,134.664457 L188.469609,131.567391 L215.152026,125.611495 C214.91379,119.893834 214.199082,114.176173 213.007903,108.696749 C202.287289,62.7172275 155.354825,33.8906884 108.42236,44.6113021 C61.4898956,55.3319159 32.1868848,101.073201 43.1457344,147.290958 C54.1045839,193.508715 100.798813,222.097018 147.731277,211.376404 C175.366637,205.182272 196.807864,186.599875 207.766714,163.014525 L167.266618,153.48509 L167.266618,153.48509 Z" fill="#FFFFFF" fill-rule="nonzero"></path>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
@@ -68,10 +68,7 @@ function AccountDropdownMenu(props) {
|
|||||||
AccountDropdownMenu.propTypes = {
|
AccountDropdownMenu.propTypes = {
|
||||||
open: PropTypes.bool.isRequired,
|
open: PropTypes.bool.isRequired,
|
||||||
onClose: PropTypes.func.isRequired,
|
onClose: PropTypes.func.isRequired,
|
||||||
anchorEl: PropTypes.oneOfType([
|
anchorEl: PropTypes.oneOfType([PropTypes.element, PropTypes.func]),
|
||||||
PropTypes.func,
|
|
||||||
PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
|
|
||||||
]),
|
|
||||||
id: PropTypes.string.isRequired,
|
id: PropTypes.string.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -8,7 +8,6 @@ import * as URLS from 'config/urls';
|
|||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
import { ConnectionPropType } from 'propTypes/propTypes';
|
import { ConnectionPropType } from 'propTypes/propTypes';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import Can from 'components/Can';
|
|
||||||
|
|
||||||
function ContextMenu(props) {
|
function ContextMenu(props) {
|
||||||
const {
|
const {
|
||||||
@@ -45,57 +44,34 @@ function ContextMenu(props) {
|
|||||||
hideBackdrop={false}
|
hideBackdrop={false}
|
||||||
anchorEl={anchorEl}
|
anchorEl={anchorEl}
|
||||||
>
|
>
|
||||||
<Can I="read" a="Flow" passThrough>
|
<MenuItem
|
||||||
{(allowed) => (
|
component={Link}
|
||||||
<MenuItem
|
to={URLS.APP_FLOWS_FOR_CONNECTION(appKey, connection.id)}
|
||||||
component={Link}
|
onClick={createActionHandler({ type: 'viewFlows' })}
|
||||||
to={URLS.APP_FLOWS_FOR_CONNECTION(appKey, connection.id)}
|
>
|
||||||
onClick={createActionHandler({ type: 'viewFlows' })}
|
{formatMessage('connection.viewFlows')}
|
||||||
disabled={!allowed}
|
</MenuItem>
|
||||||
>
|
|
||||||
{formatMessage('connection.viewFlows')}
|
|
||||||
</MenuItem>
|
|
||||||
)}
|
|
||||||
</Can>
|
|
||||||
|
|
||||||
<Can I="update" a="Connection" passThrough>
|
<MenuItem onClick={createActionHandler({ type: 'test' })}>
|
||||||
{(allowed) => (
|
{formatMessage('connection.testConnection')}
|
||||||
<MenuItem
|
</MenuItem>
|
||||||
onClick={createActionHandler({ type: 'test' })}
|
|
||||||
disabled={!allowed}
|
|
||||||
>
|
|
||||||
{formatMessage('connection.testConnection')}
|
|
||||||
</MenuItem>
|
|
||||||
)}
|
|
||||||
</Can>
|
|
||||||
|
|
||||||
<Can I="create" a="Connection" passThrough>
|
<MenuItem
|
||||||
{(allowed) => (
|
component={Link}
|
||||||
<MenuItem
|
disabled={disableReconnection}
|
||||||
component={Link}
|
to={URLS.APP_RECONNECT_CONNECTION(
|
||||||
disabled={!allowed || disableReconnection}
|
appKey,
|
||||||
to={URLS.APP_RECONNECT_CONNECTION(
|
connection.id,
|
||||||
appKey,
|
connection.appAuthClientId,
|
||||||
connection.id,
|
|
||||||
connection.appAuthClientId,
|
|
||||||
)}
|
|
||||||
onClick={createActionHandler({ type: 'reconnect' })}
|
|
||||||
>
|
|
||||||
{formatMessage('connection.reconnect')}
|
|
||||||
</MenuItem>
|
|
||||||
)}
|
)}
|
||||||
</Can>
|
onClick={createActionHandler({ type: 'reconnect' })}
|
||||||
|
>
|
||||||
|
{formatMessage('connection.reconnect')}
|
||||||
|
</MenuItem>
|
||||||
|
|
||||||
<Can I="delete" a="Connection" passThrough>
|
<MenuItem onClick={createActionHandler({ type: 'delete' })}>
|
||||||
{(allowed) => (
|
{formatMessage('connection.delete')}
|
||||||
<MenuItem
|
</MenuItem>
|
||||||
onClick={createActionHandler({ type: 'delete' })}
|
|
||||||
disabled={!allowed}
|
|
||||||
>
|
|
||||||
{formatMessage('connection.delete')}
|
|
||||||
</MenuItem>
|
|
||||||
)}
|
|
||||||
</Can>
|
|
||||||
</Menu>
|
</Menu>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -3,7 +3,6 @@ import PropTypes from 'prop-types';
|
|||||||
|
|
||||||
import AppConnectionRow from 'components/AppConnectionRow';
|
import AppConnectionRow from 'components/AppConnectionRow';
|
||||||
import NoResultFound from 'components/NoResultFound';
|
import NoResultFound from 'components/NoResultFound';
|
||||||
import Can from 'components/Can';
|
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
import * as URLS from 'config/urls';
|
import * as URLS from 'config/urls';
|
||||||
import useAppConnections from 'hooks/useAppConnections';
|
import useAppConnections from 'hooks/useAppConnections';
|
||||||
@@ -17,15 +16,11 @@ function AppConnections(props) {
|
|||||||
|
|
||||||
if (!hasConnections) {
|
if (!hasConnections) {
|
||||||
return (
|
return (
|
||||||
<Can I="create" a="Connection" passThrough>
|
<NoResultFound
|
||||||
{(allowed) => (
|
to={URLS.APP_ADD_CONNECTION(appKey)}
|
||||||
<NoResultFound
|
text={formatMessage('app.noConnections')}
|
||||||
text={formatMessage('app.noConnections')}
|
data-test="connections-no-results"
|
||||||
data-test="connections-no-results"
|
/>
|
||||||
{...(allowed && { to: URLS.APP_ADD_CONNECTION(appKey) })}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Can>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -5,7 +5,6 @@ import PaginationItem from '@mui/material/PaginationItem';
|
|||||||
|
|
||||||
import * as URLS from 'config/urls';
|
import * as URLS from 'config/urls';
|
||||||
import AppFlowRow from 'components/FlowRow';
|
import AppFlowRow from 'components/FlowRow';
|
||||||
import Can from 'components/Can';
|
|
||||||
import NoResultFound from 'components/NoResultFound';
|
import NoResultFound from 'components/NoResultFound';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
import useConnectionFlows from 'hooks/useConnectionFlows';
|
import useConnectionFlows from 'hooks/useConnectionFlows';
|
||||||
@@ -37,20 +36,11 @@ function AppFlows(props) {
|
|||||||
|
|
||||||
if (!hasFlows) {
|
if (!hasFlows) {
|
||||||
return (
|
return (
|
||||||
<Can I="create" a="Flow" passThrough>
|
<NoResultFound
|
||||||
{(allowed) => (
|
to={URLS.CREATE_FLOW_WITH_APP_AND_CONNECTION(appKey, connectionId)}
|
||||||
<NoResultFound
|
text={formatMessage('app.noFlows')}
|
||||||
text={formatMessage('app.noFlows')}
|
data-test="flows-no-results"
|
||||||
data-test="flows-no-results"
|
/>
|
||||||
{...(allowed && {
|
|
||||||
to: URLS.CREATE_FLOW_WITH_APP_AND_CONNECTION(
|
|
||||||
appKey,
|
|
||||||
connectionId
|
|
||||||
),
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Can>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -21,9 +21,7 @@ const CustomOptions = (props) => {
|
|||||||
label,
|
label,
|
||||||
initialTabIndex,
|
initialTabIndex,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const [activeTabIndex, setActiveTabIndex] = React.useState(undefined);
|
const [activeTabIndex, setActiveTabIndex] = React.useState(undefined);
|
||||||
|
|
||||||
React.useEffect(
|
React.useEffect(
|
||||||
function applyInitialActiveTabIndex() {
|
function applyInitialActiveTabIndex() {
|
||||||
setActiveTabIndex((currentActiveTabIndex) => {
|
setActiveTabIndex((currentActiveTabIndex) => {
|
||||||
@@ -35,7 +33,6 @@ const CustomOptions = (props) => {
|
|||||||
},
|
},
|
||||||
[initialTabIndex],
|
[initialTabIndex],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popper
|
<Popper
|
||||||
open={open}
|
open={open}
|
||||||
@@ -79,10 +76,7 @@ const CustomOptions = (props) => {
|
|||||||
|
|
||||||
CustomOptions.propTypes = {
|
CustomOptions.propTypes = {
|
||||||
open: PropTypes.bool.isRequired,
|
open: PropTypes.bool.isRequired,
|
||||||
anchorEl: PropTypes.oneOfType([
|
anchorEl: PropTypes.oneOfType([PropTypes.element, PropTypes.func]).isRequired,
|
||||||
PropTypes.func,
|
|
||||||
PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
|
|
||||||
]),
|
|
||||||
data: PropTypes.arrayOf(
|
data: PropTypes.arrayOf(
|
||||||
PropTypes.shape({
|
PropTypes.shape({
|
||||||
id: PropTypes.string.isRequired,
|
id: PropTypes.string.isRequired,
|
||||||
|
@@ -61,7 +61,6 @@ function ControlledCustomAutocomplete(props) {
|
|||||||
const [isSingleChoice, setSingleChoice] = React.useState(undefined);
|
const [isSingleChoice, setSingleChoice] = React.useState(undefined);
|
||||||
const priorStepsWithExecutions = React.useContext(StepExecutionsContext);
|
const priorStepsWithExecutions = React.useContext(StepExecutionsContext);
|
||||||
const editorRef = React.useRef(null);
|
const editorRef = React.useRef(null);
|
||||||
const mountedRef = React.useRef(false);
|
|
||||||
|
|
||||||
const renderElement = React.useCallback(
|
const renderElement = React.useCallback(
|
||||||
(props) => <Element {...props} disabled={disabled} />,
|
(props) => <Element {...props} disabled={disabled} />,
|
||||||
@@ -95,14 +94,10 @@ function ControlledCustomAutocomplete(props) {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (mountedRef.current) {
|
const hasDependencies = dependsOnValues.length;
|
||||||
const hasDependencies = dependsOnValues.length;
|
if (hasDependencies) {
|
||||||
if (hasDependencies) {
|
// Reset the field when a dependent has been updated
|
||||||
// Reset the field when a dependent has been updated
|
resetEditor(editor);
|
||||||
resetEditor(editor);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
mountedRef.current = true;
|
|
||||||
}
|
}
|
||||||
}, dependsOnValues);
|
}, dependsOnValues);
|
||||||
|
|
||||||
|
@@ -64,19 +64,11 @@ function DynamicField(props) {
|
|||||||
<Stack
|
<Stack
|
||||||
direction={{ xs: 'column', sm: 'row' }}
|
direction={{ xs: 'column', sm: 'row' }}
|
||||||
spacing={{ xs: 2 }}
|
spacing={{ xs: 2 }}
|
||||||
sx={{
|
sx={{ display: 'flex', flex: 1 }}
|
||||||
display: 'flex',
|
|
||||||
flex: 1,
|
|
||||||
minWidth: 0,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{fields.map((fieldSchema, fieldSchemaIndex) => (
|
{fields.map((fieldSchema, fieldSchemaIndex) => (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{ display: 'flex', flex: '1 0 0px' }}
|
||||||
display: 'flex',
|
|
||||||
flex: '1 0 0px',
|
|
||||||
minWidth: 0,
|
|
||||||
}}
|
|
||||||
key={`field-${field.__id}-${fieldSchemaIndex}`}
|
key={`field-${field.__id}-${fieldSchemaIndex}`}
|
||||||
>
|
>
|
||||||
<InputCreator
|
<InputCreator
|
||||||
|
@@ -59,25 +59,23 @@ export default function EditorLayout() {
|
|||||||
|
|
||||||
const onFlowStatusUpdate = React.useCallback(
|
const onFlowStatusUpdate = React.useCallback(
|
||||||
async (active) => {
|
async (active) => {
|
||||||
try {
|
await updateFlowStatus({
|
||||||
await updateFlowStatus({
|
variables: {
|
||||||
variables: {
|
input: {
|
||||||
input: {
|
id: flowId,
|
||||||
id: flowId,
|
active,
|
||||||
active,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
optimisticResponse: {
|
},
|
||||||
updateFlowStatus: {
|
optimisticResponse: {
|
||||||
__typename: 'Flow',
|
updateFlowStatus: {
|
||||||
id: flowId,
|
__typename: 'Flow',
|
||||||
active,
|
id: flowId,
|
||||||
},
|
active,
|
||||||
},
|
},
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
|
||||||
await queryClient.invalidateQueries({ queryKey: ['flows', flowId] });
|
await queryClient.invalidateQueries({ queryKey: ['flows', flowId] });
|
||||||
} catch (err) {}
|
|
||||||
},
|
},
|
||||||
[flowId, queryClient],
|
[flowId, queryClient],
|
||||||
);
|
);
|
||||||
|
@@ -1,10 +1,10 @@
|
|||||||
import { EdgeLabelRenderer, getStraightPath } from 'reactflow';
|
import { EdgeLabelRenderer, getStraightPath } from 'reactflow';
|
||||||
import IconButton from '@mui/material/IconButton';
|
import IconButton from '@mui/material/IconButton';
|
||||||
import AddIcon from '@mui/icons-material/Add';
|
import AddIcon from '@mui/icons-material/Add';
|
||||||
|
import { useMutation } from '@apollo/client';
|
||||||
|
import { CREATE_STEP } from 'graphql/mutations/create-step';
|
||||||
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { useContext } from 'react';
|
|
||||||
import { EdgesContext } from '../EditorNew';
|
|
||||||
|
|
||||||
export default function Edge({
|
export default function Edge({
|
||||||
sourceX,
|
sourceX,
|
||||||
@@ -12,11 +12,11 @@ export default function Edge({
|
|||||||
targetX,
|
targetX,
|
||||||
targetY,
|
targetY,
|
||||||
source,
|
source,
|
||||||
data: { laidOut },
|
data: { flowId, setCurrentStepId, flowActive, layouted },
|
||||||
}) {
|
}) {
|
||||||
const { stepCreationInProgress, flowActive, onAddStep } =
|
const [createStep, { loading: creationInProgress }] =
|
||||||
useContext(EdgesContext);
|
useMutation(CREATE_STEP);
|
||||||
|
const queryClient = useQueryClient();
|
||||||
const [edgePath, labelX, labelY] = getStraightPath({
|
const [edgePath, labelX, labelY] = getStraightPath({
|
||||||
sourceX,
|
sourceX,
|
||||||
sourceY,
|
sourceY,
|
||||||
@@ -24,19 +24,38 @@ export default function Edge({
|
|||||||
targetY,
|
targetY,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const addStep = async (previousStepId) => {
|
||||||
|
const mutationInput = {
|
||||||
|
previousStep: {
|
||||||
|
id: previousStepId,
|
||||||
|
},
|
||||||
|
flow: {
|
||||||
|
id: flowId,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const createdStep = await createStep({
|
||||||
|
variables: { input: mutationInput },
|
||||||
|
});
|
||||||
|
|
||||||
|
const createdStepId = createdStep.data.createStep.id;
|
||||||
|
setCurrentStepId(createdStepId);
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ['flows', flowId] });
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<EdgeLabelRenderer>
|
<EdgeLabelRenderer>
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={() => onAddStep(source)}
|
onClick={() => addStep(source)}
|
||||||
color="primary"
|
color="primary"
|
||||||
sx={{
|
sx={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
|
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
|
||||||
pointerEvents: 'all',
|
pointerEvents: 'all',
|
||||||
visibility: laidOut ? 'visible' : 'hidden',
|
visibility: layouted ? 'visible' : 'hidden',
|
||||||
}}
|
}}
|
||||||
disabled={stepCreationInProgress || flowActive}
|
disabled={creationInProgress || flowActive}
|
||||||
>
|
>
|
||||||
<AddIcon />
|
<AddIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@@ -52,6 +71,9 @@ Edge.propTypes = {
|
|||||||
targetY: PropTypes.number.isRequired,
|
targetY: PropTypes.number.isRequired,
|
||||||
source: PropTypes.string.isRequired,
|
source: PropTypes.string.isRequired,
|
||||||
data: PropTypes.shape({
|
data: PropTypes.shape({
|
||||||
laidOut: PropTypes.bool,
|
flowId: PropTypes.string.isRequired,
|
||||||
|
setCurrentStepId: PropTypes.func.isRequired,
|
||||||
|
flowActive: PropTypes.bool.isRequired,
|
||||||
|
layouted: PropTypes.bool,
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
};
|
};
|
||||||
|
@@ -1,81 +1,50 @@
|
|||||||
import { useEffect, useCallback, createContext, useRef } from 'react';
|
import { useEffect, useState, useCallback } from 'react';
|
||||||
import { useMutation } from '@apollo/client';
|
import { useMutation } from '@apollo/client';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { FlowPropType } from 'propTypes/propTypes';
|
import { FlowPropType } from 'propTypes/propTypes';
|
||||||
import ReactFlow, { useNodesState, useEdgesState } from 'reactflow';
|
import ReactFlow, { useNodesState, useEdgesState, addEdge } from 'reactflow';
|
||||||
import 'reactflow/dist/style.css';
|
import 'reactflow/dist/style.css';
|
||||||
import { UPDATE_STEP } from 'graphql/mutations/update-step';
|
import { UPDATE_STEP } from 'graphql/mutations/update-step';
|
||||||
import { CREATE_STEP } from 'graphql/mutations/create-step';
|
|
||||||
|
|
||||||
import { useAutoLayout } from './useAutoLayout';
|
import { useAutoLayout } from './useAutoLayout';
|
||||||
import { useScrollBoundaries } from './useScrollBoundaries';
|
import { useScrollBoundries } from './useScrollBoundries';
|
||||||
import FlowStepNode from './FlowStepNode/FlowStepNode';
|
import FlowStepNode from './FlowStepNode/FlowStepNode';
|
||||||
import Edge from './Edge/Edge';
|
import Edge from './Edge/Edge';
|
||||||
import InvisibleNode from './InvisibleNode/InvisibleNode';
|
import InvisibleNode from './InvisibleNode/InvisibleNode';
|
||||||
import { EditorWrapper } from './style';
|
import { EditorWrapper } from './style';
|
||||||
import {
|
|
||||||
generateEdgeId,
|
|
||||||
generateInitialEdges,
|
|
||||||
generateInitialNodes,
|
|
||||||
updatedCollapsedNodes,
|
|
||||||
} from './utils';
|
|
||||||
import { EDGE_TYPES, INVISIBLE_NODE_ID, NODE_TYPES } from './constants';
|
|
||||||
|
|
||||||
export const EdgesContext = createContext();
|
const nodeTypes = { flowStep: FlowStepNode, invisible: InvisibleNode };
|
||||||
export const NodesContext = createContext();
|
|
||||||
|
|
||||||
const nodeTypes = {
|
|
||||||
[NODE_TYPES.FLOW_STEP]: FlowStepNode,
|
|
||||||
[NODE_TYPES.INVISIBLE]: InvisibleNode,
|
|
||||||
};
|
|
||||||
|
|
||||||
const edgeTypes = {
|
const edgeTypes = {
|
||||||
[EDGE_TYPES.ADD_NODE_EDGE]: Edge,
|
addNodeEdge: Edge,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const INVISIBLE_NODE_ID = 'invisible-node';
|
||||||
|
|
||||||
|
const generateEdgeId = (sourceId, targetId) => `${sourceId}-${targetId}`;
|
||||||
|
|
||||||
const EditorNew = ({ flow }) => {
|
const EditorNew = ({ flow }) => {
|
||||||
|
const [triggerStep] = flow.steps;
|
||||||
|
const [currentStepId, setCurrentStepId] = useState(triggerStep.id);
|
||||||
|
|
||||||
const [updateStep] = useMutation(UPDATE_STEP);
|
const [updateStep] = useMutation(UPDATE_STEP);
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [createStep, { loading: stepCreationInProgress }] =
|
|
||||||
useMutation(CREATE_STEP);
|
|
||||||
|
|
||||||
const [nodes, setNodes, onNodesChange] = useNodesState(
|
|
||||||
generateInitialNodes(flow),
|
|
||||||
);
|
|
||||||
const [edges, setEdges, onEdgesChange] = useEdgesState(
|
|
||||||
generateInitialEdges(flow),
|
|
||||||
);
|
|
||||||
|
|
||||||
|
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
||||||
|
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
||||||
useAutoLayout();
|
useAutoLayout();
|
||||||
useScrollBoundaries();
|
useScrollBoundries();
|
||||||
|
|
||||||
const createdStepIdRef = useRef(null);
|
const onConnect = useCallback(
|
||||||
|
(params) => setEdges((eds) => addEdge(params, eds)),
|
||||||
|
[setEdges],
|
||||||
|
);
|
||||||
|
|
||||||
const openNextStep = useCallback(
|
const openNextStep = useCallback(
|
||||||
(currentStepId) => {
|
(nextStep) => () => {
|
||||||
setNodes((nodes) => {
|
setCurrentStepId(nextStep?.id);
|
||||||
const currentStepIndex = nodes.findIndex(
|
|
||||||
(node) => node.id === currentStepId,
|
|
||||||
);
|
|
||||||
if (currentStepIndex >= 0) {
|
|
||||||
const nextStep = nodes[currentStepIndex + 1];
|
|
||||||
return updatedCollapsedNodes(nodes, nextStep.id);
|
|
||||||
}
|
|
||||||
return nodes;
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
[setNodes],
|
[],
|
||||||
);
|
|
||||||
|
|
||||||
const onStepClose = useCallback(() => {
|
|
||||||
setNodes((nodes) => updatedCollapsedNodes(nodes));
|
|
||||||
}, [setNodes]);
|
|
||||||
|
|
||||||
const onStepOpen = useCallback(
|
|
||||||
(stepId) => {
|
|
||||||
setNodes((nodes) => updatedCollapsedNodes(nodes, stepId));
|
|
||||||
},
|
|
||||||
[setNodes],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const onStepChange = useCallback(
|
const onStepChange = useCallback(
|
||||||
@@ -107,166 +76,178 @@ const EditorNew = ({ flow }) => {
|
|||||||
[flow.id, updateStep, queryClient],
|
[flow.id, updateStep, queryClient],
|
||||||
);
|
);
|
||||||
|
|
||||||
const onAddStep = useCallback(
|
const generateEdges = useCallback((flow, prevEdges) => {
|
||||||
async (previousStepId) => {
|
const newEdges =
|
||||||
const mutationInput = {
|
flow.steps
|
||||||
previousStep: {
|
.map((step, i) => {
|
||||||
id: previousStepId,
|
const sourceId = step.id;
|
||||||
},
|
const targetId = flow.steps[i + 1]?.id;
|
||||||
flow: {
|
const edge = prevEdges?.find(
|
||||||
id: flow.id,
|
(edge) => edge.id === generateEdgeId(sourceId, targetId),
|
||||||
},
|
);
|
||||||
};
|
if (targetId) {
|
||||||
|
|
||||||
const {
|
|
||||||
data: { createStep: createdStep },
|
|
||||||
} = await createStep({
|
|
||||||
variables: { input: mutationInput },
|
|
||||||
});
|
|
||||||
|
|
||||||
const createdStepId = createdStep.id;
|
|
||||||
await queryClient.invalidateQueries({ queryKey: ['flows', flow.id] });
|
|
||||||
createdStepIdRef.current = createdStepId;
|
|
||||||
},
|
|
||||||
[flow.id, createStep, queryClient],
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (flow.steps.length + 1 !== nodes.length) {
|
|
||||||
setNodes((nodes) => {
|
|
||||||
const newNodes = flow.steps.map((step) => {
|
|
||||||
const createdStepId = createdStepIdRef.current;
|
|
||||||
const prevNode = nodes.find(({ id }) => id === step.id);
|
|
||||||
if (prevNode) {
|
|
||||||
return {
|
return {
|
||||||
...prevNode,
|
id: generateEdgeId(sourceId, targetId),
|
||||||
zIndex: createdStepId ? 0 : prevNode.zIndex,
|
source: sourceId,
|
||||||
|
target: targetId,
|
||||||
|
type: 'addNodeEdge',
|
||||||
data: {
|
data: {
|
||||||
...prevNode.data,
|
flowId: flow.id,
|
||||||
collapsed: createdStepId ? true : prevNode.data.collapsed,
|
flowActive: flow.active,
|
||||||
},
|
setCurrentStepId,
|
||||||
};
|
layouted: !!edge,
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
id: step.id,
|
|
||||||
type: NODE_TYPES.FLOW_STEP,
|
|
||||||
position: {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
},
|
|
||||||
zIndex: 1,
|
|
||||||
data: {
|
|
||||||
collapsed: false,
|
|
||||||
laidOut: false,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
.filter((edge) => !!edge) || [];
|
||||||
|
|
||||||
const prevInvisible = nodes.find(({ id }) => id === INVISIBLE_NODE_ID);
|
const lastStep = flow.steps[flow.steps.length - 1];
|
||||||
return [
|
|
||||||
...newNodes,
|
return lastStep
|
||||||
|
? [
|
||||||
|
...newEdges,
|
||||||
{
|
{
|
||||||
id: INVISIBLE_NODE_ID,
|
id: generateEdgeId(lastStep.id, INVISIBLE_NODE_ID),
|
||||||
type: NODE_TYPES.INVISIBLE,
|
source: lastStep.id,
|
||||||
position: {
|
target: INVISIBLE_NODE_ID,
|
||||||
x: prevInvisible?.position.x || 0,
|
type: 'addNodeEdge',
|
||||||
y: prevInvisible?.position.y || 0,
|
data: {
|
||||||
|
flowId: flow.id,
|
||||||
|
flowActive: flow.active,
|
||||||
|
setCurrentStepId,
|
||||||
|
layouted: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
]
|
||||||
|
: newEdges;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const generateNodes = useCallback(
|
||||||
|
(flow, prevNodes) => {
|
||||||
|
const newNodes = flow.steps.map((step, index) => {
|
||||||
|
const node = prevNodes?.find(({ id }) => id === step.id);
|
||||||
|
const collapsed = currentStepId !== step.id;
|
||||||
|
return {
|
||||||
|
id: step.id,
|
||||||
|
type: 'flowStep',
|
||||||
|
position: {
|
||||||
|
x: node ? node.position.x : 0,
|
||||||
|
y: node ? node.position.y : 0,
|
||||||
|
},
|
||||||
|
zIndex: collapsed ? 0 : 1,
|
||||||
|
data: {
|
||||||
|
step,
|
||||||
|
index: index,
|
||||||
|
flowId: flow.id,
|
||||||
|
collapsed,
|
||||||
|
openNextStep: openNextStep(flow.steps[index + 1]),
|
||||||
|
onOpen: () => setCurrentStepId(step.id),
|
||||||
|
onClose: () => setCurrentStepId(null),
|
||||||
|
onChange: onStepChange,
|
||||||
|
layouted: !!node,
|
||||||
|
},
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
setEdges((edges) => {
|
const prevInvisibleNode = nodes.find((node) => node.type === 'invisible');
|
||||||
const newEdges = flow.steps
|
|
||||||
.map((step, i) => {
|
|
||||||
const sourceId = step.id;
|
|
||||||
const targetId = flow.steps[i + 1]?.id;
|
|
||||||
const edge = edges?.find(
|
|
||||||
(edge) => edge.id === generateEdgeId(sourceId, targetId),
|
|
||||||
);
|
|
||||||
if (targetId) {
|
|
||||||
return {
|
|
||||||
id: generateEdgeId(sourceId, targetId),
|
|
||||||
source: sourceId,
|
|
||||||
target: targetId,
|
|
||||||
type: 'addNodeEdge',
|
|
||||||
data: {
|
|
||||||
laidOut: edge ? edge?.data.laidOut : false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
})
|
|
||||||
.filter((edge) => !!edge);
|
|
||||||
|
|
||||||
const lastStep = flow.steps[flow.steps.length - 1];
|
return [
|
||||||
const lastEdge = edges[edges.length - 1];
|
...newNodes,
|
||||||
|
{
|
||||||
|
id: INVISIBLE_NODE_ID,
|
||||||
|
type: 'invisible',
|
||||||
|
position: {
|
||||||
|
x: prevInvisibleNode ? prevInvisibleNode.position.x : 0,
|
||||||
|
y: prevInvisibleNode ? prevInvisibleNode.position.y : 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
[currentStepId, nodes, onStepChange, openNextStep],
|
||||||
|
);
|
||||||
|
|
||||||
return lastStep
|
const updateNodesData = useCallback(
|
||||||
? [
|
(steps) => {
|
||||||
...newEdges,
|
setNodes((nodes) =>
|
||||||
{
|
nodes.map((node) => {
|
||||||
id: generateEdgeId(lastStep.id, INVISIBLE_NODE_ID),
|
const step = steps.find((step) => step.id === node.id);
|
||||||
source: lastStep.id,
|
if (step) {
|
||||||
target: INVISIBLE_NODE_ID,
|
return { ...node, data: { ...node.data, step: { ...step } } };
|
||||||
type: 'addNodeEdge',
|
}
|
||||||
data: {
|
return node;
|
||||||
laidOut:
|
}),
|
||||||
lastEdge?.id ===
|
);
|
||||||
generateEdgeId(lastStep.id, INVISIBLE_NODE_ID)
|
},
|
||||||
? lastEdge?.data.laidOut
|
[setNodes],
|
||||||
: false,
|
);
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: newEdges;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (createdStepIdRef.current) {
|
const updateEdgesData = useCallback(
|
||||||
createdStepIdRef.current = null;
|
(flow) => {
|
||||||
}
|
setEdges((edges) =>
|
||||||
|
edges.map((edge) => {
|
||||||
|
return {
|
||||||
|
...edge,
|
||||||
|
data: { ...edge.data, flowId: flow.id, flowActive: flow.active },
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[setEdges],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setNodes(
|
||||||
|
nodes.map((node) => {
|
||||||
|
if (node.type === 'flowStep') {
|
||||||
|
const collapsed = currentStepId !== node.data.step.id;
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
zIndex: collapsed ? 0 : 1,
|
||||||
|
data: {
|
||||||
|
...node.data,
|
||||||
|
collapsed,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return node;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}, [currentStepId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (flow.steps.length + 1 !== nodes.length) {
|
||||||
|
const newNodes = generateNodes(flow, nodes);
|
||||||
|
const newEdges = generateEdges(flow, edges);
|
||||||
|
|
||||||
|
setNodes(newNodes);
|
||||||
|
setEdges(newEdges);
|
||||||
|
} else {
|
||||||
|
updateNodesData(flow.steps);
|
||||||
|
updateEdgesData(flow);
|
||||||
}
|
}
|
||||||
}, [flow.steps]);
|
}, [flow]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NodesContext.Provider
|
<EditorWrapper direction="column">
|
||||||
value={{
|
<ReactFlow
|
||||||
openNextStep,
|
nodes={nodes}
|
||||||
onStepOpen,
|
edges={edges}
|
||||||
onStepClose,
|
onNodesChange={onNodesChange}
|
||||||
onStepChange,
|
onEdgesChange={onEdgesChange}
|
||||||
flowId: flow.id,
|
onConnect={onConnect}
|
||||||
steps: flow.steps,
|
nodeTypes={nodeTypes}
|
||||||
}}
|
edgeTypes={edgeTypes}
|
||||||
>
|
panOnScroll
|
||||||
<EdgesContext.Provider
|
panOnScrollMode="vertical"
|
||||||
value={{
|
panOnDrag={false}
|
||||||
stepCreationInProgress,
|
zoomOnScroll={false}
|
||||||
onAddStep,
|
zoomOnPinch={false}
|
||||||
flowActive: flow.active,
|
zoomOnDoubleClick={false}
|
||||||
}}
|
panActivationKeyCode={null}
|
||||||
>
|
proOptions={{ hideAttribution: true }}
|
||||||
<EditorWrapper direction="column">
|
/>
|
||||||
<ReactFlow
|
</EditorWrapper>
|
||||||
nodes={nodes}
|
|
||||||
edges={edges}
|
|
||||||
onNodesChange={onNodesChange}
|
|
||||||
onEdgesChange={onEdgesChange}
|
|
||||||
nodeTypes={nodeTypes}
|
|
||||||
edgeTypes={edgeTypes}
|
|
||||||
panOnScroll
|
|
||||||
panOnScrollMode="vertical"
|
|
||||||
panOnDrag={false}
|
|
||||||
zoomOnScroll={false}
|
|
||||||
zoomOnPinch={false}
|
|
||||||
zoomOnDoubleClick={false}
|
|
||||||
panActivationKeyCode={null}
|
|
||||||
proOptions={{ hideAttribution: true }}
|
|
||||||
/>
|
|
||||||
</EditorWrapper>
|
|
||||||
</EdgesContext.Provider>
|
|
||||||
</NodesContext.Provider>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -1,23 +1,30 @@
|
|||||||
import { Handle, Position } from 'reactflow';
|
import { Handle, Position } from 'reactflow';
|
||||||
|
import { Box } from '@mui/material';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import FlowStep from 'components/FlowStep';
|
import FlowStep from 'components/FlowStep';
|
||||||
|
import { StepPropType } from 'propTypes/propTypes';
|
||||||
|
|
||||||
import { NodeWrapper, NodeInnerWrapper } from './style.js';
|
import { NodeWrapper, NodeInnerWrapper } from './style.js';
|
||||||
import { useContext } from 'react';
|
|
||||||
import { NodesContext } from '../EditorNew.jsx';
|
|
||||||
|
|
||||||
function FlowStepNode({ data: { collapsed, laidOut }, id }) {
|
|
||||||
const { openNextStep, onStepOpen, onStepClose, onStepChange, flowId, steps } =
|
|
||||||
useContext(NodesContext);
|
|
||||||
|
|
||||||
const step = steps.find(({ id: stepId }) => stepId === id);
|
|
||||||
|
|
||||||
|
function FlowStepNode({
|
||||||
|
data: {
|
||||||
|
step,
|
||||||
|
index,
|
||||||
|
flowId,
|
||||||
|
collapsed,
|
||||||
|
openNextStep,
|
||||||
|
onOpen,
|
||||||
|
onClose,
|
||||||
|
onChange,
|
||||||
|
layouted,
|
||||||
|
},
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<NodeWrapper
|
<NodeWrapper
|
||||||
className="nodrag"
|
className="nodrag"
|
||||||
sx={{
|
sx={{
|
||||||
visibility: laidOut ? 'visible' : 'hidden',
|
visibility: layouted ? 'visible' : 'hidden',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<NodeInnerWrapper>
|
<NodeInnerWrapper>
|
||||||
@@ -27,17 +34,16 @@ function FlowStepNode({ data: { collapsed, laidOut }, id }) {
|
|||||||
isConnectable={false}
|
isConnectable={false}
|
||||||
style={{ visibility: 'hidden' }}
|
style={{ visibility: 'hidden' }}
|
||||||
/>
|
/>
|
||||||
{step && (
|
<FlowStep
|
||||||
<FlowStep
|
step={step}
|
||||||
step={step}
|
index={index + 1}
|
||||||
collapsed={collapsed}
|
collapsed={collapsed}
|
||||||
onOpen={() => onStepOpen(step.id)}
|
onOpen={onOpen}
|
||||||
onClose={onStepClose}
|
onClose={onClose}
|
||||||
onChange={onStepChange}
|
onChange={onChange}
|
||||||
flowId={flowId}
|
flowId={flowId}
|
||||||
onContinue={() => openNextStep(step.id)}
|
onContinue={openNextStep}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
<Handle
|
<Handle
|
||||||
type="source"
|
type="source"
|
||||||
position={Position.Bottom}
|
position={Position.Bottom}
|
||||||
@@ -50,10 +56,16 @@ function FlowStepNode({ data: { collapsed, laidOut }, id }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
FlowStepNode.propTypes = {
|
FlowStepNode.propTypes = {
|
||||||
id: PropTypes.string,
|
|
||||||
data: PropTypes.shape({
|
data: PropTypes.shape({
|
||||||
|
step: StepPropType.isRequired,
|
||||||
|
index: PropTypes.number.isRequired,
|
||||||
|
flowId: PropTypes.string.isRequired,
|
||||||
collapsed: PropTypes.bool.isRequired,
|
collapsed: PropTypes.bool.isRequired,
|
||||||
laidOut: PropTypes.bool.isRequired,
|
openNextStep: PropTypes.func.isRequired,
|
||||||
|
onOpen: PropTypes.func.isRequired,
|
||||||
|
onClose: PropTypes.func.isRequired,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
layouted: PropTypes.bool.isRequired,
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -1,10 +0,0 @@
|
|||||||
export const INVISIBLE_NODE_ID = 'invisible-node';
|
|
||||||
|
|
||||||
export const NODE_TYPES = {
|
|
||||||
FLOW_STEP: 'flowStep',
|
|
||||||
INVISIBLE: 'invisible',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const EDGE_TYPES = {
|
|
||||||
ADD_NODE_EDGE: 'addNodeEdge',
|
|
||||||
};
|
|
@@ -4,7 +4,7 @@ import { usePrevious } from 'hooks/usePrevious';
|
|||||||
import { isEqual } from 'lodash';
|
import { isEqual } from 'lodash';
|
||||||
import { useNodesInitialized, useNodes, useReactFlow } from 'reactflow';
|
import { useNodesInitialized, useNodes, useReactFlow } from 'reactflow';
|
||||||
|
|
||||||
const getLaidOutElements = (nodes, edges) => {
|
const getLayoutedElements = (nodes, edges) => {
|
||||||
const graph = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
|
const graph = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
|
||||||
graph.setGraph({
|
graph.setGraph({
|
||||||
rankdir: 'TB',
|
rankdir: 'TB',
|
||||||
@@ -36,18 +36,18 @@ export const useAutoLayout = () => {
|
|||||||
|
|
||||||
const onLayout = useCallback(
|
const onLayout = useCallback(
|
||||||
(nodes, edges) => {
|
(nodes, edges) => {
|
||||||
const laidOutElements = getLaidOutElements(nodes, edges);
|
const layoutedElements = getLayoutedElements(nodes, edges);
|
||||||
|
|
||||||
setNodes([
|
setNodes([
|
||||||
...laidOutElements.nodes.map((node) => ({
|
...layoutedElements.nodes.map((node) => ({
|
||||||
...node,
|
...node,
|
||||||
data: { ...node.data, laidOut: true },
|
data: { ...node.data, layouted: true },
|
||||||
})),
|
})),
|
||||||
]);
|
]);
|
||||||
setEdges([
|
setEdges([
|
||||||
...laidOutElements.edges.map((edge) => ({
|
...layoutedElements.edges.map((edge) => ({
|
||||||
...edge,
|
...edge,
|
||||||
data: { ...edge.data, laidOut: true },
|
data: { ...edge.data, layouted: true },
|
||||||
})),
|
})),
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useViewport, useReactFlow } from 'reactflow';
|
import { useViewport, useReactFlow } from 'reactflow';
|
||||||
|
|
||||||
export const useScrollBoundaries = () => {
|
export const useScrollBoundries = () => {
|
||||||
const { setViewport } = useReactFlow();
|
const { setViewport } = useReactFlow();
|
||||||
const { x, y, zoom } = useViewport();
|
const { x, y, zoom } = useViewport();
|
||||||
|
|
@@ -1,88 +0,0 @@
|
|||||||
import { INVISIBLE_NODE_ID, NODE_TYPES } from './constants';
|
|
||||||
|
|
||||||
export const generateEdgeId = (sourceId, targetId) => `${sourceId}-${targetId}`;
|
|
||||||
|
|
||||||
export const updatedCollapsedNodes = (nodes, openStepId) => {
|
|
||||||
return nodes.map((node) => {
|
|
||||||
if (node.type !== NODE_TYPES.FLOW_STEP) {
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
|
|
||||||
const collapsed = node.id !== openStepId;
|
|
||||||
return {
|
|
||||||
...node,
|
|
||||||
zIndex: collapsed ? 0 : 1,
|
|
||||||
data: { ...node.data, collapsed },
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const generateInitialNodes = (flow) => {
|
|
||||||
const newNodes = flow.steps.map((step, index) => {
|
|
||||||
const collapsed = index !== 0;
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: step.id,
|
|
||||||
type: NODE_TYPES.FLOW_STEP,
|
|
||||||
position: {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
},
|
|
||||||
zIndex: collapsed ? 0 : 1,
|
|
||||||
data: {
|
|
||||||
collapsed,
|
|
||||||
laidOut: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return [
|
|
||||||
...newNodes,
|
|
||||||
{
|
|
||||||
id: INVISIBLE_NODE_ID,
|
|
||||||
type: NODE_TYPES.INVISIBLE,
|
|
||||||
position: {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const generateInitialEdges = (flow) => {
|
|
||||||
const newEdges = flow.steps
|
|
||||||
.map((step, i) => {
|
|
||||||
const sourceId = step.id;
|
|
||||||
const targetId = flow.steps[i + 1]?.id;
|
|
||||||
if (targetId) {
|
|
||||||
return {
|
|
||||||
id: generateEdgeId(sourceId, targetId),
|
|
||||||
source: sourceId,
|
|
||||||
target: targetId,
|
|
||||||
type: 'addNodeEdge',
|
|
||||||
data: {
|
|
||||||
laidOut: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
})
|
|
||||||
.filter((edge) => !!edge);
|
|
||||||
|
|
||||||
const lastStep = flow.steps[flow.steps.length - 1];
|
|
||||||
|
|
||||||
return lastStep
|
|
||||||
? [
|
|
||||||
...newEdges,
|
|
||||||
{
|
|
||||||
id: generateEdgeId(lastStep.id, INVISIBLE_NODE_ID),
|
|
||||||
source: lastStep.id,
|
|
||||||
target: INVISIBLE_NODE_ID,
|
|
||||||
type: 'addNodeEdge',
|
|
||||||
data: {
|
|
||||||
laidOut: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: newEdges;
|
|
||||||
};
|
|
@@ -11,6 +11,9 @@ import IconButton from '@mui/material/IconButton';
|
|||||||
import ErrorIcon from '@mui/icons-material/Error';
|
import ErrorIcon from '@mui/icons-material/Error';
|
||||||
import CircularProgress from '@mui/material/CircularProgress';
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
||||||
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
|
import * as yup from 'yup';
|
||||||
|
|
||||||
import { EditorContext } from 'contexts/Editor';
|
import { EditorContext } from 'contexts/Editor';
|
||||||
import { StepExecutionsProvider } from 'contexts/StepExecutions';
|
import { StepExecutionsProvider } from 'contexts/StepExecutions';
|
||||||
import TestSubstep from 'components/TestSubstep';
|
import TestSubstep from 'components/TestSubstep';
|
||||||
@@ -30,18 +33,77 @@ import {
|
|||||||
Header,
|
Header,
|
||||||
Wrapper,
|
Wrapper,
|
||||||
} from './style';
|
} from './style';
|
||||||
|
import isEmpty from 'helpers/isEmpty';
|
||||||
import { StepPropType } from 'propTypes/propTypes';
|
import { StepPropType } from 'propTypes/propTypes';
|
||||||
import useTriggers from 'hooks/useTriggers';
|
import useTriggers from 'hooks/useTriggers';
|
||||||
import useActions from 'hooks/useActions';
|
import useActions from 'hooks/useActions';
|
||||||
import useTriggerSubsteps from 'hooks/useTriggerSubsteps';
|
import useTriggerSubsteps from 'hooks/useTriggerSubsteps';
|
||||||
import useActionSubsteps from 'hooks/useActionSubsteps';
|
import useActionSubsteps from 'hooks/useActionSubsteps';
|
||||||
import useStepWithTestExecutions from 'hooks/useStepWithTestExecutions';
|
import useStepWithTestExecutions from 'hooks/useStepWithTestExecutions';
|
||||||
import { validationSchemaResolver } from './validation';
|
|
||||||
import { isEqual } from 'lodash';
|
|
||||||
|
|
||||||
const validIcon = <CheckCircleIcon color="success" />;
|
const validIcon = <CheckCircleIcon color="success" />;
|
||||||
const errorIcon = <ErrorIcon color="error" />;
|
const errorIcon = <ErrorIcon color="error" />;
|
||||||
|
|
||||||
|
function generateValidationSchema(substeps) {
|
||||||
|
const fieldValidations = substeps?.reduce(
|
||||||
|
(allValidations, { arguments: args }) => {
|
||||||
|
if (!args || !Array.isArray(args)) return allValidations;
|
||||||
|
const substepArgumentValidations = {};
|
||||||
|
for (const arg of args) {
|
||||||
|
const { key, required } = arg;
|
||||||
|
// base validation for the field if not exists
|
||||||
|
if (!substepArgumentValidations[key]) {
|
||||||
|
substepArgumentValidations[key] = yup.mixed();
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
typeof substepArgumentValidations[key] === 'object' &&
|
||||||
|
(arg.type === 'string' || arg.type === 'dropdown')
|
||||||
|
) {
|
||||||
|
// if the field is required, add the required validation
|
||||||
|
if (required) {
|
||||||
|
substepArgumentValidations[key] = substepArgumentValidations[key]
|
||||||
|
.required(`${key} is required.`)
|
||||||
|
.test(
|
||||||
|
'empty-check',
|
||||||
|
`${key} must be not empty`,
|
||||||
|
(value) => !isEmpty(value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// if the field depends on another field, add the dependsOn required validation
|
||||||
|
if (Array.isArray(arg.dependsOn) && arg.dependsOn.length > 0) {
|
||||||
|
for (const dependsOnKey of arg.dependsOn) {
|
||||||
|
const missingDependencyValueMessage = `We're having trouble loading '${key}' data as required field '${dependsOnKey}' is missing.`;
|
||||||
|
// TODO: make `dependsOnKey` agnostic to the field. However, nested validation schema is not supported.
|
||||||
|
// So the fields under the `parameters` key are subject to their siblings only and thus, `parameters.` is removed.
|
||||||
|
substepArgumentValidations[key] = substepArgumentValidations[
|
||||||
|
key
|
||||||
|
].when(`${dependsOnKey.replace('parameters.', '')}`, {
|
||||||
|
is: (value) => Boolean(value) === false,
|
||||||
|
then: (schema) =>
|
||||||
|
schema
|
||||||
|
.notOneOf([''], missingDependencyValueMessage)
|
||||||
|
.required(missingDependencyValueMessage),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...allValidations,
|
||||||
|
...substepArgumentValidations,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
const validationSchema = yup.object({
|
||||||
|
parameters: yup.object(fieldValidations),
|
||||||
|
});
|
||||||
|
|
||||||
|
return yupResolver(validationSchema);
|
||||||
|
}
|
||||||
|
|
||||||
function FlowStep(props) {
|
function FlowStep(props) {
|
||||||
const { collapsed, onChange, onContinue, flowId } = props;
|
const { collapsed, onChange, onContinue, flowId } = props;
|
||||||
const editorContext = React.useContext(EditorContext);
|
const editorContext = React.useContext(EditorContext);
|
||||||
@@ -52,10 +114,6 @@ function FlowStep(props) {
|
|||||||
const isAction = step.type === 'action';
|
const isAction = step.type === 'action';
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
const [currentSubstep, setCurrentSubstep] = React.useState(0);
|
const [currentSubstep, setCurrentSubstep] = React.useState(0);
|
||||||
const [formResolverContext, setFormResolverContext] = React.useState({
|
|
||||||
substeps: [],
|
|
||||||
additionalFields: {},
|
|
||||||
});
|
|
||||||
const useAppsOptions = {};
|
const useAppsOptions = {};
|
||||||
|
|
||||||
if (isTrigger) {
|
if (isTrigger) {
|
||||||
@@ -110,12 +168,6 @@ function FlowStep(props) {
|
|||||||
? triggerSubstepsData
|
? triggerSubstepsData
|
||||||
: actionSubstepsData || [];
|
: actionSubstepsData || [];
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (!isEqual(substeps, formResolverContext.substeps)) {
|
|
||||||
setFormResolverContext({ substeps, additionalFields: {} });
|
|
||||||
}
|
|
||||||
}, [substeps]);
|
|
||||||
|
|
||||||
const handleChange = React.useCallback(({ step }) => {
|
const handleChange = React.useCallback(({ step }) => {
|
||||||
onChange(step);
|
onChange(step);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -128,6 +180,11 @@ function FlowStep(props) {
|
|||||||
handleChange({ step: val });
|
handleChange({ step: val });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const stepValidationSchema = React.useMemo(
|
||||||
|
() => generateValidationSchema(substeps),
|
||||||
|
[substeps],
|
||||||
|
);
|
||||||
|
|
||||||
if (!apps?.data) {
|
if (!apps?.data) {
|
||||||
return (
|
return (
|
||||||
<CircularProgress
|
<CircularProgress
|
||||||
@@ -156,15 +213,6 @@ function FlowStep(props) {
|
|||||||
value !== substepIndex ? substepIndex : null,
|
value !== substepIndex ? substepIndex : null,
|
||||||
);
|
);
|
||||||
|
|
||||||
const addAdditionalFieldsValidation = (additionalFields) => {
|
|
||||||
if (additionalFields) {
|
|
||||||
setFormResolverContext((prev) => ({
|
|
||||||
...prev,
|
|
||||||
additionalFields: { ...prev.additionalFields, ...additionalFields },
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const validationStatusIcon =
|
const validationStatusIcon =
|
||||||
step.status === 'completed' ? validIcon : errorIcon;
|
step.status === 'completed' ? validIcon : errorIcon;
|
||||||
|
|
||||||
@@ -218,8 +266,7 @@ function FlowStep(props) {
|
|||||||
<Form
|
<Form
|
||||||
defaultValues={step}
|
defaultValues={step}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
resolver={validationSchemaResolver}
|
resolver={stepValidationSchema}
|
||||||
context={formResolverContext}
|
|
||||||
>
|
>
|
||||||
<ChooseAppAndEventSubstep
|
<ChooseAppAndEventSubstep
|
||||||
expanded={currentSubstep === 0}
|
expanded={currentSubstep === 0}
|
||||||
@@ -283,9 +330,6 @@ function FlowStep(props) {
|
|||||||
onSubmit={expandNextStep}
|
onSubmit={expandNextStep}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
step={step}
|
step={step}
|
||||||
addAdditionalFieldsValidation={
|
|
||||||
addAdditionalFieldsValidation
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
@@ -316,6 +360,7 @@ function FlowStep(props) {
|
|||||||
FlowStep.propTypes = {
|
FlowStep.propTypes = {
|
||||||
collapsed: PropTypes.bool,
|
collapsed: PropTypes.bool,
|
||||||
step: StepPropType.isRequired,
|
step: StepPropType.isRequired,
|
||||||
|
index: PropTypes.number,
|
||||||
onOpen: PropTypes.func,
|
onOpen: PropTypes.func,
|
||||||
onClose: PropTypes.func,
|
onClose: PropTypes.func,
|
||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
|
@@ -1,120 +0,0 @@
|
|||||||
import * as yup from 'yup';
|
|
||||||
import { yupResolver } from '@hookform/resolvers/yup';
|
|
||||||
import isEmpty from 'helpers/isEmpty';
|
|
||||||
|
|
||||||
function addRequiredValidation({ required, schema, key }) {
|
|
||||||
// if the field is required, add the required validation
|
|
||||||
if (required) {
|
|
||||||
return schema
|
|
||||||
.required(`${key} is required.`)
|
|
||||||
.test(
|
|
||||||
'empty-check',
|
|
||||||
`${key} must be not empty`,
|
|
||||||
(value) => !isEmpty(value),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return schema;
|
|
||||||
}
|
|
||||||
|
|
||||||
function addDependsOnValidation({ schema, dependsOn, key, args }) {
|
|
||||||
// if the field depends on another field, add the dependsOn required validation
|
|
||||||
if (Array.isArray(dependsOn) && dependsOn.length > 0) {
|
|
||||||
for (const dependsOnKey of dependsOn) {
|
|
||||||
const dependsOnKeyShort = dependsOnKey.replace('parameters.', '');
|
|
||||||
const dependsOnField = args.find(({ key }) => key === dependsOnKeyShort);
|
|
||||||
|
|
||||||
if (dependsOnField?.required) {
|
|
||||||
const missingDependencyValueMessage = `We're having trouble loading '${key}' data as required field '${dependsOnKey}' is missing.`;
|
|
||||||
|
|
||||||
// TODO: make `dependsOnKey` agnostic to the field. However, nested validation schema is not supported.
|
|
||||||
// So the fields under the `parameters` key are subject to their siblings only and thus, `parameters.` is removed.
|
|
||||||
return schema.when(dependsOnKeyShort, {
|
|
||||||
is: (dependsOnValue) => Boolean(dependsOnValue) === false,
|
|
||||||
then: (schema) =>
|
|
||||||
schema
|
|
||||||
.notOneOf([''], missingDependencyValueMessage)
|
|
||||||
.required(missingDependencyValueMessage),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return schema;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function validationSchemaResolver(data, context, options) {
|
|
||||||
const { substeps = [], additionalFields = {} } = context;
|
|
||||||
|
|
||||||
const fieldValidations = [
|
|
||||||
...substeps,
|
|
||||||
{
|
|
||||||
arguments: Object.values(additionalFields)
|
|
||||||
.filter((field) => !!field)
|
|
||||||
.flat(),
|
|
||||||
},
|
|
||||||
].reduce((allValidations, { arguments: args }) => {
|
|
||||||
if (!args || !Array.isArray(args)) return allValidations;
|
|
||||||
|
|
||||||
const substepArgumentValidations = {};
|
|
||||||
|
|
||||||
for (const arg of args) {
|
|
||||||
const { key, required } = arg;
|
|
||||||
|
|
||||||
// base validation for the field if not exists
|
|
||||||
if (!substepArgumentValidations[key]) {
|
|
||||||
substepArgumentValidations[key] = yup.mixed();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (arg.type === 'dynamic') {
|
|
||||||
const fieldsSchema = {};
|
|
||||||
|
|
||||||
for (const field of arg.fields) {
|
|
||||||
fieldsSchema[field.key] = yup.mixed();
|
|
||||||
|
|
||||||
fieldsSchema[field.key] = addRequiredValidation({
|
|
||||||
required: field.required,
|
|
||||||
schema: fieldsSchema[field.key],
|
|
||||||
key: field.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
fieldsSchema[field.key] = addDependsOnValidation({
|
|
||||||
schema: fieldsSchema[field.key],
|
|
||||||
dependsOn: field.dependsOn,
|
|
||||||
key: field.key,
|
|
||||||
args,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
substepArgumentValidations[key] = yup
|
|
||||||
.array()
|
|
||||||
.of(yup.object(fieldsSchema));
|
|
||||||
} else if (
|
|
||||||
typeof substepArgumentValidations[key] === 'object' &&
|
|
||||||
(arg.type === 'string' || arg.type === 'dropdown')
|
|
||||||
) {
|
|
||||||
substepArgumentValidations[key] = addRequiredValidation({
|
|
||||||
required,
|
|
||||||
schema: substepArgumentValidations[key],
|
|
||||||
key,
|
|
||||||
});
|
|
||||||
|
|
||||||
substepArgumentValidations[key] = addDependsOnValidation({
|
|
||||||
schema: substepArgumentValidations[key],
|
|
||||||
dependsOn: arg.dependsOn,
|
|
||||||
key,
|
|
||||||
args,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...allValidations,
|
|
||||||
...substepArgumentValidations,
|
|
||||||
};
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
const validationSchema = yup.object({
|
|
||||||
parameters: yup.object(fieldValidations),
|
|
||||||
});
|
|
||||||
|
|
||||||
return yupResolver(validationSchema)(data, context, options);
|
|
||||||
}
|
|
@@ -43,10 +43,7 @@ function FlowStepContextMenu(props) {
|
|||||||
FlowStepContextMenu.propTypes = {
|
FlowStepContextMenu.propTypes = {
|
||||||
stepId: PropTypes.string.isRequired,
|
stepId: PropTypes.string.isRequired,
|
||||||
onClose: PropTypes.func.isRequired,
|
onClose: PropTypes.func.isRequired,
|
||||||
anchorEl: PropTypes.oneOfType([
|
anchorEl: PropTypes.element.isRequired,
|
||||||
PropTypes.func,
|
|
||||||
PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
|
|
||||||
]).isRequired,
|
|
||||||
deletable: PropTypes.bool.isRequired,
|
deletable: PropTypes.bool.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -19,9 +19,7 @@ function FlowSubstep(props) {
|
|||||||
onCollapse,
|
onCollapse,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
step,
|
step,
|
||||||
addAdditionalFieldsValidation,
|
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const { name, arguments: args } = substep;
|
const { name, arguments: args } = substep;
|
||||||
const editorContext = React.useContext(EditorContext);
|
const editorContext = React.useContext(EditorContext);
|
||||||
const formContext = useFormContext();
|
const formContext = useFormContext();
|
||||||
@@ -56,7 +54,6 @@ function FlowSubstep(props) {
|
|||||||
stepId={step.id}
|
stepId={step.id}
|
||||||
disabled={editorContext.readOnly}
|
disabled={editorContext.readOnly}
|
||||||
showOptionValue={true}
|
showOptionValue={true}
|
||||||
addAdditionalFieldsValidation={addAdditionalFieldsValidation}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
@@ -1,8 +1,6 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { FormProvider, useForm, useWatch } from 'react-hook-form';
|
import { FormProvider, useForm, useWatch } from 'react-hook-form';
|
||||||
|
|
||||||
const noop = () => null;
|
const noop = () => null;
|
||||||
|
|
||||||
export default function Form(props) {
|
export default function Form(props) {
|
||||||
const {
|
const {
|
||||||
children,
|
children,
|
||||||
@@ -11,31 +9,24 @@ export default function Form(props) {
|
|||||||
resolver,
|
resolver,
|
||||||
render,
|
render,
|
||||||
mode = 'all',
|
mode = 'all',
|
||||||
context,
|
|
||||||
...formProps
|
...formProps
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const methods = useForm({
|
const methods = useForm({
|
||||||
defaultValues,
|
defaultValues,
|
||||||
reValidateMode: 'onBlur',
|
reValidateMode: 'onBlur',
|
||||||
resolver,
|
resolver,
|
||||||
mode,
|
mode,
|
||||||
context,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const form = useWatch({ control: methods.control });
|
const form = useWatch({ control: methods.control });
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* For fields having `dependsOn` fields, we need to re-validate the form.
|
* For fields having `dependsOn` fields, we need to re-validate the form.
|
||||||
*/
|
*/
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
methods.trigger();
|
methods.trigger();
|
||||||
}, [methods.trigger, form]);
|
}, [methods.trigger, form]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
methods.reset(defaultValues);
|
methods.reset(defaultValues);
|
||||||
}, [defaultValues]);
|
}, [defaultValues]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormProvider {...methods}>
|
<FormProvider {...methods}>
|
||||||
<form onSubmit={methods.handleSubmit(onSubmit)} {...formProps}>
|
<form onSubmit={methods.handleSubmit(onSubmit)} {...formProps}>
|
||||||
|
@@ -23,9 +23,7 @@ export default function InputCreator(props) {
|
|||||||
disabled,
|
disabled,
|
||||||
showOptionValue,
|
showOptionValue,
|
||||||
shouldUnregister,
|
shouldUnregister,
|
||||||
addAdditionalFieldsValidation,
|
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
key: name,
|
key: name,
|
||||||
label,
|
label,
|
||||||
@@ -35,7 +33,6 @@ export default function InputCreator(props) {
|
|||||||
description,
|
description,
|
||||||
type,
|
type,
|
||||||
} = schema;
|
} = schema;
|
||||||
|
|
||||||
const { data, loading } = useDynamicData(stepId, schema);
|
const { data, loading } = useDynamicData(stepId, schema);
|
||||||
const { data: additionalFieldsData, isLoading: isDynamicFieldsLoading } =
|
const { data: additionalFieldsData, isLoading: isDynamicFieldsLoading } =
|
||||||
useDynamicFields(stepId, schema);
|
useDynamicFields(stepId, schema);
|
||||||
@@ -43,10 +40,6 @@ export default function InputCreator(props) {
|
|||||||
|
|
||||||
const computedName = namePrefix ? `${namePrefix}.${name}` : name;
|
const computedName = namePrefix ? `${namePrefix}.${name}` : name;
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
addAdditionalFieldsValidation?.({ [name]: additionalFields });
|
|
||||||
}, [additionalFields]);
|
|
||||||
|
|
||||||
if (type === 'dynamic') {
|
if (type === 'dynamic') {
|
||||||
return (
|
return (
|
||||||
<DynamicField
|
<DynamicField
|
||||||
|
@@ -5,10 +5,8 @@ import AddCircleIcon from '@mui/icons-material/AddCircle';
|
|||||||
import CardActionArea from '@mui/material/CardActionArea';
|
import CardActionArea from '@mui/material/CardActionArea';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import { CardContent } from './style';
|
import { CardContent } from './style';
|
||||||
|
|
||||||
export default function NoResultFound(props) {
|
export default function NoResultFound(props) {
|
||||||
const { text, to } = props;
|
const { text, to } = props;
|
||||||
|
|
||||||
const ActionAreaLink = React.useMemo(
|
const ActionAreaLink = React.useMemo(
|
||||||
() =>
|
() =>
|
||||||
React.forwardRef(function InlineLink(linkProps, ref) {
|
React.forwardRef(function InlineLink(linkProps, ref) {
|
||||||
@@ -17,12 +15,12 @@ export default function NoResultFound(props) {
|
|||||||
}),
|
}),
|
||||||
[to],
|
[to],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card elevation={0}>
|
<Card elevation={0}>
|
||||||
<CardActionArea component={ActionAreaLink} {...props}>
|
<CardActionArea component={ActionAreaLink} {...props}>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{!!to && <AddCircleIcon color="primary" />}
|
{!!to && <AddCircleIcon color="primary" />}
|
||||||
|
|
||||||
<Typography variant="body1">{text}</Typography>
|
<Typography variant="body1">{text}</Typography>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</CardActionArea>
|
</CardActionArea>
|
||||||
|
@@ -7,7 +7,6 @@ import { useQuery } from '@tanstack/react-query';
|
|||||||
import api from 'helpers/api';
|
import api from 'helpers/api';
|
||||||
|
|
||||||
const variableRegExp = /({.*?})/;
|
const variableRegExp = /({.*?})/;
|
||||||
|
|
||||||
// TODO: extract this function to a separate file
|
// TODO: extract this function to a separate file
|
||||||
function computeArguments(args, getValues) {
|
function computeArguments(args, getValues) {
|
||||||
const initialValue = {};
|
const initialValue = {};
|
||||||
|
@@ -1,6 +1,4 @@
|
|||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
import { Settings } from 'luxon';
|
|
||||||
|
|
||||||
import ThemeProvider from 'components/ThemeProvider';
|
import ThemeProvider from 'components/ThemeProvider';
|
||||||
import IntlProvider from 'components/IntlProvider';
|
import IntlProvider from 'components/IntlProvider';
|
||||||
import ApolloProvider from 'components/ApolloProvider';
|
import ApolloProvider from 'components/ApolloProvider';
|
||||||
@@ -12,9 +10,6 @@ import Router from 'components/Router';
|
|||||||
import routes from 'routes';
|
import routes from 'routes';
|
||||||
import reportWebVitals from './reportWebVitals';
|
import reportWebVitals from './reportWebVitals';
|
||||||
|
|
||||||
// Sets the default locale to English for all luxon DateTime instances created afterwards.
|
|
||||||
Settings.defaultLocale = 'en';
|
|
||||||
|
|
||||||
const container = document.getElementById('root');
|
const container = document.getElementById('root');
|
||||||
const root = createRoot(container);
|
const root = createRoot(container);
|
||||||
|
|
||||||
|
@@ -30,7 +30,6 @@ import AppIcon from 'components/AppIcon';
|
|||||||
import Container from 'components/Container';
|
import Container from 'components/Container';
|
||||||
import PageTitle from 'components/PageTitle';
|
import PageTitle from 'components/PageTitle';
|
||||||
import useApp from 'hooks/useApp';
|
import useApp from 'hooks/useApp';
|
||||||
import Can from 'components/Can';
|
|
||||||
|
|
||||||
const ReconnectConnection = (props) => {
|
const ReconnectConnection = (props) => {
|
||||||
const { application, onClose } = props;
|
const { application, onClose } = props;
|
||||||
@@ -93,7 +92,7 @@ export default function Application() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return options;
|
return options;
|
||||||
}, [appKey, appConfig?.data, currentUserAbility, formatMessage]);
|
}, [appKey, appConfig?.data, currentUserAbility]);
|
||||||
|
|
||||||
if (loading) return null;
|
if (loading) return null;
|
||||||
|
|
||||||
@@ -119,46 +118,37 @@ export default function Application() {
|
|||||||
<Route
|
<Route
|
||||||
path={`${URLS.FLOWS}/*`}
|
path={`${URLS.FLOWS}/*`}
|
||||||
element={
|
element={
|
||||||
<Can I="create" a="Flow" passThrough>
|
<ConditionalIconButton
|
||||||
{(allowed) => (
|
type="submit"
|
||||||
<ConditionalIconButton
|
variant="contained"
|
||||||
type="submit"
|
color="primary"
|
||||||
variant="contained"
|
size="large"
|
||||||
color="primary"
|
component={Link}
|
||||||
size="large"
|
to={URLS.CREATE_FLOW_WITH_APP_AND_CONNECTION(
|
||||||
component={Link}
|
appKey,
|
||||||
to={URLS.CREATE_FLOW_WITH_APP_AND_CONNECTION(
|
connectionId,
|
||||||
appKey,
|
|
||||||
connectionId,
|
|
||||||
)}
|
|
||||||
fullWidth
|
|
||||||
icon={<AddIcon />}
|
|
||||||
disabled={!allowed}
|
|
||||||
>
|
|
||||||
{formatMessage('app.createFlow')}
|
|
||||||
</ConditionalIconButton>
|
|
||||||
)}
|
)}
|
||||||
</Can>
|
fullWidth
|
||||||
|
icon={<AddIcon />}
|
||||||
|
disabled={!currentUserAbility.can('create', 'Flow')}
|
||||||
|
>
|
||||||
|
{formatMessage('app.createFlow')}
|
||||||
|
</ConditionalIconButton>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path={`${URLS.CONNECTIONS}/*`}
|
path={`${URLS.CONNECTIONS}/*`}
|
||||||
element={
|
element={
|
||||||
<Can I="create" a="Connection" passThrough>
|
<SplitButton
|
||||||
{(allowed) => (
|
disabled={
|
||||||
<SplitButton
|
(appConfig?.data &&
|
||||||
disabled={
|
!appConfig?.data?.canConnect &&
|
||||||
!allowed ||
|
!appConfig?.data?.canCustomConnect) ||
|
||||||
(appConfig?.data &&
|
connectionOptions.every(({ disabled }) => disabled)
|
||||||
!appConfig?.data?.canConnect &&
|
}
|
||||||
!appConfig?.data?.canCustomConnect) ||
|
options={connectionOptions}
|
||||||
connectionOptions.every(({ disabled }) => disabled)
|
/>
|
||||||
}
|
|
||||||
options={connectionOptions}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Can>
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Routes>
|
</Routes>
|
||||||
@@ -179,20 +169,17 @@ export default function Application() {
|
|||||||
label={formatMessage('app.connections')}
|
label={formatMessage('app.connections')}
|
||||||
to={URLS.APP_CONNECTIONS(appKey)}
|
to={URLS.APP_CONNECTIONS(appKey)}
|
||||||
value={URLS.APP_CONNECTIONS_PATTERN}
|
value={URLS.APP_CONNECTIONS_PATTERN}
|
||||||
disabled={
|
disabled={!app.supportsConnections}
|
||||||
!currentUserAbility.can('read', 'Connection') ||
|
|
||||||
!app.supportsConnections
|
|
||||||
}
|
|
||||||
component={Link}
|
component={Link}
|
||||||
data-test="connections-tab"
|
data-test="connections-tab"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Tab
|
<Tab
|
||||||
label={formatMessage('app.flows')}
|
label={formatMessage('app.flows')}
|
||||||
to={URLS.APP_FLOWS(appKey)}
|
to={URLS.APP_FLOWS(appKey)}
|
||||||
value={URLS.APP_FLOWS_PATTERN}
|
value={URLS.APP_FLOWS_PATTERN}
|
||||||
component={Link}
|
component={Link}
|
||||||
data-test="flows-tab"
|
data-test="flows-tab"
|
||||||
disabled={!currentUserAbility.can('read', 'Flow')}
|
|
||||||
/>
|
/>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -200,20 +187,14 @@ export default function Application() {
|
|||||||
<Routes>
|
<Routes>
|
||||||
<Route
|
<Route
|
||||||
path={`${URLS.FLOWS}/*`}
|
path={`${URLS.FLOWS}/*`}
|
||||||
element={
|
element={<AppFlows appKey={appKey} />}
|
||||||
<Can I="read" a="Flow">
|
|
||||||
<AppFlows appKey={appKey} />
|
|
||||||
</Can>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path={`${URLS.CONNECTIONS}/*`}
|
path={`${URLS.CONNECTIONS}/*`}
|
||||||
element={
|
element={<AppConnections appKey={appKey} />}
|
||||||
<Can I="read" a="Connection">
|
|
||||||
<AppConnections appKey={appKey} />
|
|
||||||
</Can>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path="/"
|
path="/"
|
||||||
element={
|
element={
|
||||||
@@ -237,24 +218,17 @@ export default function Application() {
|
|||||||
<Route
|
<Route
|
||||||
path="/connections/add"
|
path="/connections/add"
|
||||||
element={
|
element={
|
||||||
<Can I="create" a="Connection">
|
<AddAppConnection onClose={goToApplicationPage} application={app} />
|
||||||
<AddAppConnection
|
|
||||||
onClose={goToApplicationPage}
|
|
||||||
application={app}
|
|
||||||
/>
|
|
||||||
</Can>
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path="/connections/:connectionId/reconnect"
|
path="/connections/:connectionId/reconnect"
|
||||||
element={
|
element={
|
||||||
<Can I="create" a="Connection">
|
<ReconnectConnection
|
||||||
<ReconnectConnection
|
application={app}
|
||||||
application={app}
|
onClose={goToApplicationPage}
|
||||||
onClose={goToApplicationPage}
|
/>
|
||||||
/>
|
|
||||||
</Can>
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Routes>
|
</Routes>
|
||||||
|
@@ -84,14 +84,10 @@ export default function Applications() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{!isLoading && !hasApps && (
|
{!isLoading && !hasApps && (
|
||||||
<Can I="create" a="Connection" passThrough>
|
<NoResultFound
|
||||||
{(allowed) => (
|
text={formatMessage('apps.noConnections')}
|
||||||
<NoResultFound
|
to={URLS.NEW_APP_CONNECTION}
|
||||||
text={formatMessage('apps.noConnections')}
|
/>
|
||||||
{...(allowed && { to: URLS.NEW_APP_CONNECTION })}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Can>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!isLoading &&
|
{!isLoading &&
|
||||||
|
@@ -7,15 +7,13 @@ import * as URLS from 'config/urls';
|
|||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
import { CREATE_FLOW } from 'graphql/mutations/create-flow';
|
import { CREATE_FLOW } from 'graphql/mutations/create-flow';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
|
|
||||||
export default function CreateFlow() {
|
export default function CreateFlow() {
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
const [createFlow, { error }] = useMutation(CREATE_FLOW);
|
const [createFlow] = useMutation(CREATE_FLOW);
|
||||||
const appKey = searchParams.get('appKey');
|
const appKey = searchParams.get('appKey');
|
||||||
const connectionId = searchParams.get('connectionId');
|
const connectionId = searchParams.get('connectionId');
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
async function initiate() {
|
async function initiate() {
|
||||||
const variables = {};
|
const variables = {};
|
||||||
@@ -35,11 +33,6 @@ export default function CreateFlow() {
|
|||||||
}
|
}
|
||||||
initiate();
|
initiate();
|
||||||
}, [createFlow, navigate, appKey, connectionId]);
|
}, [createFlow, navigate, appKey, connectionId]);
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
@@ -52,6 +45,7 @@ export default function CreateFlow() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CircularProgress size={16} thickness={7.5} />
|
<CircularProgress size={16} thickness={7.5} />
|
||||||
|
|
||||||
<Typography variant="body2">
|
<Typography variant="body2">
|
||||||
{formatMessage('createFlow.creating')}
|
{formatMessage('createFlow.creating')}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
@@ -17,7 +17,6 @@ import Container from 'components/Container';
|
|||||||
import PageTitle from 'components/PageTitle';
|
import PageTitle from 'components/PageTitle';
|
||||||
import SearchInput from 'components/SearchInput';
|
import SearchInput from 'components/SearchInput';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
import useCurrentUserAbility from 'hooks/useCurrentUserAbility';
|
|
||||||
import * as URLS from 'config/urls';
|
import * as URLS from 'config/urls';
|
||||||
import useLazyFlows from 'hooks/useLazyFlows';
|
import useLazyFlows from 'hooks/useLazyFlows';
|
||||||
|
|
||||||
@@ -27,7 +26,6 @@ export default function Flows() {
|
|||||||
const page = parseInt(searchParams.get('page') || '', 10) || 1;
|
const page = parseInt(searchParams.get('page') || '', 10) || 1;
|
||||||
const [flowName, setFlowName] = React.useState('');
|
const [flowName, setFlowName] = React.useState('');
|
||||||
const [isLoading, setIsLoading] = React.useState(false);
|
const [isLoading, setIsLoading] = React.useState(false);
|
||||||
const currentUserAbility = useCurrentUserAbility();
|
|
||||||
|
|
||||||
const { data, mutate: fetchFlows } = useLazyFlows(
|
const { data, mutate: fetchFlows } = useLazyFlows(
|
||||||
{ flowName, page },
|
{ flowName, page },
|
||||||
@@ -126,9 +124,7 @@ export default function Flows() {
|
|||||||
{!isLoading && !hasFlows && (
|
{!isLoading && !hasFlows && (
|
||||||
<NoResultFound
|
<NoResultFound
|
||||||
text={formatMessage('flows.noFlows')}
|
text={formatMessage('flows.noFlows')}
|
||||||
{...(currentUserAbility.can('create', 'Flow') && {
|
to={URLS.CREATE_FLOW}
|
||||||
to: URLS.CREATE_FLOW,
|
|
||||||
})}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!isLoading && pageInfo && pageInfo.totalPages > 1 && (
|
{!isLoading && pageInfo && pageInfo.totalPages > 1 && (
|
||||||
|
Reference in New Issue
Block a user