Compare commits

..

102 Commits

Author SHA1 Message Date
Rıdvan Akca
745263cb41 feat: add sftp integration 2024-04-17 14:07:22 +02:00
Ömer Faruk Aydın
e4292815cd Merge pull request #1812 from automatisch/AUT-917
fix: expose missing createdAt and updatedAt fields from flow
2024-04-16 14:10:26 +02:00
Rıdvan Akca
ab37250d5d fix: expose missing createdAt and updatedAt fields from flow 2024-04-15 13:57:38 +02:00
Ömer Faruk Aydın
e5be8d3ba7 Merge pull request #1802 from automatisch/AUT-688
refactor: rewrite get connected apps with RQ
2024-04-15 11:43:35 +02:00
Ali BARIN
96a421fa22 Merge pull request #1811 from automatisch/AUT-920
fix: make inputs look and behave disabled when flow is in published state
2024-04-12 16:19:37 +02:00
kasia.oczkowska
12f72401b1 fix: make inputs look and behave disabled when flow is in published state 2024-04-12 14:58:24 +01:00
Ali BARIN
7391a9eddc Merge pull request #1810 from automatisch/AUT-921
fix: disable add connection button for unauthorized users
2024-04-12 15:15:38 +02:00
Ali BARIN
30dee27f72 Merge pull request #1809 from automatisch/AUT-914
fix: invalidate app connections upon reconnecting a connection
2024-04-12 15:15:11 +02:00
Ali BARIN
51a9939034 Merge pull request #1808 from automatisch/AUT-922
fix: disable create flow button when user doesn't have permissions
2024-04-12 15:14:44 +02:00
Ali BARIN
e03c6e0ca4 Merge pull request #1807 from automatisch/update-query-key
fix: update old query key
2024-04-12 15:14:26 +02:00
Rıdvan Akca
bece5c6488 fix: invalidate app connections upon reconnecting a connection 2024-04-12 14:55:45 +02:00
kasia.oczkowska
d49bb4c52d fix: disable add connection button for unauthorized users 2024-04-12 13:43:26 +01:00
kattoczko
73d0eec30c Merge branch 'main' into update-query-key 2024-04-12 14:10:40 +02:00
kasia.oczkowska
5c756b16ca fix: disable create flow button when user doesn't have permissions 2024-04-12 12:41:50 +01:00
Ali BARIN
f482c2422c Merge pull request #1806 from automatisch/AUT-913
fix: invalidate app connections upon creating a connection
2024-04-12 13:31:03 +02:00
kasia.oczkowska
2e564c863f fix: update old query key 2024-04-12 12:25:02 +01:00
Rıdvan Akca
d9917a81bb fix: invalidate app connections upon creating a connection 2024-04-12 13:10:51 +02:00
Ali BARIN
61dc431f92 Merge pull request #1805 from automatisch/AUT-919
fix: pass current user id to usePlanAndUsage hook
2024-04-12 13:06:57 +02:00
Ali BARIN
7d2fb8d9d7 Merge pull request #1803 from automatisch/AUT-912
fix: invalidate useCurrentUser when updating profile settings
2024-04-12 13:06:24 +02:00
Ali BARIN
608b79b66f Merge pull request #1804 from automatisch/unify-query-keys 2024-04-12 11:57:13 +02:00
kasia.oczkowska
009754c18b fix: pass current user id to usePlanAndUsage hook 2024-04-12 10:43:40 +01:00
Rıdvan Akca
5df07c289e fix: invalidate useCurrentUser when updating profile settings 2024-04-12 11:28:13 +02:00
kasia.oczkowska
a36d10870b feat: unify react-query query keys 2024-04-12 10:07:51 +01:00
kasia.oczkowska
b549ba3e39 refactor: rewrite get connected apps with RQ 2024-04-11 14:00:53 +01:00
Ali BARIN
897c96361f Merge pull request #1801 from automatisch/remove-unused-get-app-auth-client
refactor: remove not used files related to gql get-app-auth-client
2024-04-11 12:18:03 +02:00
kasia.oczkowska
e7693d8aa6 refactor: remove not used files related to gql get-app-auth-client 2024-04-11 11:10:58 +01:00
Ali BARIN
1fe755f836 Merge pull request #1800 from automatisch/AUT-689
refactor: rewrite useDynamicData with RQ
2024-04-10 17:47:22 +02:00
Rıdvan Akca
ea1a63f7dd refactor: rewrite useDynamicData with RQ 2024-04-10 17:25:01 +02:00
Ali BARIN
85134722a5 Merge pull request #1799 from automatisch/AUT-709
refactor: rewrite test connection with RQ
2024-04-10 17:21:37 +02:00
Rıdvan Akca
5c9d3ed134 refactor: rewrite test connection with RQ 2024-04-10 16:39:55 +02:00
Ali BARIN
17fb935ea0 Merge pull request #1798 from automatisch/fix-flow-counts
fix: show flow counts using useConnectionFlows
2024-04-10 16:37:06 +02:00
Ali BARIN
196642a1cf feat(AppConnectionRow): embed skeleton in place of flow count 2024-04-10 14:08:30 +00:00
Rıdvan Akca
009cf63d8c fix: show flow counts using useConnectionFlows 2024-04-10 15:29:21 +02:00
Ali BARIN
da399aacd6 Merge pull request #1766 from automatisch/AUT-705
refactor: rewrite useStepWithTestExecutions with RQ
2024-04-10 13:30:04 +02:00
Rıdvan Akca
3632ee77e5 refactor: rewrite useStepWithTestExecutions with RQ 2024-04-09 16:32:52 +02:00
Ali BARIN
2901f337cc Merge pull request #1797 from automatisch/disable-retry-on-mount
fix: disable retry on mount by default
2024-04-09 14:31:57 +02:00
Ali BARIN
f0bd2f335b fix: disable retry on mount by default 2024-04-08 15:20:10 +00:00
Ali BARIN
acdd026448 Merge pull request #1780 from automatisch/AUT-686
refactor: rewrite useBillingAndUsageData with useSubscription and useUserTrial
2024-04-08 15:22:55 +02:00
Ali BARIN
fb0a328ab0 Merge pull request #1791 from automatisch/AUT-905
refactor: rewrite get app connections with RQ
2024-04-08 15:21:02 +02:00
Rıdvan Akca
d2a7889fc9 refactor: remove useBillingAndUsageData hook 2024-04-08 14:49:37 +02:00
Rıdvan Akca
88c50e014d fix: update SubscriptionCancelledAlert and CheckoutCompletedAlert based on useSubscription and useUserTrial 2024-04-08 14:45:42 +02:00
Rıdvan Akca
f0ef12f904 refactor: rewrite useSubscription with RQ and use it in UsageDataInformation 2024-04-08 14:45:42 +02:00
Rıdvan Akca
1827f5413f refactor(useUserTrial): return hasTrial field from hook 2024-04-08 14:45:42 +02:00
Rıdvan Akca
0609f30e25 feat: introduce usePlanAndUsage with RQ 2024-04-08 14:45:42 +02:00
Rıdvan Akca
d4e4d95b6d refactor: rewrite get app connections with RQ 2024-04-08 14:44:36 +02:00
Ali BARIN
d74af4931e Merge pull request #1793 from automatisch/AUT-682
refactor: rewrite useAuthClients with RQ
2024-04-08 14:40:44 +02:00
Ali BARIN
bee043d10d Merge pull request #1792 from automatisch/fix-deleting-flows
fix: refetch app flows after delete and duplicate
2024-04-08 14:25:52 +02:00
Rıdvan Akca
a65e48b98a fix: refetch app flows after delete and duplicate 2024-04-08 13:52:32 +02:00
Ali BARIN
ee26b54d54 Merge pull request #1761 from automatisch/AUT-859
refactor: rewrite useFlow and useStepConnection with RQ
2024-04-08 13:33:48 +02:00
Ömer Faruk Aydın
855ec53dc2 Merge pull request #1795 from automatisch/rest-get-user-apps
feat: Implement users get apps API endpoint
2024-04-07 03:54:47 +02:00
Faruk AYDIN
3e3e48110d feat: Implement users get apps API endpoint 2024-04-07 03:45:33 +02:00
Rıdvan Akca
fc04a357c8 refactor: rewrite useFlow and useStepConnection with RQ 2024-04-05 17:51:28 +02:00
Ali BARIN
c8147370de Merge pull request #1794 from automatisch/fix-application-page
fix: destructure app config data correctly on Application page
2024-04-05 16:50:57 +02:00
kasia.oczkowska
999426be89 fix: destructure app config data correctly on Application page 2024-04-05 15:36:50 +01:00
kasia.oczkowska
91458f91ef refactor: rewrite useAuthClients with RQ 2024-04-05 15:35:05 +01:00
Ali BARIN
4b9ed29cc0 Merge pull request #1758 from automatisch/dependabot/npm_and_yarn/webpack-dev-middleware-5.3.4
chore(deps): bump webpack-dev-middleware from 5.3.0 to 5.3.4
2024-04-05 16:03:44 +02:00
Ali BARIN
e3bcb673fb Merge pull request #1787 from automatisch/dependabot/npm_and_yarn/vite-3.2.10
chore(deps): bump vite from 3.2.8 to 3.2.10
2024-04-05 16:03:20 +02:00
Ali BARIN
bf4776ca4f Merge pull request #1788 from automatisch/AUT-867
fix: introduce fix for token management
2024-04-05 14:19:05 +02:00
Ali BARIN
9f7f30a92a Merge pull request #1790 from automatisch/AUT-907
fix: set loading false if there is no flowName
2024-04-05 14:18:03 +02:00
Rıdvan Akca
5c29fff55e fix: set loading false if there is no flowName 2024-04-05 14:06:29 +02:00
Ali BARIN
a0160c2573 Merge pull request #1789 from automatisch/fix-use-apps
fix: introduce fix for useApps not using name as param
2024-04-05 12:00:50 +02:00
kasia.oczkowska
87d3ca287d fix: introduce fix for useApps not using name as param 2024-04-05 10:48:49 +01:00
kasia.oczkowska
526e093689 fix: introduce fix for token management 2024-04-04 14:16:25 +01:00
Ömer Faruk Aydın
0930c9d8d6 Merge pull request #1786 from automatisch/flow-error-message
fix: Use soft deleted filter to get soft deleted user
2024-04-04 00:50:50 +02:00
dependabot[bot]
ec680a713d chore(deps): bump vite from 3.2.8 to 3.2.10
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 3.2.8 to 3.2.10.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v3.2.10/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v3.2.10/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-03 18:08:37 +00:00
Faruk AYDIN
583f90d1e9 fix: Use soft deleted filter to get soft deleted user 2024-04-03 19:10:40 +02:00
Ömer Faruk Aydın
8ba95381bc Merge pull request #1784 from automatisch/create-dynamic-data-action
feat: Implement create dynamic data API endpoint
2024-04-03 01:23:14 +02:00
Faruk AYDIN
ec6d634b99 feat: Implement create dynamic data API endpoint 2024-04-03 01:17:06 +02:00
Ali BARIN
bc082acbe7 Merge pull request #1785 from automatisch/make-stages-dynamic-in-pipedrive
feat(pipedrive/create-deal): add dynamic stages
2024-04-03 00:09:46 +02:00
Ali BARIN
e474ba02cb Merge pull request #1645 from automatisch/flex-http-request
feat(http-request/custom-request): utilize accept header for parsing response
2024-04-02 19:54:44 +02:00
Ali BARIN
ea922aaf10 feat(pipedrive/create-deal): add dynamic stages 2024-04-02 15:43:53 +00:00
Ömer Faruk Aydın
766e6e20d8 Merge pull request #1783 from automatisch/rest-test-connection
feat: Implement test connection API endpoint
2024-03-30 00:22:38 +01:00
Faruk AYDIN
8e646c244e feat: Implement test connection API endpoint 2024-03-30 00:12:59 +01:00
Ömer Faruk Aydın
26f31a5899 Merge pull request #1782 from automatisch/rest-get-app-connections
feat: Implement get app connections API endpoint
2024-03-29 00:44:15 +01:00
Faruk AYDIN
5c79e374dd feat: Implement get app connections API endpoint 2024-03-29 00:21:58 +01:00
Ömer Faruk Aydın
7c1473ea95 Merge pull request #1781 from automatisch/fix-app-config-endpoint
fix: Fetch app auth clients for app config endpoint
2024-03-28 22:55:46 +01:00
Faruk AYDIN
1fe4cc3258 fix: Fetch app auth clients for app config endpoint 2024-03-28 22:48:17 +01:00
Ömer Faruk Aydın
042ad4cea1 Merge pull request #1774 from automatisch/rest-admin-get-app-auth-client
feat: Implement new admin get app auth client API endpoint
2024-03-28 20:47:50 +01:00
Ömer Faruk Aydın
e4c998dbce Merge pull request #1773 from automatisch/rest-admin-get-app-auth-clients
feat: Implement new admin get auth clients API endpoint
2024-03-28 20:47:40 +01:00
Ömer Faruk Aydın
83c8cacdac Merge pull request #1771 from automatisch/rest-get-app-auth-clients
feat: Implement new get app auth clients API endpoint
2024-03-28 20:47:12 +01:00
Ömer Faruk Aydın
f75d5d906e Merge pull request #1770 from automatisch/add-app-key-to-auth-clients
feat: Implement new get auth clients api endpoint
2024-03-28 20:44:51 +01:00
Faruk AYDIN
85b3856564 chore: Correct the folder of get auth client mock 2024-03-28 20:41:12 +01:00
Faruk AYDIN
75cb2569b5 chore: Remove old app auth client routers 2024-03-28 20:41:12 +01:00
Faruk AYDIN
0a4ac1cece feat: Implement new admin get app auth client API endpoint 2024-03-28 20:41:12 +01:00
Faruk AYDIN
a873fd14bd chore: Remove old admin app auth clients API endpoint 2024-03-28 20:40:45 +01:00
Faruk AYDIN
85b4cd4998 feat: Implement new admin get auth clients API endpoint 2024-03-28 20:40:45 +01:00
Faruk AYDIN
e9bc9b1aa8 fix: Typo for the get auth clients test file 2024-03-28 20:40:45 +01:00
Faruk AYDIN
e3bf599bf6 feat: Implement new get app auth clients API endpoint 2024-03-28 20:40:14 +01:00
Faruk AYDIN
01ae96840e refactor: Remove redundant appConfigId from get auth clients mock 2024-03-28 20:38:46 +01:00
Faruk AYDIN
186160ebf4 feat: Make appKey column of app auth clients not nullable 2024-03-28 20:38:46 +01:00
Faruk AYDIN
70f5e45c1f chore: Remove old app auth clients API endpoint 2024-03-28 20:38:46 +01:00
Faruk AYDIN
6dc54ecabc feat: Implement new get auth clients api endpoint 2024-03-28 20:38:46 +01:00
Faruk AYDIN
d21888c047 feat: Remove app config relation from app auth clients 2024-03-28 20:38:46 +01:00
Faruk AYDIN
33f7a90042 feat: Remove app auth clients relation from app configs 2024-03-28 20:38:46 +01:00
Faruk AYDIN
a00d3a2c5e feat: Remove app config id from app auth clients 2024-03-28 20:38:46 +01:00
Faruk AYDIN
abc64d769c feat: Migrate app config id to app key 2024-03-28 20:38:46 +01:00
Faruk AYDIN
88754ac569 feat: Add appKey to app auth clients 2024-03-28 20:38:46 +01:00
Ali BARIN
e3ee05d47d Merge pull request #1772 from automatisch/fix-signal
fix(useDynamicFields): pass signal in RQ
2024-03-28 14:22:39 +01:00
Rıdvan Akca
3b004e7483 fix(useDynamicFields): pass signal in RQ 2024-03-27 10:57:17 +03:00
dependabot[bot]
565db852e0 chore(deps): bump webpack-dev-middleware from 5.3.0 to 5.3.4
Bumps [webpack-dev-middleware](https://github.com/webpack/webpack-dev-middleware) from 5.3.0 to 5.3.4.
- [Release notes](https://github.com/webpack/webpack-dev-middleware/releases)
- [Changelog](https://github.com/webpack/webpack-dev-middleware/blob/v5.3.4/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-dev-middleware/compare/v5.3.0...v5.3.4)

---
updated-dependencies:
- dependency-name: webpack-dev-middleware
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-22 09:08:14 +00:00
Ali BARIN
bd1ad5fa56 feat(http-request/custom-request): utilize accept header for parsing response 2024-02-23 18:03:56 +00:00
Ali BARIN
f2e22e7445 feat: keep axios defaults for instances 2024-02-23 18:02:33 +00:00
161 changed files with 2679 additions and 1738 deletions

View File

@@ -67,6 +67,7 @@
"pluralize": "^8.0.0", "pluralize": "^8.0.0",
"raw-body": "^2.5.2", "raw-body": "^2.5.2",
"showdown": "^2.1.0", "showdown": "^2.1.0",
"ssh2-sftp-client": "^10.0.3",
"winston": "^3.7.1", "winston": "^3.7.1",
"xmlrpc": "^1.3.2" "xmlrpc": "^1.3.2"
}, },
@@ -95,6 +96,7 @@
"url": "https://github.com/automatisch/automatisch/issues" "url": "https://github.com/automatisch/automatisch/issues"
}, },
"devDependencies": { "devDependencies": {
"node-gyp": "^10.1.0",
"nodemon": "^2.0.13", "nodemon": "^2.0.13",
"supertest": "^6.3.3", "supertest": "^6.3.3",
"vitest": "^1.1.3" "vitest": "^1.1.3"

View File

@@ -90,7 +90,7 @@ export default defineAction({
async run($) { async run($) {
const method = $.step.parameters.method; const method = $.step.parameters.method;
const data = $.step.parameters.data; const data = $.step.parameters.data || null;
const url = $.step.parameters.url; const url = $.step.parameters.url;
const headers = $.step.parameters.headers; const headers = $.step.parameters.headers;
@@ -108,14 +108,17 @@ export default defineAction({
return result; return result;
}, {}); }, {});
let contentType = headersObject['content-type']; let expectedResponseContentType = headersObject.accept;
// in case HEAD request is not supported by the URL // in case HEAD request is not supported by the URL
try { try {
const metadataResponse = await $.http.head(url, { const metadataResponse = await $.http.head(url, {
headers: headersObject, headers: headersObject,
}); });
contentType = metadataResponse.headers['content-type'];
if (!expectedResponseContentType) {
expectedResponseContentType = metadataResponse.headers['content-type'];
}
throwIfFileSizeExceedsLimit(metadataResponse.headers['content-length']); throwIfFileSizeExceedsLimit(metadataResponse.headers['content-length']);
// eslint-disable-next-line no-empty // eslint-disable-next-line no-empty
@@ -128,7 +131,7 @@ export default defineAction({
headers: headersObject, headers: headersObject,
}; };
if (!isPossiblyTextBased(contentType)) { if (!isPossiblyTextBased(expectedResponseContentType)) {
requestData.responseType = 'arraybuffer'; requestData.responseType = 'arraybuffer';
} }
@@ -138,7 +141,7 @@ export default defineAction({
let responseData = response.data; let responseData = response.data;
if (!isPossiblyTextBased(contentType)) { if (!isPossiblyTextBased(expectedResponseContentType)) {
responseData = Buffer.from(responseData).toString('base64'); responseData = Buffer.from(responseData).toString('base64');
} }

View File

@@ -64,32 +64,17 @@ export default defineAction({
value: '1', value: '1',
description: description:
'The ID of the stage this deal will be added to. If omitted, the deal will be placed in the first stage of the default pipeline.', 'The ID of the stage this deal will be added to. If omitted, the deal will be placed in the first stage of the default pipeline.',
options: [ variables: true,
{ source: {
label: 'Qualified (Pipeline)', type: 'query',
value: 1, name: 'getDynamicData',
}, arguments: [
{ {
label: 'Contact Made (Pipeline)', name: 'key',
value: 2, value: 'listStages',
}, },
{ ],
label: 'Prospect Qualified (Pipeline)', },
value: 3,
},
{
label: 'Needs Defined (Pipeline)',
value: 4,
},
{
label: 'Proposal Made (Pipeline)',
value: 5,
},
{
label: 'Negotiations Started (Pipeline)',
value: 6,
},
],
}, },
{ {
label: 'Owner', label: 'Owner',

View File

@@ -1,23 +1,25 @@
import listActivityTypes from './list-activity-types/index.js'; import listActivityTypes from './list-activity-types/index.js';
import listCurrencies from './list-currencies/index.js'; import listCurrencies from './list-currencies/index.js';
import listDeals from './list-deals/index.js'; import listDeals from './list-deals/index.js';
import listLeads from './list-leads/index.js';
import listLeadLabels from './list-lead-labels/index.js'; import listLeadLabels from './list-lead-labels/index.js';
import listOrganizations from './list-organizations/index.js'; import listLeads from './list-leads/index.js';
import listOrganizationLabelField from './list-organization-label-field/index.js'; import listOrganizationLabelField from './list-organization-label-field/index.js';
import listOrganizations from './list-organizations/index.js';
import listPersonLabelField from './list-person-label-field/index.js'; import listPersonLabelField from './list-person-label-field/index.js';
import listPersons from './list-persons/index.js'; import listPersons from './list-persons/index.js';
import listStages from './list-stages/index.js';
import listUsers from './list-users/index.js'; import listUsers from './list-users/index.js';
export default [ export default [
listActivityTypes, listActivityTypes,
listCurrencies, listCurrencies,
listDeals, listDeals,
listLeads,
listLeadLabels, listLeadLabels,
listOrganizations, listLeads,
listOrganizationLabelField, listOrganizationLabelField,
listOrganizations,
listPersonLabelField, listPersonLabelField,
listPersons, listPersons,
listStages,
listUsers, listUsers,
]; ];

View File

@@ -0,0 +1,23 @@
export default {
name: 'List stages',
key: 'listStages',
async run($) {
const stages = {
data: [],
};
const { data } = await $.http.get('/api/v1/stages');
if (data.data?.length) {
for (const stage of data.data) {
stages.data.push({
value: stage.id,
name: stage.name,
});
}
}
return stages;
},
};

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 122.88 103.95"><title>sftp</title><path d="M21.56,47.27l6.21-.39a4.56,4.56,0,0,0,.82,2.29,3.45,3.45,0,0,0,2.88,1.28,3.16,3.16,0,0,0,2.15-.65,2,2,0,0,0,.76-1.52,2,2,0,0,0-.71-1.48,8.45,8.45,0,0,0-3.34-1.22A14.06,14.06,0,0,1,24.21,43a5.14,5.14,0,0,1-1.85-4.07,5.44,5.44,0,0,1,.95-3.07,6.19,6.19,0,0,1,2.84-2.28,13.26,13.26,0,0,1,5.19-.83,10.58,10.58,0,0,1,6.18,1.51,6.53,6.53,0,0,1,2.53,4.8l-6.14.37a3.38,3.38,0,0,0-1-2.1,3.34,3.34,0,0,0-2.17-.65,2.63,2.63,0,0,0-1.72.48,1.49,1.49,0,0,0-.58,1.18,1.19,1.19,0,0,0,.47.9,5.23,5.23,0,0,0,2.18.78,27.38,27.38,0,0,1,6.13,1.87,6.38,6.38,0,0,1,2.7,2.35,5.94,5.94,0,0,1,.84,3.13,6.73,6.73,0,0,1-1.12,3.75,7.1,7.1,0,0,1-3.15,2.6,12.64,12.64,0,0,1-5.08.89c-3.6,0-6.09-.7-7.47-2.08a8.18,8.18,0,0,1-2.35-5.27ZM9.07,15.63h3.09V8.2A1.87,1.87,0,0,1,14,6.33h3.32V1.87A1.88,1.88,0,0,1,19.23,0H49.57a1.87,1.87,0,0,1,1.87,1.87V6.33h59.5a1.87,1.87,0,0,1,1.87,1.87v7.43h3.41a1.87,1.87,0,0,1,1.87,1.87,1.75,1.75,0,0,1,0,.4L111.51,66a7.12,7.12,0,0,1-2,4.07,5.62,5.62,0,0,1-4,1.69H69.25v5.57h.17A2.67,2.67,0,0,1,72.08,80v4.81h13a2.67,2.67,0,0,1,2.54,1.87h27.84a7,7,0,0,1,6.85,4.48,8.05,8.05,0,0,1,.57,3,8.14,8.14,0,0,1-.57,3,7,7,0,0,1-6.85,4.47H87.73A2.67,2.67,0,0,1,85.08,104H39.92a2.67,2.67,0,0,1-2.65-2.37H7.42A7,7,0,0,1,.57,97.11a8.14,8.14,0,0,1-.57-3,8.05,8.05,0,0,1,.57-3,7,7,0,0,1,6.85-4.48h30a2.67,2.67,0,0,1,2.54-1.87H50.68V80a2.67,2.67,0,0,1,2.66-2.66h1V71.75H18A5.69,5.69,0,0,1,13.89,70,6.84,6.84,0,0,1,12,65.92L6.92,17.69a1.83,1.83,0,0,1,.37-1.33,1.86,1.86,0,0,1,1.29-.71,4,4,0,0,1,.49,0ZM57.86,77.32h7.86v-5.5H57.86v5.5ZM87.74,90.19v7.86h27.72A3.63,3.63,0,0,0,119,95.8a4.68,4.68,0,0,0,.31-1.68,4.63,4.63,0,0,0-.31-1.67,3.63,3.63,0,0,0-3.58-2.26ZM37.26,98.05V90.19H7.42a3.63,3.63,0,0,0-3.58,2.26,4.63,4.63,0,0,0-.31,1.67,4.73,4.73,0,0,0,.31,1.68,3.63,3.63,0,0,0,3.58,2.25ZM10.85,19.37l4.84,46.17a3.14,3.14,0,0,0,.83,1.86A2.07,2.07,0,0,0,18,68h87.54a2,2,0,0,0,1.41-.6,3.28,3.28,0,0,0,.88-1.92l6.28-46.12Zm5.05-9.3v5.55l93.17-.79V10.07H49.57A1.87,1.87,0,0,1,47.69,8.2V3.74H21.1V8.2a1.87,1.87,0,0,1-1.87,1.87Zm28.3,23H60.35v4.54H50.76v3.7H59v4.28H50.76v8.62H44.2V33.12Zm18.35,0H82.42v5.22H75.75V54.26H69.22V38.34H62.55V33.12Zm22.93,0H96.35a7.48,7.48,0,0,1,5.32,1.68,6.36,6.36,0,0,1,1.77,4.82,6.55,6.55,0,0,1-1.93,5q-1.94,1.82-5.89,1.81H92v7.82H85.48V33.12Zm6.56,9h1.61a4.14,4.14,0,0,0,2.67-.66,2.13,2.13,0,0,0,.77-1.68,2.37,2.37,0,0,0-.67-1.69,3.49,3.49,0,0,0-2.51-.69H92v4.72Z"/></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -0,0 +1,69 @@
import verifyCredentials from './verify-credentials.js';
import isStillVerified from './is-still-verified.js';
export default {
fields: [
{
key: 'screenName',
label: 'Screen Name',
type: 'string',
required: true,
readOnly: false,
value: null,
placeholder: null,
description:
'Screen name of your connection to be used on Automatisch UI.',
clickToCopy: false,
},
{
key: 'host',
label: 'Host',
type: 'string',
required: true,
readOnly: false,
value: null,
placeholder: null,
description: 'The host information Automatisch will connect to.',
docUrl: 'https://automatisch.io/docs/ftp#host',
clickToCopy: false,
},
{
key: 'username',
label: 'Username',
type: 'string',
required: true,
readOnly: false,
value: null,
placeholder: null,
description: 'Your SFTP login credentials.',
docUrl: 'https://automatisch.io/docs/sftp#username',
clickToCopy: false,
},
{
key: 'password',
label: 'Password',
type: 'string',
required: true,
readOnly: false,
value: null,
placeholder: null,
description: null,
docUrl: 'https://automatisch.io/docs/sftp#password',
clickToCopy: false,
},
{
key: 'port',
label: 'Port',
type: 'string',
required: false,
readOnly: false,
value: '21',
placeholder: null,
description: null,
docUrl: 'https://automatisch.io/docs/sftp#port',
clickToCopy: false,
},
],
verifyCredentials,
isStillVerified,
};

View File

@@ -0,0 +1,8 @@
import verifyCredentials from './verify-credentials.js';
const isStillVerified = async ($) => {
await verifyCredentials($);
return true;
};
export default isStillVerified;

View File

@@ -0,0 +1,23 @@
import Client from 'ssh2-sftp-client';
const verifyCredentials = async ($) => {
const sftpClient = new Client();
try {
await sftpClient.connect({
host: $.auth.data.host,
port: $.auth.data.port,
username: $.auth.data.username,
password: $.auth.data.password,
});
} catch (err) {
console.log(err);
}
await sftpClient.end();
await $.auth.set({
screenName: $.auth.data.screenName,
});
};
export default verifyCredentials;

View File

@@ -0,0 +1,14 @@
import defineApp from '../../helpers/define-app.js';
import auth from './auth/index.js';
export default defineApp({
name: 'SFTP',
key: 'sftp',
iconUrl: '{BASE_URL}/apps/sftp/assets/favicon.svg',
authDocUrl: 'https://automatisch.io/docs/apps/sftp/connection',
supportsConnections: true,
baseUrl: '',
apiBaseUrl: '',
primaryColor: '000000',
auth,
});

View File

@@ -3,6 +3,9 @@ import AppConfig from '../../../../models/app-config.js';
export default async (request, response) => { export default async (request, response) => {
const appConfig = await AppConfig.query() const appConfig = await AppConfig.query()
.withGraphFetched({
appAuthClients: true,
})
.findOne({ .findOne({
key: request.params.appKey, key: request.params.appKey,
}) })

View File

@@ -0,0 +1,24 @@
import { renderObject } from '../../../../helpers/renderer.js';
import App from '../../../../models/app.js';
export default async (request, response) => {
const app = await App.findOneByKey(request.params.appKey);
const connections = await request.currentUser.authorizedConnections
.clone()
.select('connections.*')
.withGraphFetched({
appConfig: true,
appAuthClient: true,
})
.fullOuterJoinRelated('steps')
.where({
'connections.key': app.key,
'connections.draft': false,
})
.countDistinct('steps.flow_id as flowCount')
.groupBy('connections.id')
.orderBy('created_at', 'desc');
renderObject(response, connections);
};

View File

@@ -0,0 +1,101 @@
import { describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import app from '../../../../app.js';
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id.js';
import { createUser } from '../../../../../test/factories/user.js';
import { createConnection } from '../../../../../test/factories/connection.js';
import { createPermission } from '../../../../../test/factories/permission.js';
import getConnectionsMock from '../../../../../test/mocks/rest/api/v1/apps/get-connections.js';
describe('GET /api/v1/apps/:appKey/connections', () => {
let currentUser, currentUserRole, token;
beforeEach(async () => {
currentUser = await createUser();
currentUserRole = await currentUser.$relatedQuery('role');
token = createAuthTokenByUserId(currentUser.id);
});
it('should return the connections data of specified app for current user', async () => {
const currentUserConnectionOne = await createConnection({
userId: currentUser.id,
key: 'deepl',
draft: false,
});
const currentUserConnectionTwo = await createConnection({
userId: currentUser.id,
key: 'deepl',
draft: false,
});
await createPermission({
action: 'read',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
const response = await request(app)
.get('/api/v1/apps/deepl/connections')
.set('Authorization', token)
.expect(200);
const expectedPayload = await getConnectionsMock([
currentUserConnectionTwo,
currentUserConnectionOne,
]);
expect(response.body).toEqual(expectedPayload);
});
it('should return the connections data of specified app for another user', async () => {
const anotherUser = await createUser();
const anotherUserConnectionOne = await createConnection({
userId: anotherUser.id,
key: 'deepl',
draft: false,
});
const anotherUserConnectionTwo = await createConnection({
userId: anotherUser.id,
key: 'deepl',
draft: false,
});
await createPermission({
action: 'read',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: [],
});
const response = await request(app)
.get('/api/v1/apps/deepl/connections')
.set('Authorization', token)
.expect(200);
const expectedPayload = await getConnectionsMock([
anotherUserConnectionTwo,
anotherUserConnectionOne,
]);
expect(response.body).toEqual(expectedPayload);
});
it('should return not found response for invalid connection UUID', async () => {
await createPermission({
action: 'update',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
await request(app)
.get('/api/v1/connections/invalid-connection-id/connections')
.set('Authorization', token)
.expect(404);
});
});

View File

@@ -0,0 +1,14 @@
import { renderObject } from '../../../../helpers/renderer.js';
export default async (request, response) => {
let connection = await request.currentUser.authorizedConnections
.clone()
.findOne({
id: request.params.connectionId,
})
.throwIfNotFound();
connection = await connection.testAndUpdateConnection();
renderObject(response, connection);
};

View File

@@ -0,0 +1,123 @@
import { describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import Crypto from 'crypto';
import app from '../../../../app.js';
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id.js';
import { createUser } from '../../../../../test/factories/user.js';
import { createConnection } from '../../../../../test/factories/connection.js';
import { createPermission } from '../../../../../test/factories/permission.js';
describe('POST /api/v1/connections/:connectionId/test', () => {
let currentUser, currentUserRole, token;
beforeEach(async () => {
currentUser = await createUser();
currentUserRole = await currentUser.$relatedQuery('role');
token = createAuthTokenByUserId(currentUser.id);
});
it('should update the connection as not verified for current user', async () => {
const currentUserConnection = await createConnection({
userId: currentUser.id,
key: 'deepl',
verified: true,
});
await createPermission({
action: 'read',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
await createPermission({
action: 'update',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
const response = await request(app)
.post(`/api/v1/connections/${currentUserConnection.id}/test`)
.set('Authorization', token)
.expect(200);
expect(response.body.data.verified).toEqual(false);
});
it('should update the connection as not verified for another user', async () => {
const anotherUser = await createUser();
const anotherUserConnection = await createConnection({
userId: anotherUser.id,
key: 'deepl',
verified: true,
});
await createPermission({
action: 'read',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: [],
});
await createPermission({
action: 'update',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: [],
});
const response = await request(app)
.post(`/api/v1/connections/${anotherUserConnection.id}/test`)
.set('Authorization', token)
.expect(200);
expect(response.body.data.verified).toEqual(false);
});
it('should return not found response for not existing connection UUID', async () => {
const notExistingConnectionUUID = Crypto.randomUUID();
await createPermission({
action: 'read',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
await createPermission({
action: 'update',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
await request(app)
.post(`/api/v1/connections/${notExistingConnectionUUID}/test`)
.set('Authorization', token)
.expect(404);
});
it('should return bad request response for invalid UUID', async () => {
await createPermission({
action: 'read',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
await createPermission({
action: 'update',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
await request(app)
.post('/api/v1/connections/invalidConnectionUUID/test')
.set('Authorization', token)
.expect(400);
});
});

View File

@@ -0,0 +1,18 @@
import { renderObject } from '../../../../helpers/renderer.js';
export default async (request, response) => {
const step = await request.currentUser.authorizedSteps
.clone()
.where('steps.id', request.params.stepId)
.whereNotNull('steps.app_key')
.whereNotNull('steps.connection_id')
.first()
.throwIfNotFound();
const dynamicData = await step.createDynamicData(
request.body.dynamicDataKey,
request.body.parameters
);
renderObject(response, dynamicData);
};

View File

@@ -0,0 +1,244 @@
import { vi, describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import Crypto from 'crypto';
import app from '../../../../app.js';
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id';
import { createUser } from '../../../../../test/factories/user';
import { createConnection } from '../../../../../test/factories/connection';
import { createFlow } from '../../../../../test/factories/flow';
import { createStep } from '../../../../../test/factories/step';
import { createPermission } from '../../../../../test/factories/permission';
import listRepos from '../../../../apps/github/dynamic-data/list-repos/index.js';
import HttpError from '../../../../errors/http.js';
describe('POST /api/v1/steps/:stepId/dynamic-data', () => {
let currentUser, currentUserRole, token;
beforeEach(async () => {
currentUser = await createUser();
currentUserRole = await currentUser.$relatedQuery('role');
token = createAuthTokenByUserId(currentUser.id);
});
describe('should return dynamically created data', () => {
let repositories;
beforeEach(async () => {
repositories = [
{
value: 'automatisch/automatisch',
name: 'automatisch/automatisch',
},
{
value: 'automatisch/sample',
name: 'automatisch/sample',
},
];
vi.spyOn(listRepos, 'run').mockImplementation(async () => {
return {
data: repositories,
};
});
});
it('of the current users step', async () => {
const currentUserFlow = await createFlow({ userId: currentUser.id });
const connection = await createConnection({ userId: currentUser.id });
const actionStep = await createStep({
flowId: currentUserFlow.id,
connectionId: connection.id,
type: 'action',
appKey: 'github',
key: 'createIssue',
});
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
await createPermission({
action: 'update',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
const response = await request(app)
.post(`/api/v1/steps/${actionStep.id}/dynamic-data`)
.set('Authorization', token)
.send({
dynamicDataKey: 'listRepos',
parameters: {},
})
.expect(200);
expect(response.body.data).toEqual(repositories);
});
it('of the another users step', async () => {
const anotherUser = await createUser();
const anotherUserFlow = await createFlow({ userId: anotherUser.id });
const connection = await createConnection({ userId: anotherUser.id });
const actionStep = await createStep({
flowId: anotherUserFlow.id,
connectionId: connection.id,
type: 'action',
appKey: 'github',
key: 'createIssue',
});
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
await createPermission({
action: 'update',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
const response = await request(app)
.post(`/api/v1/steps/${actionStep.id}/dynamic-data`)
.set('Authorization', token)
.send({
dynamicDataKey: 'listRepos',
parameters: {},
})
.expect(200);
expect(response.body.data).toEqual(repositories);
});
});
describe('should return error for dynamically created data', () => {
let errors;
beforeEach(async () => {
errors = {
message: 'Not Found',
documentation_url:
'https://docs.github.com/rest/users/users#get-a-user',
};
vi.spyOn(listRepos, 'run').mockImplementation(async () => {
throw new HttpError({ message: errors });
});
});
it('of the current users step', async () => {
const currentUserFlow = await createFlow({ userId: currentUser.id });
const connection = await createConnection({ userId: currentUser.id });
const actionStep = await createStep({
flowId: currentUserFlow.id,
connectionId: connection.id,
type: 'action',
appKey: 'github',
key: 'createIssue',
});
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
await createPermission({
action: 'update',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
const response = await request(app)
.post(`/api/v1/steps/${actionStep.id}/dynamic-data`)
.set('Authorization', token)
.send({
dynamicDataKey: 'listRepos',
parameters: {},
})
.expect(200);
expect(response.body.errors).toEqual(errors);
});
});
it('should return not found response for not existing step UUID', async () => {
await createPermission({
action: 'update',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
const notExistingStepUUID = Crypto.randomUUID();
await request(app)
.get(`/api/v1/steps/${notExistingStepUUID}/dynamic-data`)
.set('Authorization', token)
.expect(404);
});
it('should return not found response for existing step UUID without app key', async () => {
await createPermission({
action: 'update',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
const step = await createStep({ appKey: null });
await request(app)
.get(`/api/v1/steps/${step.id}/dynamic-data`)
.set('Authorization', token)
.expect(404);
});
it('should return bad request response for invalid UUID', async () => {
await createPermission({
action: 'update',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
await request(app)
.post('/api/v1/steps/invalidStepUUID/dynamic-fields')
.set('Authorization', token)
.expect(400);
});
});

View File

@@ -0,0 +1,7 @@
import { renderObject } from '../../../../helpers/renderer.js';
export default async (request, response) => {
const apps = await request.currentUser.getApps(request.query.name);
renderObject(response, apps, { serializer: 'App' });
};

View File

@@ -0,0 +1,210 @@
import { describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import app from '../../../../app.js';
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id';
import { createRole } from '../../../../../test/factories/role';
import { createUser } from '../../../../../test/factories/user';
import { createPermission } from '../../../../../test/factories/permission.js';
import { createFlow } from '../../../../../test/factories/flow.js';
import { createStep } from '../../../../../test/factories/step.js';
import { createConnection } from '../../../../../test/factories/connection.js';
import getAppsMock from '../../../../../test/mocks/rest/api/v1/users/get-apps.js';
describe('GET /api/v1/users/:userId/apps', () => {
let currentUser, currentUserRole, token;
beforeEach(async () => {
currentUserRole = await createRole();
currentUser = await createUser({ roleId: currentUserRole.id });
token = createAuthTokenByUserId(currentUser.id);
});
it('should return all apps of the current user', async () => {
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
await createPermission({
action: 'read',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
const flowOne = await createFlow({ userId: currentUser.id });
await createStep({
flowId: flowOne.id,
appKey: 'webhook',
});
const flowOneActionStepConnection = await createConnection({
userId: currentUser.id,
key: 'deepl',
draft: false,
});
await createStep({
connectionId: flowOneActionStepConnection.id,
flowId: flowOne.id,
appKey: 'deepl',
});
const flowTwo = await createFlow({ userId: currentUser.id });
const flowTwoTriggerStepConnection = await createConnection({
userId: currentUser.id,
key: 'github',
draft: false,
});
await createStep({
connectionId: flowTwoTriggerStepConnection.id,
flowId: flowTwo.id,
appKey: 'github',
});
await createStep({
flowId: flowTwo.id,
appKey: 'slack',
});
const response = await request(app)
.get(`/api/v1/users/${currentUser.id}/apps`)
.set('Authorization', token)
.expect(200);
const expectedPayload = getAppsMock();
expect(response.body).toEqual(expectedPayload);
});
it('should return all apps of the another user', async () => {
const anotherUser = await createUser();
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
await createPermission({
action: 'read',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: [],
});
const flowOne = await createFlow({ userId: anotherUser.id });
await createStep({
flowId: flowOne.id,
appKey: 'webhook',
});
const flowOneActionStepConnection = await createConnection({
userId: anotherUser.id,
key: 'deepl',
draft: false,
});
await createStep({
connectionId: flowOneActionStepConnection.id,
flowId: flowOne.id,
appKey: 'deepl',
});
const flowTwo = await createFlow({ userId: anotherUser.id });
const flowTwoTriggerStepConnection = await createConnection({
userId: anotherUser.id,
key: 'github',
draft: false,
});
await createStep({
connectionId: flowTwoTriggerStepConnection.id,
flowId: flowTwo.id,
appKey: 'github',
});
await createStep({
flowId: flowTwo.id,
appKey: 'slack',
});
const response = await request(app)
.get(`/api/v1/users/${currentUser.id}/apps`)
.set('Authorization', token)
.expect(200);
const expectedPayload = getAppsMock();
expect(response.body).toEqual(expectedPayload);
});
it('should return specified app of the current user', async () => {
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
await createPermission({
action: 'read',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
const flowOne = await createFlow({ userId: currentUser.id });
await createStep({
flowId: flowOne.id,
appKey: 'webhook',
});
const flowOneActionStepConnection = await createConnection({
userId: currentUser.id,
key: 'deepl',
draft: false,
});
await createStep({
connectionId: flowOneActionStepConnection.id,
flowId: flowOne.id,
appKey: 'deepl',
});
const flowTwo = await createFlow({ userId: currentUser.id });
const flowTwoTriggerStepConnection = await createConnection({
userId: currentUser.id,
key: 'github',
draft: false,
});
await createStep({
connectionId: flowTwoTriggerStepConnection.id,
flowId: flowTwo.id,
appKey: 'github',
});
await createStep({
flowId: flowTwo.id,
appKey: 'slack',
});
const response = await request(app)
.get(`/api/v1/users/${currentUser.id}/apps?name=deepl`)
.set('Authorization', token)
.expect(200);
expect(response.body.data.length).toEqual(1);
expect(response.body.data[0].key).toEqual('deepl');
});
});

View File

@@ -6,10 +6,6 @@ export async function up(knex) {
export async function down(knex) { export async function down(knex) {
await knex.schema.table('app_auth_clients', (table) => { await knex.schema.table('app_auth_clients', (table) => {
table table.uuid('app_config_id').references('id').inTable('app_configs');
.uuid('app_config_id')
.notNullable()
.references('id')
.inTable('app_configs');
}); });
} }

View File

@@ -1,11 +0,0 @@
export async function up(knex) {
await knex.schema.table('app_configs', (table) => {
table.boolean('can_connect').defaultTo(false);
});
}
export async function down(knex) {
await knex.schema.table('app_configs', (table) => {
table.dropColumn('can_connect');
});
}

View File

@@ -1,11 +0,0 @@
export async function up(knex) {
await knex.schema.table('app_configs', (table) => {
table.boolean('can_custom_connect').defaultTo(false);
});
}
export async function down(knex) {
await knex.schema.table('app_configs', (table) => {
table.dropColumn('can_custom_connect');
});
}

View File

@@ -1,24 +0,0 @@
import AppAuthClient from '../../models/app-auth-client.js';
const getAppAuthClient = async (_parent, params, context) => {
let canSeeAllClients = false;
try {
context.currentUser.can('read', 'App');
canSeeAllClients = true;
} catch {
// void
}
const appAuthClient = AppAuthClient.query()
.findById(params.id)
.throwIfNotFound();
if (!canSeeAllClients) {
appAuthClient.where({ active: true });
}
return await appAuthClient;
};
export default getAppAuthClient;

View File

@@ -1,33 +0,0 @@
import AppConfig from '../../models/app-config.js';
const getAppAuthClients = async (_parent, params, context) => {
let canSeeAllClients = false;
try {
context.currentUser.can('read', 'App');
canSeeAllClients = true;
} catch {
// void
}
const appConfig = await AppConfig.query()
.findOne({
key: params.appKey,
})
.throwIfNotFound();
const appAuthClients = appConfig
.$relatedQuery('appAuthClients')
.where({ active: params.active })
.skipUndefined();
if (!canSeeAllClients) {
appAuthClients.where({
active: true,
});
}
return await appAuthClients;
};
export default getAppAuthClients;

View File

@@ -1,41 +0,0 @@
import App from '../../models/app.js';
import Connection from '../../models/connection.js';
const getApp = async (_parent, params, context) => {
const conditions = context.currentUser.can('read', 'Connection');
const userConnections = context.currentUser.$relatedQuery('connections');
const allConnections = Connection.query();
const connectionBaseQuery = conditions.isCreator
? userConnections
: allConnections;
const app = await App.findOneByKey(params.key);
if (context.currentUser) {
const connections = await connectionBaseQuery
.clone()
.select('connections.*')
.withGraphFetched({
appConfig: true,
appAuthClient: true,
})
.fullOuterJoinRelated('steps')
.where({
'connections.key': params.key,
'connections.draft': false,
})
.countDistinct('steps.flow_id as flowCount')
.groupBy('connections.id')
.orderBy('created_at', 'desc');
return {
...app,
connections,
};
}
return app;
};
export default getApp;

View File

@@ -1,101 +0,0 @@
import { DateTime } from 'luxon';
import Billing from '../../helpers/billing/index.ee.js';
import ExecutionStep from '../../models/execution-step.js';
const getBillingAndUsage = async (_parent, _params, context) => {
const persistedSubscription = await context.currentUser.$relatedQuery(
'currentSubscription'
);
const subscription = persistedSubscription
? paidSubscription(persistedSubscription)
: freeTrialSubscription();
return {
subscription,
usage: {
task: executionStepCount(context),
},
};
};
const paidSubscription = (subscription) => {
const currentPlan = Billing.paddlePlans.find(
(plan) => plan.productId === subscription.paddlePlanId
);
return {
status: subscription.status,
monthlyQuota: {
title: currentPlan.limit,
action: {
type: 'link',
text: 'Cancel plan',
src: subscription.cancelUrl,
},
},
nextBillAmount: {
title: subscription.nextBillAmount
? '€' + subscription.nextBillAmount
: '---',
action: {
type: 'link',
text: 'Update payment method',
src: subscription.updateUrl,
},
},
nextBillDate: {
title: subscription.nextBillDate ? subscription.nextBillDate : '---',
action: {
type: 'text',
text: '(monthly payment)',
},
},
};
};
const freeTrialSubscription = () => {
return {
status: null,
monthlyQuota: {
title: 'Free Trial',
action: {
type: 'link',
text: 'Upgrade plan',
src: '/settings/billing/upgrade',
},
},
nextBillAmount: {
title: '---',
action: null,
},
nextBillDate: {
title: '---',
action: null,
},
};
};
const executionIds = async (context) => {
return (
await context.currentUser
.$relatedQuery('executions')
.select('executions.id')
).map((execution) => execution.id);
};
const executionStepCount = async (context) => {
const executionStepCount = await ExecutionStep.query()
.whereIn('execution_id', await executionIds(context))
.andWhere(
'created_at',
'>=',
DateTime.now().minus({ days: 30 }).toISODate()
)
.count()
.first();
return executionStepCount.count;
};
export default getBillingAndUsage;

View File

@@ -1,67 +0,0 @@
import App from '../../models/app.js';
import Flow from '../../models/flow.js';
import Connection from '../../models/connection.js';
const getConnectedApps = async (_parent, params, context) => {
const conditions = context.currentUser.can('read', 'Connection');
const userConnections = context.currentUser.$relatedQuery('connections');
const allConnections = Connection.query();
const connectionBaseQuery = conditions.isCreator
? userConnections
: allConnections;
const userFlows = context.currentUser.$relatedQuery('flows');
const allFlows = Flow.query();
const flowBaseQuery = conditions.isCreator ? userFlows : allFlows;
let apps = await App.findAll(params.name);
const connections = await connectionBaseQuery
.clone()
.select('connections.key')
.where({ draft: false })
.count('connections.id as count')
.groupBy('connections.key');
const flows = await flowBaseQuery
.clone()
.withGraphJoined('steps')
.orderBy('created_at', 'desc');
const duplicatedUsedApps = flows
.map((flow) => flow.steps.map((step) => step.appKey))
.flat()
.filter(Boolean);
const connectionKeys = connections.map((connection) => connection.key);
const usedApps = [...new Set([...duplicatedUsedApps, ...connectionKeys])];
apps = apps
.filter((app) => {
return usedApps.includes(app.key);
})
.map((app) => {
const connection = connections.find(
(connection) => connection.key === app.key
);
app.connectionCount = connection?.count || 0;
app.flowCount = 0;
flows.forEach((flow) => {
const usedFlow = flow.steps.find((step) => step.appKey === app.key);
if (usedFlow) {
app.flowCount += 1;
}
});
return app;
})
.sort((appA, appB) => appA.name.localeCompare(appB.name));
return apps;
};
export default getConnectedApps;

View File

@@ -1,65 +0,0 @@
import App from '../../models/app.js';
import Step from '../../models/step.js';
import ExecutionStep from '../../models/execution-step.js';
import globalVariable from '../../helpers/global-variable.js';
import computeParameters from '../../helpers/compute-parameters.js';
const getDynamicData = async (_parent, params, context) => {
const conditions = context.currentUser.can('update', 'Flow');
const userSteps = context.currentUser.$relatedQuery('steps');
const allSteps = Step.query();
const stepBaseQuery = conditions.isCreator ? userSteps : allSteps;
const step = await stepBaseQuery
.clone()
.withGraphFetched({
connection: true,
flow: true,
})
.findById(params.stepId);
if (!step) return null;
const connection = step.connection;
if (!connection || !step.appKey) return null;
const flow = step.flow;
const app = await App.findOneByKey(step.appKey);
const $ = await globalVariable({ connection, app, flow, step });
const command = app.dynamicData.find((data) => data.key === params.key);
// apply run-time parameters that're not persisted yet
for (const parameterKey in params.parameters) {
const parameterValue = params.parameters[parameterKey];
$.step.parameters[parameterKey] = parameterValue;
}
const lastExecution = await flow.$relatedQuery('lastExecution');
const lastExecutionId = lastExecution?.id;
const priorExecutionSteps = lastExecutionId
? await ExecutionStep.query().where({
execution_id: lastExecutionId,
})
: [];
// compute variables in parameters
const computedParameters = computeParameters(
$.step.parameters,
priorExecutionSteps
);
$.step.parameters = computedParameters;
const fetchedData = await command.run($);
if (fetchedData.error) {
throw new Error(JSON.stringify(fetchedData.error));
}
return fetchedData.data;
};
export default getDynamicData;

View File

@@ -1,19 +0,0 @@
import Flow from '../../models/flow.js';
const getFlow = async (_parent, params, context) => {
const conditions = context.currentUser.can('read', 'Flow');
const userFlows = context.currentUser.$relatedQuery('flows');
const allFlows = Flow.query();
const baseQuery = conditions.isCreator ? userFlows : allFlows;
const flow = await baseQuery
.clone()
.withGraphJoined('[steps.[connection]]')
.orderBy('steps.position', 'asc')
.findOne({ 'flows.id': params.id })
.throwIfNotFound();
return flow;
};
export default getFlow;

View File

@@ -1,240 +0,0 @@
import { describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import app from '../../app';
import appConfig from '../../config/app';
import createAuthTokenByUserId from '../../helpers/create-auth-token-by-user-id';
import { createRole } from '../../../test/factories/role';
import { createPermission } from '../../../test/factories/permission';
import { createUser } from '../../../test/factories/user';
import { createFlow } from '../../../test/factories/flow';
import { createStep } from '../../../test/factories/step';
import { createConnection } from '../../../test/factories/connection';
describe('graphQL getFlow query', () => {
const query = (flowId) => {
return `
query {
getFlow(id: "${flowId}") {
id
name
active
status
steps {
id
type
key
appKey
iconUrl
webhookUrl
status
position
connection {
id
verified
createdAt
}
parameters
}
}
}
`;
};
describe('and without permissions', () => {
it('should throw not authorized error', async () => {
const userWithoutPermissions = await createUser();
const token = createAuthTokenByUserId(userWithoutPermissions.id);
const flow = await createFlow();
const response = await request(app)
.post('/graphql')
.set('Authorization', token)
.send({ query: query(flow.id) })
.expect(200);
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toEqual('Not authorized!');
});
});
describe('and with correct permission', () => {
let currentUser, currentUserRole, currentUserFlow;
beforeEach(async () => {
currentUserRole = await createRole();
currentUser = await createUser({ roleId: currentUserRole.id });
currentUserFlow = await createFlow({ userId: currentUser.id });
});
describe('and with isCreator condition', () => {
it('should return executions data of the current user', async () => {
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
const triggerStep = await createStep({
flowId: currentUserFlow.id,
type: 'trigger',
key: 'catchRawWebhook',
webhookPath: `/webhooks/flows/${currentUserFlow.id}`,
});
const actionConnection = await createConnection({
userId: currentUser.id,
formattedData: {
screenName: 'Test',
authenticationKey: 'test key',
},
});
const actionStep = await createStep({
flowId: currentUserFlow.id,
type: 'action',
connectionId: actionConnection.id,
key: 'translateText',
});
const token = createAuthTokenByUserId(currentUser.id);
const response = await request(app)
.post('/graphql')
.set('Authorization', token)
.send({ query: query(currentUserFlow.id) })
.expect(200);
const expectedResponsePayload = {
data: {
getFlow: {
active: currentUserFlow.active,
id: currentUserFlow.id,
name: currentUserFlow.name,
status: 'draft',
steps: [
{
appKey: triggerStep.appKey,
connection: null,
iconUrl: `${appConfig.baseUrl}/apps/${triggerStep.appKey}/assets/favicon.svg`,
id: triggerStep.id,
key: 'catchRawWebhook',
parameters: {},
position: 1,
status: triggerStep.status,
type: 'trigger',
webhookUrl: `${appConfig.baseUrl}/webhooks/flows/${currentUserFlow.id}`,
},
{
appKey: actionStep.appKey,
connection: {
createdAt: actionConnection.createdAt.getTime().toString(),
id: actionConnection.id,
verified: actionConnection.verified,
},
iconUrl: `${appConfig.baseUrl}/apps/${actionStep.appKey}/assets/favicon.svg`,
id: actionStep.id,
key: 'translateText',
parameters: {},
position: 2,
status: actionStep.status,
type: 'action',
webhookUrl: 'http://localhost:3000/null',
},
],
},
},
};
expect(response.body).toEqual(expectedResponsePayload);
});
});
describe('and without isCreator condition', () => {
it('should return executions data of all users', async () => {
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
const anotherUser = await createUser();
const anotherUserFlow = await createFlow({ userId: anotherUser.id });
const triggerStep = await createStep({
flowId: anotherUserFlow.id,
type: 'trigger',
key: 'catchRawWebhook',
webhookPath: `/webhooks/flows/${anotherUserFlow.id}`,
});
const actionConnection = await createConnection({
userId: anotherUser.id,
formattedData: {
screenName: 'Test',
authenticationKey: 'test key',
},
});
const actionStep = await createStep({
flowId: anotherUserFlow.id,
type: 'action',
connectionId: actionConnection.id,
key: 'translateText',
});
const token = createAuthTokenByUserId(currentUser.id);
const response = await request(app)
.post('/graphql')
.set('Authorization', token)
.send({ query: query(anotherUserFlow.id) })
.expect(200);
const expectedResponsePayload = {
data: {
getFlow: {
active: anotherUserFlow.active,
id: anotherUserFlow.id,
name: anotherUserFlow.name,
status: 'draft',
steps: [
{
appKey: triggerStep.appKey,
connection: null,
iconUrl: `${appConfig.baseUrl}/apps/${triggerStep.appKey}/assets/favicon.svg`,
id: triggerStep.id,
key: 'catchRawWebhook',
parameters: {},
position: 1,
status: triggerStep.status,
type: 'trigger',
webhookUrl: `${appConfig.baseUrl}/webhooks/flows/${anotherUserFlow.id}`,
},
{
appKey: actionStep.appKey,
connection: {
createdAt: actionConnection.createdAt.getTime().toString(),
id: actionConnection.id,
verified: actionConnection.verified,
},
iconUrl: `${appConfig.baseUrl}/apps/${actionStep.appKey}/assets/favicon.svg`,
id: actionStep.id,
key: 'translateText',
parameters: {},
position: 2,
status: actionStep.status,
type: 'action',
webhookUrl: 'http://localhost:3000/null',
},
],
},
},
};
expect(response.body).toEqual(expectedResponsePayload);
});
});
});
});

View File

@@ -1,34 +0,0 @@
import { ref } from 'objection';
import ExecutionStep from '../../models/execution-step.js';
import Step from '../../models/step.js';
const getStepWithTestExecutions = async (_parent, params, context) => {
const conditions = context.currentUser.can('update', 'Flow');
const userSteps = context.currentUser.$relatedQuery('steps');
const allSteps = Step.query();
const stepBaseQuery = conditions.isCreator ? userSteps : allSteps;
const step = await stepBaseQuery
.clone()
.findOne({ 'steps.id': params.stepId })
.throwIfNotFound();
const previousStepsWithCurrentStep = await stepBaseQuery
.clone()
.withGraphJoined('executionSteps')
.where('flow_id', '=', step.flowId)
.andWhere('position', '<', step.position)
.andWhere(
'executionSteps.created_at',
'=',
ExecutionStep.query()
.max('created_at')
.where('step_id', '=', ref('steps.id'))
.andWhere('status', 'success')
)
.orderBy('steps.position', 'asc');
return previousStepsWithCurrentStep;
};
export default getStepWithTestExecutions;

View File

@@ -1,38 +0,0 @@
import App from '../../models/app.js';
import Connection from '../../models/connection.js';
import globalVariable from '../../helpers/global-variable.js';
const testConnection = async (_parent, params, context) => {
const conditions = context.currentUser.can('update', 'Connection');
const userConnections = context.currentUser.$relatedQuery('connections');
const allConnections = Connection.query();
const connectionBaseQuery = conditions.isCreator
? userConnections
: allConnections;
let connection = await connectionBaseQuery
.clone()
.findOne({
id: params.id,
})
.throwIfNotFound();
const app = await App.findOneByKey(connection.key, false);
const $ = await globalVariable({ connection, app });
let isStillVerified;
try {
isStillVerified = !!(await app.auth.isStillVerified($));
} catch {
isStillVerified = false;
}
connection = await connection.$query().patchAndFetch({
formattedData: connection.formattedData,
verified: isStillVerified,
});
return connection;
};
export default testConnection;

View File

@@ -1,23 +0,0 @@
import getApp from './queries/get-app.js';
import getAppAuthClient from './queries/get-app-auth-client.ee.js';
import getAppAuthClients from './queries/get-app-auth-clients.ee.js';
import getBillingAndUsage from './queries/get-billing-and-usage.ee.js';
import getConnectedApps from './queries/get-connected-apps.js';
import getDynamicData from './queries/get-dynamic-data.js';
import getFlow from './queries/get-flow.js';
import getStepWithTestExecutions from './queries/get-step-with-test-executions.js';
import testConnection from './queries/test-connection.js';
const queryResolvers = {
getApp,
getAppAuthClient,
getAppAuthClients,
getBillingAndUsage,
getConnectedApps,
getDynamicData,
getFlow,
getStepWithTestExecutions,
testConnection,
};
export default queryResolvers;

View File

@@ -1,8 +1,6 @@
import mutationResolvers from './mutation-resolvers.js'; import mutationResolvers from './mutation-resolvers.js';
import queryResolvers from './query-resolvers.js';
const resolvers = { const resolvers = {
Query: queryResolvers,
Mutation: mutationResolvers, Mutation: mutationResolvers,
}; };

View File

@@ -1,19 +1,6 @@
type Query { type Query {
getApp(key: String!): App placeholderQuery(name: String): Boolean
getAppAuthClient(id: String!): AppAuthClient
getAppAuthClients(appKey: String!, active: Boolean): [AppAuthClient]
getConnectedApps(name: String): [App]
testConnection(id: String!): Connection
getFlow(id: String!): Flow
getStepWithTestExecutions(stepId: String!): [Step]
getDynamicData(
stepId: String!
key: String!
parameters: JSONObject
): JSONObject
getBillingAndUsage: GetBillingAndUsage
} }
type Mutation { type Mutation {
createAppConfig(input: CreateAppConfigInput): AppConfig createAppConfig(input: CreateAppConfigInput): AppConfig
createAppAuthClient(input: CreateAppAuthClientInput): AppAuthClient createAppAuthClient(input: CreateAppAuthClientInput): AppAuthClient
@@ -563,43 +550,6 @@ type License {
verified: Boolean verified: Boolean
} }
type GetBillingAndUsage {
subscription: Subscription
usage: Usage
}
type MonthlyQuota {
title: String
action: BillingCardAction
}
type NextBillAmount {
title: String
action: BillingCardAction
}
type NextBillDate {
title: String
action: BillingCardAction
}
type BillingCardAction {
type: String
text: String
src: String
}
type Subscription {
status: String
monthlyQuota: MonthlyQuota
nextBillAmount: NextBillAmount
nextBillDate: NextBillDate
}
type Usage {
task: Int
}
type Permission { type Permission {
id: String id: String
action: String action: String

View File

@@ -40,9 +40,6 @@ export const authenticateUser = async (request, response, next) => {
const isAuthenticatedRule = rule()(isAuthenticated); const isAuthenticatedRule = rule()(isAuthenticated);
export const authenticationRules = { export const authenticationRules = {
Query: {
'*': isAuthenticatedRule,
},
Mutation: { Mutation: {
'*': isAuthenticatedRule, '*': isAuthenticatedRule,
forgotPassword: allow, forgotPassword: allow,

View File

@@ -42,19 +42,21 @@ describe('authentication rules', () => {
const { queries, mutations } = getQueryAndMutationNames(authenticationRules); const { queries, mutations } = getQueryAndMutationNames(authenticationRules);
describe('for queries', () => { if (queries.length) {
queries.forEach((query) => { describe('for queries', () => {
it(`should apply correct rule for query: ${query}`, () => { queries.forEach((query) => {
const ruleApplied = authenticationRules.Query[query]; it(`should apply correct rule for query: ${query}`, () => {
const ruleApplied = authenticationRules.Query[query];
if (query === '*') { if (query === '*') {
expect(ruleApplied.func).toBe(isAuthenticated); expect(ruleApplied.func).toBe(isAuthenticated);
} else { } else {
expect(ruleApplied).toEqual(allow); expect(ruleApplied).toEqual(allow);
} }
});
}); });
}); });
}); }
describe('for mutations', () => { describe('for mutations', () => {
mutations.forEach((mutation) => { mutations.forEach((mutation) => {

View File

@@ -7,6 +7,10 @@ const authorizationList = {
action: 'read', action: 'read',
subject: 'User', subject: 'User',
}, },
'GET /api/v1/users/:userId/apps': {
action: 'read',
subject: 'Connection',
},
'GET /api/v1/flows/:flowId': { 'GET /api/v1/flows/:flowId': {
action: 'read', action: 'read',
subject: 'Flow', subject: 'Flow',
@@ -27,14 +31,26 @@ const authorizationList = {
action: 'update', action: 'update',
subject: 'Flow', subject: 'Flow',
}, },
'POST /api/v1/steps/:stepId/dynamic-data': {
action: 'update',
subject: 'Flow',
},
'GET /api/v1/connections/:connectionId/flows': { 'GET /api/v1/connections/:connectionId/flows': {
action: 'read', action: 'read',
subject: 'Flow', subject: 'Flow',
}, },
'POST /api/v1/connections/:connectionId/test': {
action: 'update',
subject: 'Connection',
},
'GET /api/v1/apps/:appKey/flows': { 'GET /api/v1/apps/:appKey/flows': {
action: 'read', action: 'read',
subject: 'Flow', subject: 'Flow',
}, },
'GET /api/v1/apps/:appKey/connections': {
action: 'read',
subject: 'Connection',
},
'GET /api/v1/executions/:executionId': { 'GET /api/v1/executions/:executionId': {
action: 'read', action: 'read',
subject: 'Execution', subject: 'Execution',

View File

@@ -2,7 +2,7 @@ import axios from 'axios';
import { HttpsProxyAgent } from 'https-proxy-agent'; import { HttpsProxyAgent } from 'https-proxy-agent';
import { HttpProxyAgent } from 'http-proxy-agent'; import { HttpProxyAgent } from 'http-proxy-agent';
const config = {}; const config = axios.defaults;
const httpProxyUrl = process.env.http_proxy; const httpProxyUrl = process.env.http_proxy;
const httpsProxyUrl = process.env.https_proxy; const httpsProxyUrl = process.env.https_proxy;
const supportsProxy = httpProxyUrl || httpsProxyUrl; const supportsProxy = httpProxyUrl || httpsProxyUrl;

View File

@@ -2,6 +2,7 @@ import logger from './logger.js';
import objection from 'objection'; import objection from 'objection';
import * as Sentry from './sentry.ee.js'; import * as Sentry from './sentry.ee.js';
const { NotFoundError, DataError } = objection; const { NotFoundError, DataError } = objection;
import HttpError from '../errors/http.js';
// Do not remove `next` argument as the function signature will not fit for an error handler middleware // Do not remove `next` argument as the function signature will not fit for an error handler middleware
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
@@ -18,6 +19,17 @@ const errorHandler = (error, request, response, next) => {
response.status(400).end(); response.status(400).end();
} }
if (error instanceof HttpError) {
const httpErrorPayload = {
errors: JSON.parse(error.message),
meta: {
type: 'HttpError',
},
};
response.status(200).json(httpErrorPayload);
}
const statusCode = error.statusCode || 500; const statusCode = error.statusCode || 500;
logger.error(request.method + ' ' + request.url + ' ' + statusCode); logger.error(request.method + ' ' + request.url + ' ' + statusCode);
@@ -37,7 +49,7 @@ const errorHandler = (error, request, response, next) => {
const notFoundAppError = (error) => { const notFoundAppError = (error) => {
return ( return (
error.message.includes('An application with the') || error.message.includes('An application with the') &&
error.message.includes("key couldn't be found.") error.message.includes("key couldn't be found.")
); );
}; };

View File

@@ -1,7 +1,6 @@
import AES from 'crypto-js/aes.js'; import AES from 'crypto-js/aes.js';
import enc from 'crypto-js/enc-utf8.js'; import enc from 'crypto-js/enc-utf8.js';
import appConfig from '../config/app.js'; import appConfig from '../config/app.js';
import AppConfig from './app-config.js';
import Base from './base.js'; import Base from './base.js';
class AppAuthClient extends Base { class AppAuthClient extends Base {
@@ -60,21 +59,6 @@ class AppAuthClient extends Base {
this.encryptData(); this.encryptData();
} }
async assignCanConnectForAppConfig() {
const appConfig = await AppConfig.query().findOne({ key: this.appKey });
await appConfig?.$query()?.patch({});
}
async $afterInsert(queryContext) {
await super.$afterInsert(queryContext);
await this.assignCanConnectForAppConfig();
}
async $afterUpdate(opt, queryContext) {
await super.$afterUpdate(opt, queryContext);
await this.assignCanConnectForAppConfig();
}
async $afterFind() { async $afterFind() {
this.decryptData(); this.decryptData();
} }

View File

@@ -15,56 +15,45 @@ class AppConfig extends Base {
allowCustomConnection: { type: 'boolean', default: false }, allowCustomConnection: { type: 'boolean', default: false },
shared: { type: 'boolean', default: false }, shared: { type: 'boolean', default: false },
disabled: { type: 'boolean', default: false }, disabled: { type: 'boolean', default: false },
canConnect: { type: 'boolean', default: false },
canCustomConnect: { type: 'boolean', default: false },
}, },
}; };
static relationMappings = () => ({
appAuthClients: {
relation: Base.HasManyRelation,
modelClass: AppAuthClient,
join: {
from: 'app_configs.key',
to: 'app_auth_clients.app_key',
},
},
});
static get virtualAttributes() {
return ['canConnect', 'canCustomConnect'];
}
get canCustomConnect() {
return !this.disabled && this.allowCustomConnection;
}
get canConnect() {
const hasSomeActiveAppAuthClients = !!this.appAuthClients?.some(
(appAuthClient) => appAuthClient.active
);
const shared = this.shared;
const active = this.disabled === false;
const conditions = [hasSomeActiveAppAuthClients, shared, active];
return conditions.every(Boolean);
}
async getApp() { async getApp() {
if (!this.key) return null; if (!this.key) return null;
return await App.findOneByKey(this.key); return await App.findOneByKey(this.key);
} }
async hasActiveAppAuthClients() {
const appAuthClients = await AppAuthClient.query().where({
appKey: this.key,
});
const hasSomeActiveAppAuthClients = !!appAuthClients?.some(
(appAuthClient) => appAuthClient.active
);
return hasSomeActiveAppAuthClients;
}
async assignCanConnect() {
const shared = this.shared;
const active = this.disabled === false;
const hasSomeActiveAppAuthClients = await this.hasActiveAppAuthClients();
const conditions = [hasSomeActiveAppAuthClients, shared, active];
const canConnect = conditions.every(Boolean);
this.canConnect = canConnect;
}
async assignCanCustomConnect() {
const canCustomConnect = !this.disabled && this.allowCustomConnection;
this.canCustomConnect = canCustomConnect;
}
async $beforeInsert(queryContext) {
await super.$beforeInsert(queryContext);
await this.assignCanConnect();
await this.assignCanCustomConnect();
}
async $beforeUpdate(opt, queryContext) {
await super.$beforeUpdate(opt, queryContext);
await this.assignCanConnect();
await this.assignCanCustomConnect();
}
} }
export default AppConfig; export default AppConfig;

View File

@@ -153,6 +153,24 @@ class Connection extends Base {
return await App.findOneByKey(this.key); return await App.findOneByKey(this.key);
} }
async testAndUpdateConnection() {
const app = await this.getApp();
const $ = await globalVariable({ connection: this, app });
let isStillVerified;
try {
isStillVerified = !!(await app.auth.isStillVerified($));
} catch {
isStillVerified = false;
}
return await this.$query().patchAndFetch({
formattedData: this.formattedData,
verified: isStillVerified,
});
}
async verifyWebhook(request) { async verifyWebhook(request) {
if (!this.key) return true; if (!this.key) return true;

View File

@@ -160,7 +160,7 @@ class Flow extends Base {
} }
async isPaused() { async isPaused() {
const user = await this.$relatedQuery('user'); const user = await this.$relatedQuery('user').withSoftDeleted();
const allowedToRunFlows = await user.isAllowedToRunFlows(); const allowedToRunFlows = await user.isAllowedToRunFlows();
return allowedToRunFlows ? false : true; return allowedToRunFlows ? false : true;
} }

View File

@@ -8,6 +8,7 @@ import ExecutionStep from './execution-step.js';
import Telemetry from '../helpers/telemetry/index.js'; import Telemetry from '../helpers/telemetry/index.js';
import appConfig from '../config/app.js'; import appConfig from '../config/app.js';
import globalVariable from '../helpers/global-variable.js'; import globalVariable from '../helpers/global-variable.js';
import computeParameters from '../helpers/compute-parameters.js';
class Step extends Base { class Step extends Base {
static tableName = 'steps'; static tableName = 'steps';
@@ -217,6 +218,39 @@ class Step extends Base {
return dynamicFields; return dynamicFields;
} }
async createDynamicData(dynamicDataKey, parameters) {
const connection = await this.$relatedQuery('connection');
const flow = await this.$relatedQuery('flow');
const app = await this.getApp();
const $ = await globalVariable({ connection, app, flow, step: this });
const command = app.dynamicData.find((data) => data.key === dynamicDataKey);
for (const parameterKey in parameters) {
const parameterValue = parameters[parameterKey];
$.step.parameters[parameterKey] = parameterValue;
}
const lastExecution = await flow.$relatedQuery('lastExecution');
const lastExecutionId = lastExecution?.id;
const priorExecutionSteps = lastExecutionId
? await ExecutionStep.query().where({
execution_id: lastExecutionId,
})
: [];
const computedParameters = computeParameters(
$.step.parameters,
priorExecutionSteps
);
$.step.parameters = computedParameters;
const dynamicData = (await command.run($)).data;
return dynamicData;
}
async updateWebhookUrl() { async updateWebhookUrl() {
if (this.isAction) return this; if (this.isAction) return this;

View File

@@ -7,6 +7,7 @@ import { hasValidLicense } from '../helpers/license.ee.js';
import userAbility from '../helpers/user-ability.js'; import userAbility from '../helpers/user-ability.js';
import createAuthTokenByUserId from '../helpers/create-auth-token-by-user-id.js'; import createAuthTokenByUserId from '../helpers/create-auth-token-by-user-id.js';
import Base from './base.js'; import Base from './base.js';
import App from './app.js';
import Connection from './connection.js'; import Connection from './connection.js';
import Execution from './execution.js'; import Execution from './execution.js';
import Flow from './flow.js'; import Flow from './flow.js';
@@ -155,6 +156,13 @@ class User extends Base {
return conditions.isCreator ? this.$relatedQuery('steps') : Step.query(); return conditions.isCreator ? this.$relatedQuery('steps') : Step.query();
} }
get authorizedConnections() {
const conditions = this.can('read', 'Connection');
return conditions.isCreator
? this.$relatedQuery('connections')
: Connection.query();
}
get authorizedExecutions() { get authorizedExecutions() {
const conditions = this.can('read', 'Execution'); const conditions = this.can('read', 'Execution');
return conditions.isCreator return conditions.isCreator
@@ -306,6 +314,56 @@ class User extends Base {
return invoices; return invoices;
} }
async getApps(name) {
const connections = await this.authorizedConnections
.clone()
.select('connections.key')
.where({ draft: false })
.count('connections.id as count')
.groupBy('connections.key');
const flows = await this.authorizedFlows
.clone()
.withGraphJoined('steps')
.orderBy('created_at', 'desc');
const duplicatedUsedApps = flows
.map((flow) => flow.steps.map((step) => step.appKey))
.flat()
.filter(Boolean);
const connectionKeys = connections.map((connection) => connection.key);
const usedApps = [...new Set([...duplicatedUsedApps, ...connectionKeys])];
let apps = await App.findAll(name);
apps = apps
.filter((app) => {
return usedApps.includes(app.key);
})
.map((app) => {
const connection = connections.find(
(connection) => connection.key === app.key
);
app.connectionCount = connection?.count || 0;
app.flowCount = 0;
flows.forEach((flow) => {
const usedFlow = flow.steps.find((step) => step.appKey === app.key);
if (usedFlow) {
app.flowCount += 1;
}
});
return app;
})
.sort((appA, appB) => appA.name.localeCompare(appB.name));
return apps;
}
async $beforeInsert(queryContext) { async $beforeInsert(queryContext) {
await super.$beforeInsert(queryContext); await super.$beforeInsert(queryContext);

View File

@@ -6,6 +6,7 @@ import { checkIsEnterprise } from '../../../helpers/check-is-enterprise.js';
import getAppAction from '../../../controllers/api/v1/apps/get-app.js'; import getAppAction from '../../../controllers/api/v1/apps/get-app.js';
import getAppsAction from '../../../controllers/api/v1/apps/get-apps.js'; import getAppsAction from '../../../controllers/api/v1/apps/get-apps.js';
import getAuthAction from '../../../controllers/api/v1/apps/get-auth.js'; import getAuthAction from '../../../controllers/api/v1/apps/get-auth.js';
import getConnectionsAction from '../../../controllers/api/v1/apps/get-connections.js';
import getConfigAction from '../../../controllers/api/v1/apps/get-config.ee.js'; import getConfigAction from '../../../controllers/api/v1/apps/get-config.ee.js';
import getAuthClientsAction from '../../../controllers/api/v1/apps/get-auth-clients.ee.js'; import getAuthClientsAction from '../../../controllers/api/v1/apps/get-auth-clients.ee.js';
import getAuthClientAction from '../../../controllers/api/v1/apps/get-auth-client.ee.js'; import getAuthClientAction from '../../../controllers/api/v1/apps/get-auth-client.ee.js';
@@ -21,6 +22,13 @@ router.get('/', authenticateUser, asyncHandler(getAppsAction));
router.get('/:appKey', authenticateUser, asyncHandler(getAppAction)); router.get('/:appKey', authenticateUser, asyncHandler(getAppAction));
router.get('/:appKey/auth', authenticateUser, asyncHandler(getAuthAction)); router.get('/:appKey/auth', authenticateUser, asyncHandler(getAuthAction));
router.get(
'/:appKey/connections',
authenticateUser,
authorizeUser,
asyncHandler(getConnectionsAction)
);
router.get( router.get(
'/:appKey/config', '/:appKey/config',
authenticateUser, authenticateUser,

View File

@@ -3,6 +3,7 @@ import asyncHandler from 'express-async-handler';
import { authenticateUser } from '../../../helpers/authentication.js'; import { authenticateUser } from '../../../helpers/authentication.js';
import { authorizeUser } from '../../../helpers/authorization.js'; import { authorizeUser } from '../../../helpers/authorization.js';
import getFlowsAction from '../../../controllers/api/v1/connections/get-flows.js'; import getFlowsAction from '../../../controllers/api/v1/connections/get-flows.js';
import createTestAction from '../../../controllers/api/v1/connections/create-test.js';
const router = Router(); const router = Router();
@@ -13,4 +14,11 @@ router.get(
asyncHandler(getFlowsAction) asyncHandler(getFlowsAction)
); );
router.post(
'/:connectionId/test',
authenticateUser,
authorizeUser,
asyncHandler(createTestAction)
);
export default router; export default router;

View File

@@ -5,6 +5,7 @@ import { authorizeUser } from '../../../helpers/authorization.js';
import getConnectionAction from '../../../controllers/api/v1/steps/get-connection.js'; import getConnectionAction from '../../../controllers/api/v1/steps/get-connection.js';
import getPreviousStepsAction from '../../../controllers/api/v1/steps/get-previous-steps.js'; import getPreviousStepsAction from '../../../controllers/api/v1/steps/get-previous-steps.js';
import createDynamicFieldsAction from '../../../controllers/api/v1/steps/create-dynamic-fields.js'; import createDynamicFieldsAction from '../../../controllers/api/v1/steps/create-dynamic-fields.js';
import createDynamicDataAction from '../../../controllers/api/v1/steps/create-dynamic-data.js';
const router = Router(); const router = Router();
@@ -29,4 +30,11 @@ router.post(
asyncHandler(createDynamicFieldsAction) asyncHandler(createDynamicFieldsAction)
); );
router.post(
'/:stepId/dynamic-data',
authenticateUser,
authorizeUser,
asyncHandler(createDynamicDataAction)
);
export default router; export default router;

View File

@@ -1,9 +1,11 @@
import { Router } from 'express'; import { Router } from 'express';
import asyncHandler from 'express-async-handler'; import asyncHandler from 'express-async-handler';
import { authenticateUser } from '../../../helpers/authentication.js'; import { authenticateUser } from '../../../helpers/authentication.js';
import { authorizeUser } from '../../../helpers/authorization.js';
import checkIsCloud from '../../../helpers/check-is-cloud.js'; import checkIsCloud from '../../../helpers/check-is-cloud.js';
import getCurrentUserAction from '../../../controllers/api/v1/users/get-current-user.js'; import getCurrentUserAction from '../../../controllers/api/v1/users/get-current-user.js';
import getUserTrialAction from '../../../controllers/api/v1/users/get-user-trial.ee.js'; import getUserTrialAction from '../../../controllers/api/v1/users/get-user-trial.ee.js';
import getAppsAction from '../../../controllers/api/v1/users/get-apps.js';
import getInvoicesAction from '../../../controllers/api/v1/users/get-invoices.ee.js'; import getInvoicesAction from '../../../controllers/api/v1/users/get-invoices.ee.js';
import getSubscriptionAction from '../../../controllers/api/v1/users/get-subscription.ee.js'; import getSubscriptionAction from '../../../controllers/api/v1/users/get-subscription.ee.js';
import getPlanAndUsageAction from '../../../controllers/api/v1/users/get-plan-and-usage.ee.js'; import getPlanAndUsageAction from '../../../controllers/api/v1/users/get-plan-and-usage.ee.js';
@@ -11,6 +13,14 @@ import getPlanAndUsageAction from '../../../controllers/api/v1/users/get-plan-an
const router = Router(); const router = Router();
router.get('/me', authenticateUser, asyncHandler(getCurrentUserAction)); router.get('/me', authenticateUser, asyncHandler(getCurrentUserAction));
router.get(
'/:userId/apps',
authenticateUser,
authorizeUser,
asyncHandler(getAppsAction)
);
router.get( router.get(
'/invoices', '/invoices',
authenticateUser, authenticateUser,

View File

@@ -1,12 +1,22 @@
const appSerializer = (app) => { const appSerializer = (app) => {
return { let appData = {
name: app.name,
key: app.key, key: app.key,
name: app.name,
iconUrl: app.iconUrl, iconUrl: app.iconUrl,
primaryColor: app.primaryColor,
authDocUrl: app.authDocUrl, authDocUrl: app.authDocUrl,
supportsConnections: app.supportsConnections, supportsConnections: app.supportsConnections,
primaryColor: app.primaryColor,
}; };
if (app.connectionCount) {
appData.connectionCount = app.connectionCount;
}
if (app.flowCount) {
appData.flowCount = app.flowCount;
}
return appData;
}; };
export default appSerializer; export default appSerializer;

View File

@@ -6,6 +6,8 @@ const flowSerializer = (flow) => {
name: flow.name, name: flow.name,
active: flow.active, active: flow.active,
status: flow.status, status: flow.status,
createdAt: flow.createdAt.getTime(),
updatedAt: flow.updatedAt.getTime(),
}; };
if (flow.steps?.length > 0) { if (flow.steps?.length > 0) {

View File

@@ -27,6 +27,8 @@ describe('flowSerializer', () => {
name: flow.name, name: flow.name,
active: flow.active, active: flow.active,
status: flow.status, status: flow.status,
createdAt: flow.createdAt.getTime(),
updatedAt: flow.updatedAt.getTime(),
}; };
expect(flowSerializer(flow)).toEqual(expectedPayload); expect(flowSerializer(flow)).toEqual(expectedPayload);

View File

@@ -19,6 +19,8 @@ export const createStep = async (params = {}) => {
params.appKey = params.appKey =
params?.appKey || (params.type === 'action' ? 'deepl' : 'webhook'); params?.appKey || (params.type === 'action' ? 'deepl' : 'webhook');
params.parameters = params?.parameters || {};
const step = await Step.query().insertAndFetch(params); const step = await Step.query().insertAndFetch(params);
return step; return step;

View File

@@ -0,0 +1,25 @@
const getConnectionsMock = (connections) => {
return {
data: connections.map((connection) => ({
id: connection.id,
key: connection.key,
reconnectable: connection.reconnectable,
verified: connection.verified,
appAuthClientId: connection.appAuthClientId,
formattedData: {
screenName: connection.formattedData.screenName,
},
createdAt: connection.createdAt.getTime(),
updatedAt: connection.updatedAt.getTime(),
})),
meta: {
count: connections.length,
currentPage: null,
isArray: true,
totalPages: null,
type: 'Connection',
},
};
};
export default getConnectionsMock;

View File

@@ -9,6 +9,8 @@ const getExecutionMock = async (execution, flow, steps) => {
name: flow.name, name: flow.name,
active: flow.active, active: flow.active,
status: flow.active ? 'published' : 'draft', status: flow.active ? 'published' : 'draft',
createdAt: flow.createdAt.getTime(),
updatedAt: flow.updatedAt.getTime(),
steps: steps.map((step) => ({ steps: steps.map((step) => ({
id: step.id, id: step.id,
type: step.type, type: step.type,

View File

@@ -10,6 +10,8 @@ const getExecutionsMock = async (executions, flow, steps) => {
name: flow.name, name: flow.name,
active: flow.active, active: flow.active,
status: flow.active ? 'published' : 'draft', status: flow.active ? 'published' : 'draft',
createdAt: flow.createdAt.getTime(),
updatedAt: flow.updatedAt.getTime(),
steps: steps.map((step) => ({ steps: steps.map((step) => ({
id: step.id, id: step.id,
type: step.type, type: step.type,

View File

@@ -4,6 +4,8 @@ const getFlowMock = async (flow, steps) => {
id: flow.id, id: flow.id,
name: flow.name, name: flow.name,
status: flow.active ? 'published' : 'draft', status: flow.active ? 'published' : 'draft',
createdAt: flow.createdAt.getTime(),
updatedAt: flow.updatedAt.getTime(),
steps: steps.map((step) => ({ steps: steps.map((step) => ({
appKey: step.appKey, appKey: step.appKey,
iconUrl: step.iconUrl, iconUrl: step.iconUrl,

View File

@@ -7,6 +7,8 @@ const getFlowsMock = async (flows, steps) => {
id: flow.id, id: flow.id,
name: flow.name, name: flow.name,
status: flow.active ? 'published' : 'draft', status: flow.active ? 'published' : 'draft',
createdAt: flow.createdAt.getTime(),
updatedAt: flow.updatedAt.getTime(),
steps: flowSteps.map((step) => ({ steps: flowSteps.map((step) => ({
appKey: step.appKey, appKey: step.appKey,
iconUrl: step.iconUrl, iconUrl: step.iconUrl,

View File

@@ -0,0 +1,55 @@
const getAppsMock = () => {
const appsData = [
{
authDocUrl: 'https://automatisch.io/docs/apps/deepl/connection',
connectionCount: 1,
flowCount: 1,
iconUrl: 'http://localhost:3000/apps/deepl/assets/favicon.svg',
key: 'deepl',
name: 'DeepL',
primaryColor: '0d2d45',
supportsConnections: true,
},
{
authDocUrl: 'https://automatisch.io/docs/apps/github/connection',
connectionCount: 1,
flowCount: 1,
iconUrl: 'http://localhost:3000/apps/github/assets/favicon.svg',
key: 'github',
name: 'GitHub',
primaryColor: '000000',
supportsConnections: true,
},
{
authDocUrl: 'https://automatisch.io/docs/apps/slack/connection',
flowCount: 1,
iconUrl: 'http://localhost:3000/apps/slack/assets/favicon.svg',
key: 'slack',
name: 'Slack',
primaryColor: '4a154b',
supportsConnections: true,
},
{
authDocUrl: 'https://automatisch.io/docs/apps/webhook/connection',
flowCount: 1,
iconUrl: 'http://localhost:3000/apps/webhook/assets/favicon.svg',
key: 'webhook',
name: 'Webhook',
primaryColor: '0059F7',
supportsConnections: false,
},
];
return {
data: appsData,
meta: {
count: appsData.length,
currentPage: null,
isArray: true,
totalPages: null,
type: 'Object',
},
};
};
export default getAppsMock;

View File

@@ -1,6 +1,7 @@
import { Model } from 'objection'; import { Model } from 'objection';
import { client as knex } from '../../src/config/database.js'; import { client as knex } from '../../src/config/database.js';
import logger from '../../src/helpers/logger.js'; import logger from '../../src/helpers/logger.js';
import { vi } from 'vitest';
global.beforeAll(async () => { global.beforeAll(async () => {
global.knex = null; global.knex = null;
@@ -22,8 +23,8 @@ global.afterEach(async () => {
await global.knex.rollback(); await global.knex.rollback();
Model.knex(knex); Model.knex(knex);
// jest.restoreAllMocks(); vi.restoreAllMocks();
// jest.clearAllMocks(); vi.clearAllMocks();
}); });
global.afterAll(async () => { global.afterAll(async () => {

View File

@@ -345,6 +345,12 @@ export default defineConfig({
{ text: 'Connection', link: '/apps/scheduler/connection' }, { text: 'Connection', link: '/apps/scheduler/connection' },
], ],
}, },
{
text: 'SFTP',
collapsible: true,
collapsed: true,
items: [{ text: 'Connection', link: '/apps/sftp/connection' }],
},
{ {
text: 'SignalWire', text: 'SignalWire',
collapsible: true, collapsible: true,

View File

@@ -0,0 +1,15 @@
# SFTP
:::info
This page explains the steps you need to follow to set up the SFTP connection in Automatisch. If any of the steps are outdated, please let us know!
:::
SSH File Transfer Protocol (also known as Secure File Transfer Protocol or SFTP) is a network protocol that provides file access, file transfer, and file management over any reliable data stream. You need to provide the following information from Automatisch by using SFTP connection.
1. Write any screen name to be displayed in Automatisch.
2. Fill host address field with the SFTP host address.
3. Fill username field with the SFTP username.
4. Fill password field with the SFTP password.
5. Fill port field with the SFTP port. Default is 21.
6. Click **Submit** button on Automatisch.
7. Now, you can start using the SFTP connection with Automatisch.

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 122.88 103.95"><title>sftp</title><path d="M21.56,47.27l6.21-.39a4.56,4.56,0,0,0,.82,2.29,3.45,3.45,0,0,0,2.88,1.28,3.16,3.16,0,0,0,2.15-.65,2,2,0,0,0,.76-1.52,2,2,0,0,0-.71-1.48,8.45,8.45,0,0,0-3.34-1.22A14.06,14.06,0,0,1,24.21,43a5.14,5.14,0,0,1-1.85-4.07,5.44,5.44,0,0,1,.95-3.07,6.19,6.19,0,0,1,2.84-2.28,13.26,13.26,0,0,1,5.19-.83,10.58,10.58,0,0,1,6.18,1.51,6.53,6.53,0,0,1,2.53,4.8l-6.14.37a3.38,3.38,0,0,0-1-2.1,3.34,3.34,0,0,0-2.17-.65,2.63,2.63,0,0,0-1.72.48,1.49,1.49,0,0,0-.58,1.18,1.19,1.19,0,0,0,.47.9,5.23,5.23,0,0,0,2.18.78,27.38,27.38,0,0,1,6.13,1.87,6.38,6.38,0,0,1,2.7,2.35,5.94,5.94,0,0,1,.84,3.13,6.73,6.73,0,0,1-1.12,3.75,7.1,7.1,0,0,1-3.15,2.6,12.64,12.64,0,0,1-5.08.89c-3.6,0-6.09-.7-7.47-2.08a8.18,8.18,0,0,1-2.35-5.27ZM9.07,15.63h3.09V8.2A1.87,1.87,0,0,1,14,6.33h3.32V1.87A1.88,1.88,0,0,1,19.23,0H49.57a1.87,1.87,0,0,1,1.87,1.87V6.33h59.5a1.87,1.87,0,0,1,1.87,1.87v7.43h3.41a1.87,1.87,0,0,1,1.87,1.87,1.75,1.75,0,0,1,0,.4L111.51,66a7.12,7.12,0,0,1-2,4.07,5.62,5.62,0,0,1-4,1.69H69.25v5.57h.17A2.67,2.67,0,0,1,72.08,80v4.81h13a2.67,2.67,0,0,1,2.54,1.87h27.84a7,7,0,0,1,6.85,4.48,8.05,8.05,0,0,1,.57,3,8.14,8.14,0,0,1-.57,3,7,7,0,0,1-6.85,4.47H87.73A2.67,2.67,0,0,1,85.08,104H39.92a2.67,2.67,0,0,1-2.65-2.37H7.42A7,7,0,0,1,.57,97.11a8.14,8.14,0,0,1-.57-3,8.05,8.05,0,0,1,.57-3,7,7,0,0,1,6.85-4.48h30a2.67,2.67,0,0,1,2.54-1.87H50.68V80a2.67,2.67,0,0,1,2.66-2.66h1V71.75H18A5.69,5.69,0,0,1,13.89,70,6.84,6.84,0,0,1,12,65.92L6.92,17.69a1.83,1.83,0,0,1,.37-1.33,1.86,1.86,0,0,1,1.29-.71,4,4,0,0,1,.49,0ZM57.86,77.32h7.86v-5.5H57.86v5.5ZM87.74,90.19v7.86h27.72A3.63,3.63,0,0,0,119,95.8a4.68,4.68,0,0,0,.31-1.68,4.63,4.63,0,0,0-.31-1.67,3.63,3.63,0,0,0-3.58-2.26ZM37.26,98.05V90.19H7.42a3.63,3.63,0,0,0-3.58,2.26,4.63,4.63,0,0,0-.31,1.67,4.73,4.73,0,0,0,.31,1.68,3.63,3.63,0,0,0,3.58,2.25ZM10.85,19.37l4.84,46.17a3.14,3.14,0,0,0,.83,1.86A2.07,2.07,0,0,0,18,68h87.54a2,2,0,0,0,1.41-.6,3.28,3.28,0,0,0,.88-1.92l6.28-46.12Zm5.05-9.3v5.55l93.17-.79V10.07H49.57A1.87,1.87,0,0,1,47.69,8.2V3.74H21.1V8.2a1.87,1.87,0,0,1-1.87,1.87Zm28.3,23H60.35v4.54H50.76v3.7H59v4.28H50.76v8.62H44.2V33.12Zm18.35,0H82.42v5.22H75.75V54.26H69.22V38.34H62.55V33.12Zm22.93,0H96.35a7.48,7.48,0,0,1,5.32,1.68,6.36,6.36,0,0,1,1.77,4.82,6.55,6.55,0,0,1-1.93,5q-1.94,1.82-5.89,1.81H92v7.82H85.48V33.12Zm6.56,9h1.61a4.14,4.14,0,0,0,2.67-.66,2.13,2.13,0,0,0,.77-1.68,2.37,2.37,0,0,0-.67-1.69,3.49,3.49,0,0,0-2.51-.69H92v4.72Z"/></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -15,7 +15,7 @@ function AccountDropdownMenu(props) {
const navigate = useNavigate(); const navigate = useNavigate();
const { open, onClose, anchorEl, id } = props; const { open, onClose, anchorEl, id } = props;
const logout = async () => { const logout = async () => {
authentication.updateToken(''); authentication.removeToken();
await apolloClient.clearStore(); await apolloClient.clearStore();
onClose(); onClose();
navigate(URLS.LOGIN); navigate(URLS.LOGIN);

View File

@@ -17,6 +17,7 @@ import useFormatMessage from 'hooks/useFormatMessage';
import { generateExternalLink } from 'helpers/translationValues'; import { generateExternalLink } from 'helpers/translationValues';
import { Form } from './style'; import { Form } from './style';
import useAppAuth from 'hooks/useAppAuth'; import useAppAuth from 'hooks/useAppAuth';
import { useQueryClient } from '@tanstack/react-query';
function AddAppConnection(props) { function AddAppConnection(props) {
const { application, connectionId, onClose } = props; const { application, connectionId, onClose } = props;
@@ -36,6 +37,7 @@ function AddAppConnection(props) {
appAuthClientId, appAuthClientId,
useShared: !!appAuthClientId, useShared: !!appAuthClientId,
}); });
const queryClient = useQueryClient();
React.useEffect(function relayProviderData() { React.useEffect(function relayProviderData() {
if (window.opener) { if (window.opener) {
@@ -78,6 +80,10 @@ function AddAppConnection(props) {
const response = await authenticate({ const response = await authenticate({
fields: data, fields: data,
}); });
await queryClient.invalidateQueries({
queryKey: ['apps', key, 'connections'],
});
onClose(response); onClose(response);
} catch (err) { } catch (err) {
const error = err; const error = err;

View File

@@ -10,16 +10,18 @@ import Chip from '@mui/material/Chip';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
import * as URLS from 'config/urls'; import * as URLS from 'config/urls';
import useFormatMessage from 'hooks/useFormatMessage'; import useFormatMessage from 'hooks/useFormatMessage';
import useAppAuthClients from 'hooks/useAppAuthClients.ee'; import useAdminAppAuthClients from 'hooks/useAdminAppAuthClients';
import NoResultFound from 'components/NoResultFound'; import NoResultFound from 'components/NoResultFound';
function AdminApplicationAuthClients(props) { function AdminApplicationAuthClients(props) {
const { appKey } = props; const { appKey } = props;
const formatMessage = useFormatMessage(); const formatMessage = useFormatMessage();
const { appAuthClients, loading } = useAppAuthClients({ appKey }); const { data: appAuthClients, isLoading } = useAdminAppAuthClients(appKey);
if (loading)
if (isLoading)
return <CircularProgress sx={{ display: 'block', margin: '20px auto' }} />; return <CircularProgress sx={{ display: 'block', margin: '20px auto' }} />;
if (!appAuthClients?.length) {
if (!appAuthClients?.data.length) {
return ( return (
<NoResultFound <NoResultFound
to={URLS.ADMIN_APP_AUTH_CLIENTS_CREATE(appKey)} to={URLS.ADMIN_APP_AUTH_CLIENTS_CREATE(appKey)}
@@ -27,7 +29,8 @@ function AdminApplicationAuthClients(props) {
/> />
); );
} }
const sortedAuthClients = appAuthClients.slice().sort((a, b) => {
const sortedAuthClients = appAuthClients.data.slice().sort((a, b) => {
if (a.id < b.id) { if (a.id < b.id) {
return -1; return -1;
} }
@@ -36,6 +39,7 @@ function AdminApplicationAuthClients(props) {
} }
return 0; return 0;
}); });
return ( return (
<div> <div>
{sortedAuthClients.map((client) => ( {sortedAuthClients.map((client) => (

View File

@@ -6,29 +6,33 @@ import ListItem from '@mui/material/ListItem';
import ListItemButton from '@mui/material/ListItemButton'; import ListItemButton from '@mui/material/ListItemButton';
import ListItemText from '@mui/material/ListItemText'; import ListItemText from '@mui/material/ListItemText';
import * as React from 'react'; import * as React from 'react';
import useAppAuthClients from 'hooks/useAppAuthClients.ee'; import useAppAuthClients from 'hooks/useAppAuthClients';
import useFormatMessage from 'hooks/useFormatMessage'; import useFormatMessage from 'hooks/useFormatMessage';
function AppAuthClientsDialog(props) { function AppAuthClientsDialog(props) {
const { appKey, onClientClick, onClose } = props; const { appKey, onClientClick, onClose } = props;
const { appAuthClients } = useAppAuthClients({ appKey, active: true }); const { data: appAuthClients } = useAppAuthClients(appKey);
const formatMessage = useFormatMessage(); const formatMessage = useFormatMessage();
React.useEffect( React.useEffect(
function autoAuthenticateSingleClient() { function autoAuthenticateSingleClient() {
if (appAuthClients?.length === 1) { if (appAuthClients?.data.length === 1) {
onClientClick(appAuthClients[0].id); onClientClick(appAuthClients.data[0].id);
} }
}, },
[appAuthClients], [appAuthClients?.data],
); );
if (!appAuthClients?.length || appAuthClients?.length === 1)
if (!appAuthClients?.data.length || appAuthClients?.data.length === 1)
return <React.Fragment />; return <React.Fragment />;
return ( return (
<Dialog onClose={onClose} open={true}> <Dialog onClose={onClose} open={true}>
<DialogTitle>{formatMessage('appAuthClientsDialog.title')}</DialogTitle> <DialogTitle>{formatMessage('appAuthClientsDialog.title')}</DialogTitle>
<List sx={{ pt: 0 }}> <List sx={{ pt: 0 }}>
{appAuthClients.map((appAuthClient) => ( {appAuthClients.data.map((appAuthClient) => (
<ListItem disableGutters key={appAuthClient.id}> <ListItem disableGutters key={appAuthClient.id}>
<ListItemButton onClick={() => onClientClick(appAuthClient.id)}> <ListItemButton onClick={() => onClientClick(appAuthClient.id)}>
<ListItemText primary={appAuthClient.name} /> <ListItemText primary={appAuthClient.name} />

View File

@@ -7,6 +7,7 @@ import MenuItem from '@mui/material/MenuItem';
import * as URLS from 'config/urls'; 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';
function ContextMenu(props) { function ContextMenu(props) {
const { const {
@@ -18,15 +19,24 @@ function ContextMenu(props) {
disableReconnection, disableReconnection,
} = props; } = props;
const formatMessage = useFormatMessage(); const formatMessage = useFormatMessage();
const queryClient = useQueryClient();
const createActionHandler = React.useCallback( const createActionHandler = React.useCallback(
(action) => { (action) => {
return function clickHandler(event) { return async function clickHandler(event) {
onMenuItemClick(event, action); onMenuItemClick(event, action);
if (['test', 'reconnect', 'delete'].includes(action.type)) {
await queryClient.invalidateQueries({
queryKey: ['apps', appKey, 'connections'],
});
}
onClose(); onClose();
}; };
}, },
[onMenuItemClick, onClose], [onMenuItemClick, onClose, queryClient],
); );
return ( return (
<Menu <Menu
open={true} open={true}

View File

@@ -1,21 +1,24 @@
import { useLazyQuery, useMutation } from '@apollo/client'; import * as React from 'react';
import { useMutation } from '@apollo/client';
import CheckCircleIcon from '@mui/icons-material/CheckCircle'; import CheckCircleIcon from '@mui/icons-material/CheckCircle';
import ErrorIcon from '@mui/icons-material/Error'; import ErrorIcon from '@mui/icons-material/Error';
import Skeleton from '@mui/material/Skeleton';
import MoreHorizIcon from '@mui/icons-material/MoreHoriz'; import MoreHorizIcon from '@mui/icons-material/MoreHoriz';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import Card from '@mui/material/Card'; import Card from '@mui/material/Card';
import CardActionArea from '@mui/material/CardActionArea'; import CardActionArea from '@mui/material/CardActionArea';
import CircularProgress from '@mui/material/CircularProgress'; import CircularProgress from '@mui/material/CircularProgress';
import Stack from '@mui/material/Stack'; import Stack from '@mui/material/Stack';
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import * as React from 'react';
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
import ConnectionContextMenu from 'components/AppConnectionContextMenu'; import ConnectionContextMenu from 'components/AppConnectionContextMenu';
import { DELETE_CONNECTION } from 'graphql/mutations/delete-connection'; import { DELETE_CONNECTION } from 'graphql/mutations/delete-connection';
import { TEST_CONNECTION } from 'graphql/queries/test-connection';
import useFormatMessage from 'hooks/useFormatMessage'; import useFormatMessage from 'hooks/useFormatMessage';
import { ConnectionPropType } from 'propTypes/propTypes'; import { ConnectionPropType } from 'propTypes/propTypes';
import { CardContent, Typography } from './style'; import { CardContent, Typography } from './style';
import useConnectionFlows from 'hooks/useConnectionFlows';
import useTestConnection from 'hooks/useTestConnection';
const countTranslation = (value) => ( const countTranslation = (value) => (
<> <>
@@ -23,36 +26,39 @@ const countTranslation = (value) => (
<br /> <br />
</> </>
); );
function AppConnectionRow(props) { function AppConnectionRow(props) {
const enqueueSnackbar = useEnqueueSnackbar();
const [verificationVisible, setVerificationVisible] = React.useState(false);
const [testConnection, { called: testCalled, loading: testLoading }] =
useLazyQuery(TEST_CONNECTION, {
fetchPolicy: 'network-only',
onCompleted: () => {
setTimeout(() => setVerificationVisible(false), 3000);
},
onError: () => {
setTimeout(() => setVerificationVisible(false), 3000);
},
});
const [deleteConnection] = useMutation(DELETE_CONNECTION);
const formatMessage = useFormatMessage(); const formatMessage = useFormatMessage();
const { const enqueueSnackbar = useEnqueueSnackbar();
id, const { id, key, formattedData, verified, createdAt, reconnectable } =
key, props.connection;
formattedData, const [verificationVisible, setVerificationVisible] = React.useState(false);
verified,
createdAt,
flowCount,
reconnectable,
} = props.connection;
const contextButtonRef = React.useRef(null); const contextButtonRef = React.useRef(null);
const [anchorEl, setAnchorEl] = React.useState(null); const [anchorEl, setAnchorEl] = React.useState(null);
const [deleteConnection] = useMutation(DELETE_CONNECTION);
const { mutate: testConnection, isPending: isTestConnectionPending } =
useTestConnection(
{ connectionId: id },
{
onSettled: () => {
setTimeout(() => setVerificationVisible(false), 3000);
},
},
);
const handleClose = () => { const handleClose = () => {
setAnchorEl(null); setAnchorEl(null);
}; };
const { data, isLoading: isConnectionFlowsLoading } = useConnectionFlows({
connectionId: id,
});
const flowCount = data?.meta?.count;
const onContextMenuClick = () => setAnchorEl(contextButtonRef.current); const onContextMenuClick = () => setAnchorEl(contextButtonRef.current);
const onContextMenuAction = React.useCallback( const onContextMenuAction = React.useCallback(
async (event, action) => { async (event, action) => {
if (action.type === 'delete') { if (action.type === 'delete') {
@@ -68,6 +74,7 @@ function AppConnectionRow(props) {
}); });
}, },
}); });
enqueueSnackbar(formatMessage('connection.deletedMessage'), { enqueueSnackbar(formatMessage('connection.deletedMessage'), {
variant: 'success', variant: 'success',
SnackbarProps: { SnackbarProps: {
@@ -81,9 +88,11 @@ function AppConnectionRow(props) {
}, },
[deleteConnection, id, testConnection, formatMessage, enqueueSnackbar], [deleteConnection, id, testConnection, formatMessage, enqueueSnackbar],
); );
const relativeCreatedAt = DateTime.fromMillis( const relativeCreatedAt = DateTime.fromMillis(
parseInt(createdAt, 10), parseInt(createdAt, 10),
).toRelative(); ).toRelative();
return ( return (
<> <>
<Card sx={{ my: 2 }} data-test="app-connection-row"> <Card sx={{ my: 2 }} data-test="app-connection-row">
@@ -103,7 +112,7 @@ function AppConnectionRow(props) {
<Box> <Box>
<Stack direction="row" alignItems="center" spacing={1}> <Stack direction="row" alignItems="center" spacing={1}>
{verificationVisible && testCalled && testLoading && ( {verificationVisible && isTestConnectionPending && (
<> <>
<CircularProgress size={16} /> <CircularProgress size={16} />
<Typography variant="caption"> <Typography variant="caption">
@@ -112,8 +121,7 @@ function AppConnectionRow(props) {
</> </>
)} )}
{verificationVisible && {verificationVisible &&
testCalled && !isTestConnectionPending &&
!testLoading &&
verified && ( verified && (
<> <>
<CheckCircleIcon fontSize="small" color="success" /> <CheckCircleIcon fontSize="small" color="success" />
@@ -123,8 +131,7 @@ function AppConnectionRow(props) {
</> </>
)} )}
{verificationVisible && {verificationVisible &&
testCalled && !isTestConnectionPending &&
!testLoading &&
!verified && ( !verified && (
<> <>
<ErrorIcon fontSize="small" color="error" /> <ErrorIcon fontSize="small" color="error" />
@@ -143,7 +150,13 @@ function AppConnectionRow(props) {
sx={{ display: ['none', 'inline-block'] }} sx={{ display: ['none', 'inline-block'] }}
> >
{formatMessage('connection.flowCount', { {formatMessage('connection.flowCount', {
count: countTranslation(flowCount), count: countTranslation(
isConnectionFlowsLoading ? (
<Skeleton variant="text" width={15} />
) : (
flowCount
),
),
})} })}
</Typography> </Typography>
</Box> </Box>

View File

@@ -1,20 +1,19 @@
import * as React from 'react'; import * as React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useQuery } from '@apollo/client';
import { GET_APP_CONNECTIONS } from 'graphql/queries/get-app-connections';
import AppConnectionRow from 'components/AppConnectionRow'; import AppConnectionRow from 'components/AppConnectionRow';
import NoResultFound from 'components/NoResultFound'; import NoResultFound from 'components/NoResultFound';
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';
function AppConnections(props) { function AppConnections(props) {
const { appKey } = props; const { appKey } = props;
const formatMessage = useFormatMessage(); const formatMessage = useFormatMessage();
const { data } = useQuery(GET_APP_CONNECTIONS, { const { data } = useAppConnections(appKey);
variables: { key: appKey }, const appConnections = data?.data || [];
});
const appConnections = data?.getApp?.connections || [];
const hasConnections = appConnections?.length; const hasConnections = appConnections?.length;
if (!hasConnections) { if (!hasConnections) {
return ( return (
<NoResultFound <NoResultFound

View File

@@ -47,7 +47,7 @@ function AppFlows(props) {
return ( return (
<> <>
{flows?.map((appFlow) => ( {flows?.map((appFlow) => (
<AppFlowRow key={appFlow.id} flow={appFlow} /> <AppFlowRow key={appFlow.id} flow={appFlow} appKey={appKey} />
))} ))}
{pageInfo && pageInfo.totalPages > 1 && ( {pageInfo && pageInfo.totalPages > 1 && (

View File

@@ -2,6 +2,7 @@ import * as React from 'react';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import Alert from '@mui/material/Alert'; import Alert from '@mui/material/Alert';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import useFormatMessage from 'hooks/useFormatMessage'; import useFormatMessage from 'hooks/useFormatMessage';
export default function CheckoutCompletedAlert() { export default function CheckoutCompletedAlert() {
@@ -9,7 +10,9 @@ export default function CheckoutCompletedAlert() {
const location = useLocation(); const location = useLocation();
const state = location.state; const state = location.state;
const checkoutCompleted = state?.checkoutCompleted; const checkoutCompleted = state?.checkoutCompleted;
if (!checkoutCompleted) return <React.Fragment />; if (!checkoutCompleted) return <React.Fragment />;
return ( return (
<Alert <Alert
severity="success" severity="success"

View File

@@ -1,5 +1,4 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useLazyQuery, useQuery } from '@apollo/client';
import Autocomplete from '@mui/material/Autocomplete'; import Autocomplete from '@mui/material/Autocomplete';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
import Collapse from '@mui/material/Collapse'; import Collapse from '@mui/material/Collapse';
@@ -12,8 +11,6 @@ import AppAuthClientsDialog from 'components/AppAuthClientsDialog/index.ee';
import FlowSubstepTitle from 'components/FlowSubstepTitle'; import FlowSubstepTitle from 'components/FlowSubstepTitle';
import useAppConfig from 'hooks/useAppConfig.ee'; import useAppConfig from 'hooks/useAppConfig.ee';
import { EditorContext } from 'contexts/Editor'; import { EditorContext } from 'contexts/Editor';
import { GET_APP_CONNECTIONS } from 'graphql/queries/get-app-connections';
import { TEST_CONNECTION } from 'graphql/queries/test-connection';
import useAuthenticateApp from 'hooks/useAuthenticateApp.ee'; import useAuthenticateApp from 'hooks/useAuthenticateApp.ee';
import useFormatMessage from 'hooks/useFormatMessage'; import useFormatMessage from 'hooks/useFormatMessage';
import { import {
@@ -21,6 +18,10 @@ import {
StepPropType, StepPropType,
SubstepPropType, SubstepPropType,
} from 'propTypes/propTypes'; } from 'propTypes/propTypes';
import useStepConnection from 'hooks/useStepConnection';
import { useQueryClient } from '@tanstack/react-query';
import useAppConnections from 'hooks/useAppConnections';
import useTestConnection from 'hooks/useTestConnection';
const ADD_CONNECTION_VALUE = 'ADD_CONNECTION'; const ADD_CONNECTION_VALUE = 'ADD_CONNECTION';
const ADD_SHARED_CONNECTION_VALUE = 'ADD_SHARED_CONNECTION'; const ADD_SHARED_CONNECTION_VALUE = 'ADD_SHARED_CONNECTION';
@@ -44,40 +45,42 @@ function ChooseConnectionSubstep(props) {
onChange, onChange,
application, application,
} = props; } = props;
const { connection, appKey } = step; const { appKey } = step;
const formatMessage = useFormatMessage(); const formatMessage = useFormatMessage();
const editorContext = React.useContext(EditorContext); const editorContext = React.useContext(EditorContext);
const [showAddConnectionDialog, setShowAddConnectionDialog] = const [showAddConnectionDialog, setShowAddConnectionDialog] =
React.useState(false); React.useState(false);
const [showAddSharedConnectionDialog, setShowAddSharedConnectionDialog] = const [showAddSharedConnectionDialog, setShowAddSharedConnectionDialog] =
React.useState(false); React.useState(false);
const queryClient = useQueryClient();
const { authenticate } = useAuthenticateApp({ const { authenticate } = useAuthenticateApp({
appKey: application.key, appKey: application.key,
useShared: true, useShared: true,
}); });
const { data, loading, refetch } = useQuery(GET_APP_CONNECTIONS, { const {
variables: { key: appKey }, data,
}); isLoading: isAppConnectionsLoading,
refetch,
} = useAppConnections(appKey);
const { data: appConfig } = useAppConfig(application.key); const { data: appConfig } = useAppConfig(application.key);
const { data: stepConnectionData } = useStepConnection(step.id);
const stepConnection = stepConnectionData?.data;
// TODO: show detailed error when connection test/verification fails // TODO: show detailed error when connection test/verification fails
const [ const { mutate: testConnection, isPending: isTestConnectionPending } =
testConnection, useTestConnection({
{ loading: testResultLoading, refetch: retestConnection }, connectionId: stepConnection?.id,
] = useLazyQuery(TEST_CONNECTION, { });
variables: {
id: connection?.id,
},
});
React.useEffect(() => { React.useEffect(() => {
if (connection?.id) { if (stepConnection?.id) {
testConnection({ testConnection({
variables: { variables: {
id: connection.id, id: stepConnection.id,
}, },
}); });
} }
@@ -85,11 +88,10 @@ function ChooseConnectionSubstep(props) {
}, []); }, []);
const connectionOptions = React.useMemo(() => { const connectionOptions = React.useMemo(() => {
const appWithConnections = data?.getApp; const appWithConnections = data?.data;
const options = const options =
appWithConnections?.connections?.map((connection) => appWithConnections?.map((connection) => optionGenerator(connection)) ||
optionGenerator(connection), [];
) || [];
if (!appConfig?.data || appConfig?.data?.canCustomConnect) { if (!appConfig?.data || appConfig?.data?.canCustomConnect) {
options.push({ options.push({
@@ -154,8 +156,9 @@ function ChooseConnectionSubstep(props) {
}, },
[onChange, refetch, step], [onChange, refetch, step],
); );
const handleChange = React.useCallback( const handleChange = React.useCallback(
(event, selectedOption) => { async (event, selectedOption) => {
if (typeof selectedOption === 'object') { if (typeof selectedOption === 'object') {
// TODO: try to simplify type casting below. // TODO: try to simplify type casting below.
const typedSelectedOption = selectedOption; const typedSelectedOption = selectedOption;
@@ -172,7 +175,7 @@ function ChooseConnectionSubstep(props) {
return; return;
} }
if (connectionId !== step.connection?.id) { if (connectionId !== stepConnection?.id) {
onChange({ onChange({
step: { step: {
...step, ...step,
@@ -181,19 +184,23 @@ function ChooseConnectionSubstep(props) {
}, },
}, },
}); });
await queryClient.invalidateQueries({
queryKey: ['steps', step.id, 'connection'],
});
} }
} }
}, },
[step, onChange], [step, onChange, queryClient],
); );
React.useEffect(() => { React.useEffect(() => {
if (step.connection?.id) { if (stepConnection?.id) {
retestConnection({ testConnection({
id: step.connection.id, id: stepConnection?.id,
}); });
} }
}, [step.connection?.id, retestConnection]); }, [stepConnection?.id, testConnection]);
const onToggle = expanded ? onCollapse : onExpand; const onToggle = expanded ? onCollapse : onExpand;
@@ -203,7 +210,7 @@ function ChooseConnectionSubstep(props) {
expanded={expanded} expanded={expanded}
onClick={onToggle} onClick={onToggle}
title={name} title={name}
valid={testResultLoading ? null : connection?.verified} valid={isTestConnectionPending ? null : stepConnection?.verified}
/> />
<Collapse in={expanded} timeout="auto" unmountOnExit> <Collapse in={expanded} timeout="auto" unmountOnExit>
<ListItem <ListItem
@@ -229,9 +236,9 @@ function ChooseConnectionSubstep(props) {
required required
/> />
)} )}
value={getOption(connectionOptions, connection?.id)} value={getOption(connectionOptions, stepConnection?.id)}
onChange={handleChange} onChange={handleChange}
loading={loading} loading={isAppConnectionsLoading}
data-test="choose-connection-autocomplete" data-test="choose-connection-autocomplete"
/> />
@@ -241,8 +248,8 @@ function ChooseConnectionSubstep(props) {
onClick={onSubmit} onClick={onSubmit}
sx={{ mt: 2 }} sx={{ mt: 2 }}
disabled={ disabled={
testResultLoading || isTestConnectionPending ||
!connection?.verified || !stepConnection?.verified ||
editorContext.readOnly editorContext.readOnly
} }
data-test="flow-substep-continue-button" data-test="flow-substep-continue-button"

View File

@@ -61,31 +61,38 @@ 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 renderElement = React.useCallback( const renderElement = React.useCallback(
(props) => <Element {...props} disabled={disabled} />, (props) => <Element {...props} disabled={disabled} />,
[disabled], [disabled],
); );
const [editor] = React.useState(() => customizeEditor(createEditor())); const [editor] = React.useState(() => customizeEditor(createEditor()));
const [showVariableSuggestions, setShowVariableSuggestions] = const [showVariableSuggestions, setShowVariableSuggestions] =
React.useState(false); React.useState(false);
let dependsOnValues = []; let dependsOnValues = [];
if (dependsOn?.length) { if (dependsOn?.length) {
dependsOnValues = watch(dependsOn); dependsOnValues = watch(dependsOn);
} }
React.useEffect(() => { React.useEffect(() => {
const ref = ReactEditor.toDOMNode(editor, editor); const ref = ReactEditor.toDOMNode(editor, editor);
resizeObserver.observe(ref); resizeObserver.observe(ref);
return () => resizeObserver.unobserve(ref); return () => resizeObserver.unobserve(ref);
}, []); }, []);
const promoteValue = () => { const promoteValue = () => {
const serializedValue = serialize(editor.children); const serializedValue = serialize(editor.children);
controllerOnChange(serializedValue); controllerOnChange(serializedValue);
}; };
const resizeObserver = React.useMemo(function syncCustomOptionsPosition() { const resizeObserver = React.useMemo(function syncCustomOptionsPosition() {
return new ResizeObserver(() => { return new ResizeObserver(() => {
forceUpdate(); forceUpdate();
}); });
}, []); }, []);
React.useEffect(() => { React.useEffect(() => {
const hasDependencies = dependsOnValues.length; const hasDependencies = dependsOnValues.length;
if (hasDependencies) { if (hasDependencies) {
@@ -93,6 +100,7 @@ function ControlledCustomAutocomplete(props) {
resetEditor(editor); resetEditor(editor);
} }
}, dependsOnValues); }, dependsOnValues);
React.useEffect( React.useEffect(
function updateInitialValue() { function updateInitialValue() {
const hasOptions = options.length; const hasOptions = options.length;
@@ -110,16 +118,19 @@ function ControlledCustomAutocomplete(props) {
}, },
[isInitialValueSet, options, loading], [isInitialValueSet, options, loading],
); );
React.useEffect(() => { React.useEffect(() => {
if (!showVariableSuggestions && value !== serialize(editor.children)) { if (!showVariableSuggestions && value !== serialize(editor.children)) {
promoteValue(); promoteValue();
} }
}, [showVariableSuggestions]); }, [showVariableSuggestions]);
const hideSuggestionsOnShift = (event) => { const hideSuggestionsOnShift = (event) => {
if (event.code === 'Tab') { if (event.code === 'Tab') {
setShowVariableSuggestions(false); setShowVariableSuggestions(false);
} }
}; };
const handleKeyDown = (event) => { const handleKeyDown = (event) => {
hideSuggestionsOnShift(event); hideSuggestionsOnShift(event);
if (event.code === 'Tab') { if (event.code === 'Tab') {
@@ -129,15 +140,18 @@ function ControlledCustomAutocomplete(props) {
event.preventDefault(); event.preventDefault();
} }
}; };
const stepsWithVariables = React.useMemo(() => { const stepsWithVariables = React.useMemo(() => {
return processStepWithExecutions(priorStepsWithExecutions); return processStepWithExecutions(priorStepsWithExecutions);
}, [priorStepsWithExecutions]); }, [priorStepsWithExecutions]);
const handleVariableSuggestionClick = React.useCallback( const handleVariableSuggestionClick = React.useCallback(
(variable) => { (variable) => {
insertVariable(editor, variable, stepsWithVariables); insertVariable(editor, variable, stepsWithVariables);
}, },
[stepsWithVariables], [stepsWithVariables],
); );
const handleOptionClick = React.useCallback( const handleOptionClick = React.useCallback(
(event, option) => { (event, option) => {
event.stopPropagation(); event.stopPropagation();
@@ -147,17 +161,20 @@ function ControlledCustomAutocomplete(props) {
}, },
[stepsWithVariables], [stepsWithVariables],
); );
const handleClearButtonClick = (event) => { const handleClearButtonClick = (event) => {
event.stopPropagation(); event.stopPropagation();
resetEditor(editor); resetEditor(editor);
promoteValue(); promoteValue();
setSingleChoice(undefined); setSingleChoice(undefined);
}; };
const reset = (tabIndex) => { const reset = (tabIndex) => {
const isOptions = tabIndex === 0; const isOptions = tabIndex === 0;
setSingleChoice(isOptions); setSingleChoice(isOptions);
resetEditor(editor, { focus: true }); resetEditor(editor, { focus: true });
}; };
return ( return (
<Slate <Slate
editor={editor} editor={editor}

View File

@@ -22,7 +22,7 @@ function DeleteAccountDialog(props) {
const handleConfirm = React.useCallback(async () => { const handleConfirm = React.useCallback(async () => {
await deleteCurrentUser(); await deleteCurrentUser();
authentication.updateToken(''); authentication.removeToken();
await apolloClient.clearStore(); await apolloClient.clearStore();
navigate(URLS.LOGIN); navigate(URLS.LOGIN);
}, [deleteCurrentUser, currentUser]); }, [deleteCurrentUser, currentUser]);

View File

@@ -25,7 +25,7 @@ function DeleteRoleButton(props) {
const handleConfirm = React.useCallback(async () => { const handleConfirm = React.useCallback(async () => {
try { try {
await deleteRole(); await deleteRole();
queryClient.invalidateQueries({ queryKey: ['roles'] }); queryClient.invalidateQueries({ queryKey: ['admin', 'roles'] });
setShowConfirmation(false); setShowConfirmation(false);
enqueueSnackbar(formatMessage('deleteRoleButton.successfullyDeleted'), { enqueueSnackbar(formatMessage('deleteRoleButton.successfullyDeleted'), {
variant: 'success', variant: 'success',

View File

@@ -25,7 +25,6 @@ function DeleteUserButton(props) {
try { try {
await deleteUser(); await deleteUser();
queryClient.invalidateQueries({ queryKey: ['admin', 'users'] }); queryClient.invalidateQueries({ queryKey: ['admin', 'users'] });
queryClient.invalidateQueries({ queryKey: ['admin', 'user', userId] });
setShowConfirmation(false); setShowConfirmation(false);
enqueueSnackbar(formatMessage('deleteUserButton.successfullyDeleted'), { enqueueSnackbar(formatMessage('deleteUserButton.successfullyDeleted'), {
variant: 'success', variant: 'success',

View File

@@ -3,47 +3,24 @@ import { useMutation } from '@apollo/client';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
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 { GET_FLOW } from 'graphql/queries/get-flow';
import { CREATE_STEP } from 'graphql/mutations/create-step'; import { CREATE_STEP } from 'graphql/mutations/create-step';
import { UPDATE_STEP } from 'graphql/mutations/update-step'; import { UPDATE_STEP } from 'graphql/mutations/update-step';
import FlowStep from 'components/FlowStep'; import FlowStep from 'components/FlowStep';
import { FlowPropType } from 'propTypes/propTypes'; import { FlowPropType } from 'propTypes/propTypes';
import { useQueryClient } from '@tanstack/react-query';
function updateHandlerFactory(flowId, previousStepId) {
return function createStepUpdateHandler(cache, mutationResult) {
const { data } = mutationResult;
const { createStep: createdStep } = data;
const { getFlow: flow } = cache.readQuery({
query: GET_FLOW,
variables: { id: flowId },
});
const steps = flow.steps.reduce((steps, currentStep) => {
if (currentStep.id === previousStepId) {
return [...steps, currentStep, createdStep];
}
return [...steps, currentStep];
}, []);
cache.writeQuery({
query: GET_FLOW,
variables: { id: flowId },
data: { getFlow: { ...flow, steps } },
});
};
}
function Editor(props) { function Editor(props) {
const [updateStep] = useMutation(UPDATE_STEP); const [updateStep] = useMutation(UPDATE_STEP);
const [createStep, { loading: creationInProgress }] = useMutation( const [createStep, { loading: creationInProgress }] =
CREATE_STEP, useMutation(CREATE_STEP);
{
refetchQueries: ['GetFlow'],
},
);
const { flow } = props; const { flow } = props;
const [triggerStep] = flow.steps; const [triggerStep] = flow.steps;
const [currentStepId, setCurrentStepId] = React.useState(triggerStep.id); const [currentStepId, setCurrentStepId] = React.useState(triggerStep.id);
const queryClient = useQueryClient();
const onStepChange = React.useCallback( const onStepChange = React.useCallback(
(step) => { async (step) => {
const mutationInput = { const mutationInput = {
id: step.id, id: step.id,
key: step.key, key: step.key,
@@ -55,13 +32,20 @@ function Editor(props) {
id: flow.id, id: flow.id,
}, },
}; };
if (step.appKey) { if (step.appKey) {
mutationInput.appKey = step.appKey; mutationInput.appKey = step.appKey;
} }
updateStep({ variables: { input: mutationInput } });
await updateStep({ variables: { input: mutationInput } });
await queryClient.invalidateQueries({
queryKey: ['steps', step.id, 'connection'],
});
await queryClient.invalidateQueries({ queryKey: ['flows', flow.id] });
}, },
[updateStep, flow.id], [updateStep, flow.id, queryClient],
); );
const addStep = React.useCallback( const addStep = React.useCallback(
async (previousStepId) => { async (previousStepId) => {
const mutationInput = { const mutationInput = {
@@ -72,20 +56,24 @@ function Editor(props) {
id: flow.id, id: flow.id,
}, },
}; };
const createdStep = await createStep({ const createdStep = await createStep({
variables: { input: mutationInput }, variables: { input: mutationInput },
update: updateHandlerFactory(flow.id, previousStepId),
}); });
const createdStepId = createdStep.data.createStep.id; const createdStepId = createdStep.data.createStep.id;
setCurrentStepId(createdStepId); setCurrentStepId(createdStepId);
await queryClient.invalidateQueries({ queryKey: ['flows', flow.id] });
}, },
[createStep, flow.id], [createStep, flow.id, queryClient],
); );
const openNextStep = React.useCallback((nextStep) => { const openNextStep = React.useCallback((nextStep) => {
return () => { return () => {
setCurrentStepId(nextStep?.id); setCurrentStepId(nextStep?.id);
}; };
}, []); }, []);
return ( return (
<Box <Box
display="flex" display="flex"
@@ -106,6 +94,7 @@ function Editor(props) {
onOpen={() => setCurrentStepId(step.id)} onOpen={() => setCurrentStepId(step.id)}
onClose={() => setCurrentStepId(null)} onClose={() => setCurrentStepId(null)}
onChange={onStepChange} onChange={onStepChange}
flowId={flow.id}
onContinue={openNextStep(steps[index + 1])} onContinue={openNextStep(steps[index + 1])}
/> />

View File

@@ -1,6 +1,6 @@
import * as React from 'react'; import * as React from 'react';
import { Link, useParams } from 'react-router-dom'; import { Link, useParams } from 'react-router-dom';
import { useMutation, useQuery } from '@apollo/client'; import { useMutation } from '@apollo/client';
import Stack from '@mui/material/Stack'; import Stack from '@mui/material/Stack';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
@@ -8,6 +8,7 @@ import Tooltip from '@mui/material/Tooltip';
import IconButton from '@mui/material/IconButton'; import IconButton from '@mui/material/IconButton';
import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew'; import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew';
import Snackbar from '@mui/material/Snackbar'; import Snackbar from '@mui/material/Snackbar';
import { EditorProvider } from 'contexts/Editor'; import { EditorProvider } from 'contexts/Editor';
import EditableTypography from 'components/EditableTypography'; import EditableTypography from 'components/EditableTypography';
import Container from 'components/Container'; import Container from 'components/Container';
@@ -15,17 +16,20 @@ import Editor from 'components/Editor';
import useFormatMessage from 'hooks/useFormatMessage'; import useFormatMessage from 'hooks/useFormatMessage';
import { UPDATE_FLOW_STATUS } from 'graphql/mutations/update-flow-status'; import { UPDATE_FLOW_STATUS } from 'graphql/mutations/update-flow-status';
import { UPDATE_FLOW } from 'graphql/mutations/update-flow'; import { UPDATE_FLOW } from 'graphql/mutations/update-flow';
import { GET_FLOW } from 'graphql/queries/get-flow';
import * as URLS from 'config/urls'; import * as URLS from 'config/urls';
import { TopBar } from './style'; import { TopBar } from './style';
import useFlow from 'hooks/useFlow';
import { useQueryClient } from '@tanstack/react-query';
export default function EditorLayout() { export default function EditorLayout() {
const { flowId } = useParams(); const { flowId } = useParams();
const formatMessage = useFormatMessage(); const formatMessage = useFormatMessage();
const [updateFlow] = useMutation(UPDATE_FLOW); const [updateFlow] = useMutation(UPDATE_FLOW);
const [updateFlowStatus] = useMutation(UPDATE_FLOW_STATUS); const [updateFlowStatus] = useMutation(UPDATE_FLOW_STATUS);
const { data, loading } = useQuery(GET_FLOW, { variables: { id: flowId } }); const { data, isLoading: isFlowLoading } = useFlow(flowId);
const flow = data?.getFlow; const flow = data?.data;
const queryClient = useQueryClient();
const onFlowNameUpdate = React.useCallback( const onFlowNameUpdate = React.useCallback(
async (name) => { async (name) => {
await updateFlow({ await updateFlow({
@@ -38,14 +42,17 @@ export default function EditorLayout() {
optimisticResponse: { optimisticResponse: {
updateFlow: { updateFlow: {
__typename: 'Flow', __typename: 'Flow',
id: flow?.id, id: flowId,
name, name,
}, },
}, },
}); });
await queryClient.invalidateQueries({ queryKey: ['flows', flowId] });
}, },
[flow?.id], [flowId, queryClient],
); );
const onFlowStatusUpdate = React.useCallback( const onFlowStatusUpdate = React.useCallback(
async (active) => { async (active) => {
await updateFlowStatus({ await updateFlowStatus({
@@ -58,14 +65,17 @@ export default function EditorLayout() {
optimisticResponse: { optimisticResponse: {
updateFlowStatus: { updateFlowStatus: {
__typename: 'Flow', __typename: 'Flow',
id: flow?.id, id: flowId,
active, active,
}, },
}, },
}); });
await queryClient.invalidateQueries({ queryKey: ['flows', flowId] });
}, },
[flow?.id], [flowId, queryClient],
); );
return ( return (
<> <>
<TopBar <TopBar
@@ -94,7 +104,7 @@ export default function EditorLayout() {
</IconButton> </IconButton>
</Tooltip> </Tooltip>
{!loading && ( {!isFlowLoading && (
<EditableTypography <EditableTypography
variant="body1" variant="body1"
onConfirm={onFlowNameUpdate} onConfirm={onFlowNameUpdate}
@@ -124,7 +134,7 @@ export default function EditorLayout() {
<Stack direction="column" height="100%"> <Stack direction="column" height="100%">
<Container maxWidth="md"> <Container maxWidth="md">
<EditorProvider value={{ readOnly: !!flow?.active }}> <EditorProvider value={{ readOnly: !!flow?.active }}>
{!flow && !loading && 'not found'} {!flow && !isFlowLoading && 'not found'}
{flow && <Editor flow={flow} />} {flow && <Editor flow={flow} />}
</EditorProvider> </EditorProvider>

View File

@@ -2,9 +2,12 @@ import PropTypes from 'prop-types';
import { useMutation } from '@apollo/client'; import { useMutation } from '@apollo/client';
import Menu from '@mui/material/Menu'; import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem'; import MenuItem from '@mui/material/MenuItem';
import { useQueryClient } from '@tanstack/react-query';
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar'; import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
import * as React from 'react'; import * as React from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import Can from 'components/Can'; import Can from 'components/Can';
import * as URLS from 'config/urls'; import * as URLS from 'config/urls';
import { DELETE_FLOW } from 'graphql/mutations/delete-flow'; import { DELETE_FLOW } from 'graphql/mutations/delete-flow';
@@ -12,25 +15,33 @@ import { DUPLICATE_FLOW } from 'graphql/mutations/duplicate-flow';
import useFormatMessage from 'hooks/useFormatMessage'; import useFormatMessage from 'hooks/useFormatMessage';
function ContextMenu(props) { function ContextMenu(props) {
const { flowId, onClose, anchorEl } = props; const { flowId, onClose, anchorEl, onDuplicateFlow, onDeleteFlow, appKey } =
props;
const enqueueSnackbar = useEnqueueSnackbar(); const enqueueSnackbar = useEnqueueSnackbar();
const [deleteFlow] = useMutation(DELETE_FLOW);
const [duplicateFlow] = useMutation(DUPLICATE_FLOW, {
refetchQueries: ['GetFlows'],
});
const formatMessage = useFormatMessage(); const formatMessage = useFormatMessage();
const queryClient = useQueryClient();
const [duplicateFlow] = useMutation(DUPLICATE_FLOW);
const [deleteFlow] = useMutation(DELETE_FLOW);
const onFlowDuplicate = React.useCallback(async () => { const onFlowDuplicate = React.useCallback(async () => {
await duplicateFlow({ await duplicateFlow({
variables: { input: { id: flowId } }, variables: { input: { id: flowId } },
}); });
await queryClient.invalidateQueries({
queryKey: ['apps', appKey, 'flows'],
});
enqueueSnackbar(formatMessage('flow.successfullyDuplicated'), { enqueueSnackbar(formatMessage('flow.successfullyDuplicated'), {
variant: 'success', variant: 'success',
SnackbarProps: { SnackbarProps: {
'data-test': 'snackbar-duplicate-flow-success', 'data-test': 'snackbar-duplicate-flow-success',
}, },
}); });
onDuplicateFlow?.();
onClose(); onClose();
}, [flowId, onClose, duplicateFlow]); }, [flowId, onClose, duplicateFlow, queryClient, onDuplicateFlow]);
const onFlowDelete = React.useCallback(async () => { const onFlowDelete = React.useCallback(async () => {
await deleteFlow({ await deleteFlow({
variables: { input: { id: flowId } }, variables: { input: { id: flowId } },
@@ -44,11 +55,18 @@ function ContextMenu(props) {
}); });
}, },
}); });
await queryClient.invalidateQueries({
queryKey: ['apps', appKey, 'flows'],
});
enqueueSnackbar(formatMessage('flow.successfullyDeleted'), { enqueueSnackbar(formatMessage('flow.successfullyDeleted'), {
variant: 'success', variant: 'success',
}); });
onDeleteFlow?.();
onClose(); onClose();
}, [flowId, onClose, deleteFlow]); }, [flowId, onClose, deleteFlow, queryClient, onDeleteFlow]);
return ( return (
<Menu <Menu
open={true} open={true}
@@ -90,6 +108,9 @@ ContextMenu.propTypes = {
PropTypes.func, PropTypes.func,
PropTypes.shape({ current: PropTypes.instanceOf(Element) }), PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
]).isRequired, ]).isRequired,
onDeleteFlow: PropTypes.func,
onDuplicateFlow: PropTypes.func,
appKey: PropTypes.string.isRequired,
}; };
export default ContextMenu; export default ContextMenu;

View File

@@ -1,4 +1,5 @@
import * as React from 'react'; import * as React from 'react';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import Card from '@mui/material/Card'; import Card from '@mui/material/Card';
import IconButton from '@mui/material/IconButton'; import IconButton from '@mui/material/IconButton';
@@ -6,6 +7,7 @@ import CardActionArea from '@mui/material/CardActionArea';
import Chip from '@mui/material/Chip'; import Chip from '@mui/material/Chip';
import MoreHorizIcon from '@mui/icons-material/MoreHoriz'; import MoreHorizIcon from '@mui/icons-material/MoreHoriz';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import FlowAppIcons from 'components/FlowAppIcons'; import FlowAppIcons from 'components/FlowAppIcons';
import FlowContextMenu from 'components/FlowContextMenu'; import FlowContextMenu from 'components/FlowContextMenu';
import useFormatMessage from 'hooks/useFormatMessage'; import useFormatMessage from 'hooks/useFormatMessage';
@@ -35,7 +37,7 @@ function FlowRow(props) {
const formatMessage = useFormatMessage(); const formatMessage = useFormatMessage();
const contextButtonRef = React.useRef(null); const contextButtonRef = React.useRef(null);
const [anchorEl, setAnchorEl] = React.useState(null); const [anchorEl, setAnchorEl] = React.useState(null);
const { flow } = props; const { flow, onDuplicateFlow, onDeleteFlow, appKey } = props;
const handleClose = () => { const handleClose = () => {
setAnchorEl(null); setAnchorEl(null);
}; };
@@ -112,6 +114,9 @@ function FlowRow(props) {
flowId={flow.id} flowId={flow.id}
onClose={handleClose} onClose={handleClose}
anchorEl={anchorEl} anchorEl={anchorEl}
onDeleteFlow={onDeleteFlow}
onDuplicateFlow={onDuplicateFlow}
appKey={appKey}
/> />
)} )}
</> </>
@@ -120,6 +125,9 @@ function FlowRow(props) {
FlowRow.propTypes = { FlowRow.propTypes = {
flow: FlowPropType.isRequired, flow: FlowPropType.isRequired,
onDeleteFlow: PropTypes.func,
onDuplicateFlow: PropTypes.func,
appKey: PropTypes.string.isRequired,
}; };
export default FlowRow; export default FlowRow;

View File

@@ -1,6 +1,5 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import * as React from 'react'; import * as React from 'react';
import { useLazyQuery } from '@apollo/client';
import Stack from '@mui/material/Stack'; import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
@@ -24,7 +23,7 @@ import ChooseConnectionSubstep from 'components/ChooseConnectionSubstep';
import Form from 'components/Form'; import Form from 'components/Form';
import FlowStepContextMenu from 'components/FlowStepContextMenu'; import FlowStepContextMenu from 'components/FlowStepContextMenu';
import AppIcon from 'components/AppIcon'; import AppIcon from 'components/AppIcon';
import { GET_STEP_WITH_TEST_EXECUTIONS } from 'graphql/queries/get-step-with-test-executions';
import useFormatMessage from 'hooks/useFormatMessage'; import useFormatMessage from 'hooks/useFormatMessage';
import useApps from 'hooks/useApps'; import useApps from 'hooks/useApps';
import { import {
@@ -40,6 +39,7 @@ 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';
const validIcon = <CheckCircleIcon color="success" />; const validIcon = <CheckCircleIcon color="success" />;
const errorIcon = <ErrorIcon color="error" />; const errorIcon = <ErrorIcon color="error" />;
@@ -105,7 +105,7 @@ function generateValidationSchema(substeps) {
} }
function FlowStep(props) { function FlowStep(props) {
const { collapsed, onChange, onContinue } = props; const { collapsed, onChange, onContinue, flowId } = props;
const editorContext = React.useContext(EditorContext); const editorContext = React.useContext(EditorContext);
const contextButtonRef = React.useRef(null); const contextButtonRef = React.useRef(null);
const step = props.step; const step = props.step;
@@ -126,28 +126,16 @@ function FlowStep(props) {
const { data: apps } = useApps(useAppsOptions); const { data: apps } = useApps(useAppsOptions);
const [ const { data: stepWithTestExecutions, refetch } = useStepWithTestExecutions(
getStepWithTestExecutions, step.id,
{ data: stepWithTestExecutionsData, called: stepWithTestExecutionsCalled }, );
] = useLazyQuery(GET_STEP_WITH_TEST_EXECUTIONS, { const stepWithTestExecutionsData = stepWithTestExecutions?.data;
fetchPolicy: 'network-only',
});
React.useEffect(() => { React.useEffect(() => {
if (!stepWithTestExecutionsCalled && !collapsed && !isTrigger) { if (!collapsed && !isTrigger) {
getStepWithTestExecutions({ refetch(step.id);
variables: {
stepId: step.id,
},
});
} }
}, [ }, [collapsed, refetch, step.id, isTrigger]);
collapsed,
stepWithTestExecutionsCalled,
getStepWithTestExecutions,
step.id,
isTrigger,
]);
const app = apps?.data?.find((currentApp) => currentApp.key === step.appKey); const app = apps?.data?.find((currentApp) => currentApp.key === step.appKey);
@@ -274,9 +262,7 @@ function FlowStep(props) {
<Collapse in={!collapsed} unmountOnExit> <Collapse in={!collapsed} unmountOnExit>
<Content> <Content>
<List> <List>
<StepExecutionsProvider <StepExecutionsProvider value={stepWithTestExecutionsData}>
value={stepWithTestExecutionsData?.getStepWithTestExecutions}
>
<Form <Form
defaultValues={step} defaultValues={step}
onSubmit={handleSubmit} onSubmit={handleSubmit}
@@ -328,6 +314,7 @@ function FlowStep(props) {
: false : false
} }
step={step} step={step}
flowId={flowId}
/> />
)} )}
@@ -363,6 +350,7 @@ function FlowStep(props) {
deletable={!isTrigger} deletable={!isTrigger}
onClose={onContextMenuClose} onClose={onContextMenuClose}
anchorEl={anchorEl} anchorEl={anchorEl}
flowId={flowId}
/> />
)} )}
</Wrapper> </Wrapper>

View File

@@ -1,24 +1,29 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import * as React from 'react'; import * as React from 'react';
import { useMutation } from '@apollo/client'; import { useMutation } from '@apollo/client';
import Menu from '@mui/material/Menu'; import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem'; import MenuItem from '@mui/material/MenuItem';
import { DELETE_STEP } from 'graphql/mutations/delete-step'; import { DELETE_STEP } from 'graphql/mutations/delete-step';
import useFormatMessage from 'hooks/useFormatMessage'; import useFormatMessage from 'hooks/useFormatMessage';
import { useQueryClient } from '@tanstack/react-query';
function FlowStepContextMenu(props) { function FlowStepContextMenu(props) {
const { stepId, onClose, anchorEl, deletable } = props; const { stepId, onClose, anchorEl, deletable, flowId } = props;
const [deleteStep] = useMutation(DELETE_STEP, {
refetchQueries: ['GetFlow', 'GetStepWithTestExecutions'],
});
const formatMessage = useFormatMessage(); const formatMessage = useFormatMessage();
const queryClient = useQueryClient();
const [deleteStep] = useMutation(DELETE_STEP);
const deleteActionHandler = React.useCallback( const deleteActionHandler = React.useCallback(
async (event) => { async (event) => {
event.stopPropagation(); event.stopPropagation();
await deleteStep({ variables: { input: { id: stepId } } }); await deleteStep({ variables: { input: { id: stepId } } });
await queryClient.invalidateQueries({ queryKey: ['flows', flowId] });
}, },
[stepId], [stepId, queryClient],
); );
return ( return (
<Menu <Menu
open={true} open={true}

View File

@@ -178,6 +178,7 @@ export default function InputCreator(props) {
helperText={description} helperText={description}
clickToCopy={schema.clickToCopy} clickToCopy={schema.clickToCopy}
shouldUnregister={shouldUnregister} shouldUnregister={shouldUnregister}
disabled={disabled}
/> />
{isDynamicFieldsLoading && !additionalFields?.length && ( {isDynamicFieldsLoading && !additionalFields?.length && (

View File

@@ -12,17 +12,20 @@ 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'; import useFormatMessage from 'hooks/useFormatMessage';
function LoginForm() { function LoginForm() {
const isCloud = useCloud(); const isCloud = useCloud();
const navigate = useNavigate(); const navigate = useNavigate();
const formatMessage = useFormatMessage(); const formatMessage = useFormatMessage();
const authentication = useAuthentication(); const authentication = useAuthentication();
const [login, { loading }] = useMutation(LOGIN); const [login, { loading }] = useMutation(LOGIN);
React.useEffect(() => { React.useEffect(() => {
if (authentication.isAuthenticated) { if (authentication.isAuthenticated) {
navigate(URLS.DASHBOARD); navigate(URLS.DASHBOARD);
} }
}, [authentication.isAuthenticated]); }, [authentication.isAuthenticated]);
const handleSubmit = async (values) => { const handleSubmit = async (values) => {
const { data } = await login({ const { data } = await login({
variables: { variables: {
@@ -32,6 +35,7 @@ function LoginForm() {
const { token } = data.login; const { token } = data.login;
authentication.updateToken(token); authentication.updateToken(token);
}; };
return ( return (
<Paper sx={{ px: 2, py: 4 }}> <Paper sx={{ px: 2, py: 4 }}>
<Typography <Typography
@@ -107,4 +111,5 @@ function LoginForm() {
</Paper> </Paper>
); );
} }
export default LoginForm; export default LoginForm;

View File

@@ -1,17 +1,20 @@
import MuiTabs from '@mui/material/Tabs'; import MuiTabs from '@mui/material/Tabs';
import { styled } from '@mui/material/styles'; import { styled } from '@mui/material/styles';
export const ChildrenWrapper = styled('div')` export const ChildrenWrapper = styled('div')`
overflow-wrap: break-word; overflow-wrap: break-word;
word-wrap: break-word; word-wrap: break-word;
word-break: break-word; word-break: break-word;
hyphens: auto; hyphens: auto;
`; `;
export const InputLabelWrapper = styled('div')` export const InputLabelWrapper = styled('div')`
position: absolute; position: absolute;
left: ${({ theme }) => theme.spacing(1.75)}; left: ${({ theme }) => theme.spacing(1.75)};
inset: 0; inset: 0;
left: -6px; left: -6px;
`; `;
export const FakeInput = styled('div', { export const FakeInput = styled('div', {
shouldForwardProp: (prop) => prop !== 'disabled', shouldForwardProp: (prop) => prop !== 'disabled',
})` })`
@@ -31,27 +34,31 @@ export const FakeInput = styled('div', {
border-color: ${theme.palette.action.disabled}; border-color: ${theme.palette.action.disabled};
`} `}
&:hover { ${({ disabled, theme }) =>
border-color: ${({ theme }) => theme.palette.text.primary}; !disabled &&
} `
&:hover {
&:focus-within, border-color: ${theme.palette.text.primary};
&:focus {
&:before {
border-color: ${({ theme }) => theme.palette.primary.main};
border-radius: ${({ theme }) => theme.spacing(0.5)};
border-style: solid;
border-width: 2px;
bottom: -2px;
content: '';
display: block;
left: -2px;
position: absolute;
right: -2px;
top: -2px;
} }
} &:focus-within,
&:focus {
&:before {
border-color: ${theme.palette.primary.main};
border-radius: ${theme.spacing(0.5)};
border-style: solid;
border-width: 2px;
bottom: -2px;
content: '';
display: block;
left: -2px;
position: absolute;
right: -2px;
top: -2px;
}
}
`}
`; `;
export const Tabs = styled(MuiTabs)` export const Tabs = styled(MuiTabs)`
border-bottom: 1px solid ${({ theme }) => theme.palette.divider}; border-bottom: 1px solid ${({ theme }) => theme.palette.divider};
`; `;

View File

@@ -4,12 +4,12 @@ import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import api from 'helpers/api.js'; import api from 'helpers/api.js';
import useAuthentication from 'hooks/useAuthentication.js';
const queryClient = new QueryClient({ const queryClient = new QueryClient({
defaultOptions: { defaultOptions: {
queries: { queries: {
staleTime: 1000, staleTime: 1000,
retryOnMount: false,
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
// provides a convenient default while it should be overridden for other HTTP methods // provides a convenient default while it should be overridden for other HTTP methods
queryFn: async ({ queryKey, signal }) => { queryFn: async ({ queryKey, signal }) => {
@@ -25,27 +25,9 @@ const queryClient = new QueryClient({
}); });
export default function AutomatischQueryClientProvider({ children }) { export default function AutomatischQueryClientProvider({ children }) {
const { token, initialize } = useAuthentication();
React.useEffect(
function updateTokenInHttpClient() {
if (!initialize) return;
if (token) {
api.defaults.headers.Authorization = token;
} else {
delete api.defaults.headers.Authorization;
}
initialize();
},
[initialize, token],
);
return ( return (
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
{children} {children}
<ReactQueryDevtools /> <ReactQueryDevtools />
</QueryClientProvider> </QueryClientProvider>
); );

View File

@@ -1,3 +1,4 @@
import PropTypes from 'prop-types';
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'; import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
import ButtonGroup from '@mui/material/ButtonGroup'; import ButtonGroup from '@mui/material/ButtonGroup';
@@ -9,21 +10,26 @@ import Paper from '@mui/material/Paper';
import Popper from '@mui/material/Popper'; import Popper from '@mui/material/Popper';
import * as React from 'react'; import * as React from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
export default function SplitButton(props) { export default function SplitButton(props) {
const { options, disabled, defaultActionIndex = 0 } = props; const { options, disabled, defaultActionIndex = 0 } = props;
const [open, setOpen] = React.useState(false); const [open, setOpen] = React.useState(false);
const anchorRef = React.useRef(null); const anchorRef = React.useRef(null);
const multiOptions = options.length > 1; const multiOptions = options.length > 1;
const selectedOption = options[defaultActionIndex]; const selectedOption = options[defaultActionIndex];
const handleToggle = () => { const handleToggle = () => {
setOpen((prevOpen) => !prevOpen); setOpen((prevOpen) => !prevOpen);
}; };
const handleClose = (event) => { const handleClose = (event) => {
if (anchorRef.current && anchorRef.current.contains(event.target)) { if (anchorRef.current && anchorRef.current.contains(event.target)) {
return; return;
} }
setOpen(false); setOpen(false);
}; };
return ( return (
<React.Fragment> <React.Fragment>
<ButtonGroup <ButtonGroup
@@ -42,6 +48,7 @@ export default function SplitButton(props) {
borderRadius: 0, borderRadius: 0,
borderRight: '1px solid #bdbdbd', borderRight: '1px solid #bdbdbd',
}} }}
disabled={selectedOption.disabled}
> >
{selectedOption.label} {selectedOption.label}
</Button> </Button>
@@ -80,6 +87,7 @@ export default function SplitButton(props) {
selected={index === defaultActionIndex} selected={index === defaultActionIndex}
component={Link} component={Link}
to={option.to} to={option.to}
disabled={option.disabled}
> >
{option.label} {option.label}
</MenuItem> </MenuItem>
@@ -94,3 +102,17 @@ export default function SplitButton(props) {
</React.Fragment> </React.Fragment>
); );
} }
SplitButton.propTypes = {
options: PropTypes.arrayOf(
PropTypes.shape({
label: PropTypes.string.isRequired,
key: PropTypes.string.isRequired,
'data-test': PropTypes.string.isRequired,
to: PropTypes.string.isRequired,
disabled: PropTypes.bool.isRequired,
}).isRequired,
).isRequired,
disabled: PropTypes.bool,
defaultActionIndex: PropTypes.number,
};

View File

@@ -3,11 +3,21 @@ import Alert from '@mui/material/Alert';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import useSubscription from 'hooks/useSubscription.ee'; import useSubscription from 'hooks/useSubscription.ee';
import useFormatMessage from 'hooks/useFormatMessage';
import { DateTime } from 'luxon';
import useUserTrial from 'hooks/useUserTrial.ee';
export default function SubscriptionCancelledAlert() { export default function SubscriptionCancelledAlert() {
const formatMessage = useFormatMessage();
const subscription = useSubscription(); const subscription = useSubscription();
const trial = useUserTrial();
if (!subscription) return <React.Fragment />; if (subscription?.data?.status === 'active' || trial.hasTrial)
return <React.Fragment />;
const cancellationEffectiveDateObject = DateTime.fromISO(
subscription?.data?.cancellationEffectiveDate,
);
return ( return (
<Alert <Alert
@@ -18,7 +28,9 @@ export default function SubscriptionCancelledAlert() {
}} }}
> >
<Typography variant="subtitle2" sx={{ lineHeight: 1.5 }}> <Typography variant="subtitle2" sx={{ lineHeight: 1.5 }}>
{subscription.message} {formatMessage('subscriptionCancelledAlert.text', {
date: cancellationEffectiveDateObject.toFormat('DDD'),
})}
</Typography> </Typography>
</Alert> </Alert>
); );

View File

@@ -6,12 +6,15 @@ import ListItem from '@mui/material/ListItem';
import Alert from '@mui/material/Alert'; import Alert from '@mui/material/Alert';
import AlertTitle from '@mui/material/AlertTitle'; import AlertTitle from '@mui/material/AlertTitle';
import LoadingButton from '@mui/lab/LoadingButton'; import LoadingButton from '@mui/lab/LoadingButton';
import { EditorContext } from 'contexts/Editor'; import { EditorContext } from 'contexts/Editor';
import useFormatMessage from 'hooks/useFormatMessage'; import useFormatMessage from 'hooks/useFormatMessage';
import { EXECUTE_FLOW } from 'graphql/mutations/execute-flow'; import { EXECUTE_FLOW } from 'graphql/mutations/execute-flow';
import JSONViewer from 'components/JSONViewer'; import JSONViewer from 'components/JSONViewer';
import WebhookUrlInfo from 'components/WebhookUrlInfo'; import WebhookUrlInfo from 'components/WebhookUrlInfo';
import FlowSubstepTitle from 'components/FlowSubstepTitle'; import FlowSubstepTitle from 'components/FlowSubstepTitle';
import { useQueryClient } from '@tanstack/react-query';
function serializeErrors(graphQLErrors) { function serializeErrors(graphQLErrors) {
return graphQLErrors?.map((error) => { return graphQLErrors?.map((error) => {
try { try {
@@ -28,6 +31,7 @@ function serializeErrors(graphQLErrors) {
} }
}); });
} }
function TestSubstep(props) { function TestSubstep(props) {
const { const {
substep, substep,
@@ -38,13 +42,13 @@ function TestSubstep(props) {
onContinue, onContinue,
step, step,
showWebhookUrl = false, showWebhookUrl = false,
flowId,
} = props; } = props;
const formatMessage = useFormatMessage(); const formatMessage = useFormatMessage();
const editorContext = React.useContext(EditorContext); const editorContext = React.useContext(EditorContext);
const [executeFlow, { data, error, loading, called, reset }] = useMutation( const [executeFlow, { data, error, loading, called, reset }] = useMutation(
EXECUTE_FLOW, EXECUTE_FLOW,
{ {
refetchQueries: ['GetStepWithTestExecutions'],
context: { autoSnackbar: false }, context: { autoSnackbar: false },
}, },
); );
@@ -52,6 +56,8 @@ function TestSubstep(props) {
const isCompleted = !error && called && !loading; const isCompleted = !error && called && !loading;
const hasNoOutput = !response && isCompleted; const hasNoOutput = !response && isCompleted;
const { name } = substep; const { name } = substep;
const queryClient = useQueryClient();
React.useEffect( React.useEffect(
function resetTestDataOnSubstepToggle() { function resetTestDataOnSubstepToggle() {
if (!expanded) { if (!expanded) {
@@ -60,20 +66,28 @@ function TestSubstep(props) {
}, },
[expanded, reset], [expanded, reset],
); );
const handleSubmit = React.useCallback(() => {
const handleSubmit = React.useCallback(async () => {
if (isCompleted) { if (isCompleted) {
onContinue?.(); onContinue?.();
return; return;
} }
executeFlow({
await executeFlow({
variables: { variables: {
input: { input: {
stepId: step.id, stepId: step.id,
}, },
}, },
}); });
}, [onSubmit, onContinue, isCompleted, step.id]);
await queryClient.invalidateQueries({
queryKey: ['flows', flowId],
});
}, [onSubmit, onContinue, isCompleted, queryClient, flowId]);
const onToggle = expanded ? onCollapse : onExpand; const onToggle = expanded ? onCollapse : onExpand;
return ( return (
<React.Fragment> <React.Fragment>
<FlowSubstepTitle expanded={expanded} onClick={onToggle} title={name} /> <FlowSubstepTitle expanded={expanded} onClick={onToggle} title={name} />

View File

@@ -8,7 +8,7 @@ import useUserTrial from 'hooks/useUserTrial.ee';
export default function TrialStatusBadge() { export default function TrialStatusBadge() {
const data = useUserTrial(); const data = useUserTrial();
if (!data) return <React.Fragment />; if (!data.hasTrial) return <React.Fragment />;
const { message, status } = data; const { message, status } = data;

View File

@@ -10,15 +10,23 @@ import CardContent from '@mui/material/CardContent';
import Divider from '@mui/material/Divider'; import Divider from '@mui/material/Divider';
import Grid from '@mui/material/Grid'; import Grid from '@mui/material/Grid';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import TrialOverAlert from 'components/TrialOverAlert/index.ee'; import TrialOverAlert from 'components/TrialOverAlert/index.ee';
import SubscriptionCancelledAlert from 'components/SubscriptionCancelledAlert/index.ee'; import SubscriptionCancelledAlert from 'components/SubscriptionCancelledAlert/index.ee';
import CheckoutCompletedAlert from 'components/CheckoutCompletedAlert/index.ee'; import CheckoutCompletedAlert from 'components/CheckoutCompletedAlert/index.ee';
import * as URLS from 'config/urls'; import * as URLS from 'config/urls';
import useBillingAndUsageData from 'hooks/useBillingAndUsageData.ee';
import useFormatMessage from 'hooks/useFormatMessage'; import useFormatMessage from 'hooks/useFormatMessage';
import usePlanAndUsage from 'hooks/usePlanAndUsage';
import useSubscription from 'hooks/useSubscription.ee';
import useUserTrial from 'hooks/useUserTrial.ee';
import { useQueryClient } from '@tanstack/react-query';
import useCurrentUser from 'hooks/useCurrentUser';
const capitalize = (str) => str[0].toUpperCase() + str.slice(1, str.length); const capitalize = (str) => str[0].toUpperCase() + str.slice(1, str.length);
function BillingCard(props) { function BillingCard(props) {
const { name, title = '', action } = props; const { name, title = '', action, text } = props;
return ( return (
<Card <Card
sx={{ sx={{
@@ -40,42 +48,94 @@ function BillingCard(props) {
</CardContent> </CardContent>
<CardActions> <CardActions>
<Action action={action} /> <Action action={action} text={text} />
</CardActions> </CardActions>
</Card> </Card>
); );
} }
function Action(props) { function Action(props) {
const { action } = props; const { action, text } = props;
if (!action) return <React.Fragment />; if (!action) return <React.Fragment />;
const { text, type } = action;
if (type === 'link') { if (action.startsWith('http')) {
if (action.src.startsWith('http')) {
return (
<Button size="small" href={action.src} target="_blank">
{text}
</Button>
);
} else {
return (
<Button size="small" component={Link} to={action.src}>
{text}
</Button>
);
}
}
if (type === 'text') {
return ( return (
<Typography variant="subtitle2" pb={1}> <Button size="small" href={action} target="_blank">
{text} {text}
</Typography> </Button>
);
} else if (action.startsWith('/')) {
return (
<Button size="small" component={Link} to={action}>
{text}
</Button>
); );
} }
return <React.Fragment />;
return (
<Typography variant="subtitle2" pb={1}>
{text}
</Typography>
);
} }
export default function UsageDataInformation() { export default function UsageDataInformation() {
const formatMessage = useFormatMessage(); const formatMessage = useFormatMessage();
const billingAndUsageData = useBillingAndUsageData(); const queryClient = useQueryClient();
const { data: currentUser } = useCurrentUser();
const currentUserId = currentUser?.data?.id;
const { data } = usePlanAndUsage(currentUserId);
const planAndUsage = data?.data;
const trial = useUserTrial();
const subscriptionData = useSubscription();
const subscription = subscriptionData?.data;
let billingInfo;
React.useEffect(() => {
queryClient.invalidateQueries({
queryKey: ['users', currentUserId, 'planAndUsage'],
});
}, [subscription, queryClient, currentUserId]);
if (trial.hasTrial) {
billingInfo = {
monthlyQuota: {
title: formatMessage('usageDataInformation.freeTrial'),
action: URLS.SETTINGS_PLAN_UPGRADE,
text: 'Upgrade plan',
},
nextBillAmount: {
title: '---',
action: null,
text: null,
},
nextBillDate: {
title: '---',
action: null,
text: null,
},
};
} else {
billingInfo = {
monthlyQuota: {
title: planAndUsage?.plan?.limit,
action: subscription?.cancelUrl,
text: formatMessage('usageDataInformation.cancelPlan'),
},
nextBillAmount: {
title: `${subscription?.nextBillAmount}`,
action: subscription?.updateUrl,
text: formatMessage('usageDataInformation.updatePaymentMethod'),
},
nextBillDate: {
title: subscription?.nextBillDate,
action: formatMessage('usageDataInformation.monthlyPayment'),
text: formatMessage('usageDataInformation.monthlyPayment'),
},
};
}
return ( return (
<React.Fragment> <React.Fragment>
<Stack sx={{ width: '100%', mb: 2 }} spacing={2}> <Stack sx={{ width: '100%', mb: 2 }} spacing={2}>
@@ -92,11 +152,8 @@ export default function UsageDataInformation() {
{formatMessage('usageDataInformation.subscriptionPlan')} {formatMessage('usageDataInformation.subscriptionPlan')}
</Typography> </Typography>
{billingAndUsageData?.subscription?.status && ( {subscription?.status && (
<Chip <Chip label={capitalize(subscription?.status)} color="success" />
label={capitalize(billingAndUsageData?.subscription?.status)}
color="success"
/>
)} )}
</Box> </Box>
@@ -113,26 +170,27 @@ export default function UsageDataInformation() {
<Grid item xs={12} md={4}> <Grid item xs={12} md={4}>
<BillingCard <BillingCard
name={formatMessage('usageDataInformation.monthlyQuota')} name={formatMessage('usageDataInformation.monthlyQuota')}
title={billingAndUsageData?.subscription?.monthlyQuota.title} title={billingInfo.monthlyQuota.title}
action={billingAndUsageData?.subscription?.monthlyQuota.action} action={billingInfo.monthlyQuota.action}
text={billingInfo.monthlyQuota.text}
/> />
</Grid> </Grid>
<Grid item xs={12} md={4}> <Grid item xs={12} md={4}>
<BillingCard <BillingCard
name={formatMessage('usageDataInformation.nextBillAmount')} name={formatMessage('usageDataInformation.nextBillAmount')}
title={billingAndUsageData?.subscription?.nextBillAmount.title} title={billingInfo.nextBillAmount.title}
action={ action={billingInfo.nextBillAmount.action}
billingAndUsageData?.subscription?.nextBillAmount.action text={billingInfo.nextBillAmount.text}
}
/> />
</Grid> </Grid>
<Grid item xs={12} md={4}> <Grid item xs={12} md={4}>
<BillingCard <BillingCard
name={formatMessage('usageDataInformation.nextBillDate')} name={formatMessage('usageDataInformation.nextBillDate')}
title={billingAndUsageData?.subscription?.nextBillDate.title} title={billingInfo.nextBillDate.title}
action={billingAndUsageData?.subscription?.nextBillDate.action} action={billingInfo.nextBillDate.action}
text={billingInfo.nextBillDate.text}
/> />
</Grid> </Grid>
</Grid> </Grid>
@@ -171,7 +229,7 @@ export default function UsageDataInformation() {
variant="subtitle2" variant="subtitle2"
sx={{ color: 'text.secondary', mt: 2, fontWeight: 500 }} sx={{ color: 'text.secondary', mt: 2, fontWeight: 500 }}
> >
{billingAndUsageData?.usage.task} {planAndUsage?.usage.task}
</Typography> </Typography>
</Box> </Box>
@@ -179,7 +237,7 @@ export default function UsageDataInformation() {
</Box> </Box>
{/* free plan has `null` status so that we can show the upgrade button */} {/* free plan has `null` status so that we can show the upgrade button */}
{billingAndUsageData?.subscription?.status === null && ( {subscription?.status === undefined && (
<Button <Button
component={Link} component={Link}
to={URLS.SETTINGS_PLAN_UPGRADE} to={URLS.SETTINGS_PLAN_UPGRADE}

View File

@@ -17,13 +17,13 @@ export const APP_ADD_CONNECTION = (appKey, shared = false) =>
`/app/${appKey}/connections/add?shared=${shared}`; `/app/${appKey}/connections/add?shared=${shared}`;
export const APP_ADD_CONNECTION_WITH_AUTH_CLIENT_ID = ( export const APP_ADD_CONNECTION_WITH_AUTH_CLIENT_ID = (
appKey, appKey,
appAuthClientId appAuthClientId,
) => `/app/${appKey}/connections/add?appAuthClientId=${appAuthClientId}`; ) => `/app/${appKey}/connections/add?appAuthClientId=${appAuthClientId}`;
export const APP_ADD_CONNECTION_PATTERN = '/app/:appKey/connections/add'; export const APP_ADD_CONNECTION_PATTERN = '/app/:appKey/connections/add';
export const APP_RECONNECT_CONNECTION = ( export const APP_RECONNECT_CONNECTION = (
appKey, appKey,
connectionId, connectionId,
appAuthClientId appAuthClientId,
) => { ) => {
const path = `/app/${appKey}/connections/${connectionId}/reconnect`; const path = `/app/${appKey}/connections/${connectionId}/reconnect`;
if (appAuthClientId) { if (appAuthClientId) {

View File

@@ -1,31 +1,34 @@
import * as React from 'react'; import * as React from 'react';
import { getItem, setItem } from 'helpers/storage'; import { getItem, removeItem, setItem } from 'helpers/storage';
import api from 'helpers/api.js';
export const AuthenticationContext = React.createContext({ export const AuthenticationContext = React.createContext({
token: null, token: null,
updateToken: () => void 0, updateToken: () => {},
removeToken: () => {},
isAuthenticated: false, isAuthenticated: false,
initialize: () => void 0,
}); });
export const AuthenticationProvider = (props) => { export const AuthenticationProvider = (props) => {
const { children } = props; const { children } = props;
const [isInitialized, setInitialized] = React.useState(false);
const [token, setToken] = React.useState(() => getItem('token')); const [token, setToken] = React.useState(() => getItem('token'));
const value = React.useMemo(() => { const value = React.useMemo(() => {
return { return {
token, token,
updateToken: (newToken) => { updateToken: (newToken) => {
api.defaults.headers.Authorization = newToken;
setToken(newToken); setToken(newToken);
setItem('token', newToken); setItem('token', newToken);
}, },
isAuthenticated: Boolean(token) && isInitialized, removeToken: () => {
initialize: () => { delete api.defaults.headers.Authorization;
setInitialized(true); setToken(null);
removeItem('token');
}, },
isAuthenticated: Boolean(token),
}; };
}, [token, isInitialized]); }, [token]);
return ( return (
<AuthenticationContext.Provider value={value}> <AuthenticationContext.Provider value={value}>

View File

@@ -4,7 +4,7 @@ import { useNavigate } from 'react-router-dom';
import * as URLS from 'config/urls'; import * as URLS from 'config/urls';
import useCloud from 'hooks/useCloud'; import useCloud from 'hooks/useCloud';
import usePaddleInfo from 'hooks/usePaddleInfo.ee'; import usePaddleInfo from 'hooks/usePaddleInfo.ee';
import apolloClient from 'graphql/client'; import { useQueryClient } from '@tanstack/react-query';
export const PaddleContext = React.createContext({ export const PaddleContext = React.createContext({
loaded: false, loaded: false,
@@ -17,6 +17,7 @@ export const PaddleProvider = (props) => {
const { data } = usePaddleInfo(); const { data } = usePaddleInfo();
const sandbox = data?.data?.sandbox; const sandbox = data?.data?.sandbox;
const vendorId = data?.data?.vendorId; const vendorId = data?.data?.vendorId;
const queryClient = useQueryClient();
const [loaded, setLoaded] = React.useState(false); const [loaded, setLoaded] = React.useState(false);
@@ -29,8 +30,12 @@ export const PaddleProvider = (props) => {
if (completed) { if (completed) {
// Paddle has side effects in the background, // Paddle has side effects in the background,
// so we need to refetch the relevant queries // so we need to refetch the relevant queries
await apolloClient.refetchQueries({ await queryClient.refetchQueries({
include: ['GetTrialStatus', 'GetBillingAndUsage'], queryKey: ['users', 'me', 'trial'],
});
await queryClient.refetchQueries({
queryKey: ['users', 'me', 'subscription'],
}); });
navigate(URLS.SETTINGS_BILLING_AND_USAGE, { navigate(URLS.SETTINGS_BILLING_AND_USAGE, {
@@ -39,7 +44,7 @@ export const PaddleProvider = (props) => {
} }
} }
}, },
[navigate], [navigate, queryClient],
); );
const value = React.useMemo(() => { const value = React.useMemo(() => {

View File

@@ -1,11 +0,0 @@
import { gql } from '@apollo/client';
export const GET_APP_AUTH_CLIENT = gql`
query GetAppAuthClient($id: String!) {
getAppAuthClient(id: $id) {
id
appConfigId
name
active
}
}
`;

View File

@@ -1,11 +0,0 @@
import { gql } from '@apollo/client';
export const GET_APP_AUTH_CLIENTS = gql`
query GetAppAuthClients($appKey: String!, $active: Boolean) {
getAppAuthClients(appKey: $appKey, active: $active) {
id
appConfigId
name
active
}
}
`;

View File

@@ -1,20 +0,0 @@
import { gql } from '@apollo/client';
export const GET_APP_CONNECTIONS = gql`
query GetAppConnections($key: String!) {
getApp(key: $key) {
key
connections {
id
key
reconnectable
appAuthClientId
verified
flowCount
formattedData {
screenName
}
createdAt
}
}
}
`;

View File

@@ -1,37 +0,0 @@
import { gql } from '@apollo/client';
export const GET_BILLING_AND_USAGE = gql`
query GetBillingAndUsage {
getBillingAndUsage {
subscription {
status
monthlyQuota {
title
action {
type
text
src
}
}
nextBillDate {
title
action {
type
text
src
}
}
nextBillAmount {
title
action {
type
text
src
}
}
}
usage {
task
}
}
}
`;

Some files were not shown because too many files have changed in this diff Show More