Compare commits

...

34 Commits

Author SHA1 Message Date
Rıdvan Akca
aee0674d20 feat(ftp): add ftp integration 2024-03-25 14:33:17 +03:00
Ömer Faruk Aydın
fc4eeed764 Merge pull request #1760 from automatisch/rest-app-auth-clients
feat: Implement get app auth clients API endpoint
2024-03-22 15:20:00 +01:00
Faruk AYDIN
3596d13be1 feat: Implement get app auth clients API endpoint 2024-03-22 15:05:37 +01:00
Ömer Faruk Aydın
104d49ea1c Merge pull request #1759 from automatisch/rest-admin-app-auth-clients
feat: Implement admin get app auth clients API endpoint
2024-03-22 15:05:30 +01:00
Faruk AYDIN
7057317446 refactor: Use ee extension for admin app auth clients 2024-03-22 14:48:46 +01:00
Faruk AYDIN
280575df88 refactor: Move app auth client mock to correct folder 2024-03-22 14:46:43 +01:00
Faruk AYDIN
d2cb434b7b refactor: Move admin get app auth client mock to correct folder 2024-03-22 14:44:35 +01:00
Faruk AYDIN
2ecb802a2e feat: Implement admin get app auth clients API endpoint 2024-03-22 14:42:48 +01:00
Ali BARIN
46e706c415 Merge pull request #1756 from automatisch/AUT-687
refactor: rewrite useConfig with RQ
2024-03-22 10:24:52 +01:00
kasia.oczkowska
3a57349d8a refactor: rewrite useConfig with RQ 2024-03-22 09:14:29 +00:00
Ali BARIN
754c3269ec Merge pull request #1739 from automatisch/dependabot/npm_and_yarn/follow-redirects-1.15.6
chore(deps): bump follow-redirects from 1.15.3 to 1.15.6
2024-03-22 10:07:35 +01:00
Ömer Faruk Aydın
a079842408 Merge pull request #1757 from automatisch/create-dynamic-fields-endpoint
feat: Implement create dynamic fields API endpoint
2024-03-22 03:05:11 +01:00
Faruk AYDIN
7664b58553 feat: Implement create dynamic fields API endpoint 2024-03-22 02:55:23 +01:00
Ali BARIN
de77488f7e Merge pull request #1754 from automatisch/AUT-697
refactor: rewrite useNotifications with RQ
2024-03-21 15:05:35 +01:00
kasia.oczkowska
d808afd21b refactor: rewrite useNotifications with RQ 2024-03-21 14:56:54 +01:00
Ömer Faruk Aydın
b68aff76a1 Merge pull request #1755 from automatisch/get-previous-steps
feat: Implement get previous steps rest API endpoint
2024-03-21 14:56:16 +01:00
Faruk AYDIN
6da7fe158f feat: Implement get previous steps rest API endpoint 2024-03-21 14:40:18 +01:00
Faruk AYDIN
4dbc7fdc7d feat: Extend step serializers to include execution steps 2024-03-21 14:39:22 +01:00
Ali BARIN
ad1e1f7eca Merge pull request #1750 from automatisch/make-respond-with-flexible
feat(webhooks/respond-with): accept custom headers
2024-03-21 12:03:51 +01:00
Ömer Faruk Aydın
9c3f7a3823 Merge pull request #1753 from automatisch/use-objection-for-factories
refactor: Use objection instead of knex for factories
2024-03-20 17:31:37 +01:00
Faruk AYDIN
86f4cb7701 refactor: Use objection instead of knex for factories 2024-03-20 17:24:44 +01:00
Ali BARIN
359a90245d Merge pull request #1734 from automatisch/AUT-845
refactor: rewrite useUsers with RQ
2024-03-20 16:08:07 +01:00
kasia.oczkowska
d8d7d86359 feat: invalidate queries on user related actions 2024-03-20 15:00:54 +00:00
Ali BARIN
7189b629c0 Merge pull request #1746 from automatisch/AUT-856
refactor: rewrite useFlows as useConnectionFlows and useAppFlows with RQ
2024-03-20 15:13:36 +01:00
kasia.oczkowska
55c9b5566c feat: rename hooks 2024-03-20 14:23:33 +01:00
kasia.oczkowska
ab671ccbf7 refactor: rewrite useUsers with RQ 2024-03-20 13:14:25 +00:00
Rıdvan Akca
316bda8c3f refactor: rewrite useFlows as useConnectionFlows and useAppFlows with RQ 2024-03-20 11:45:26 +03:00
Ömer Faruk Aydın
76f77e8a4c Merge pull request #1752 from automatisch/fix-step-factory
fix: Adjust step factory to use objection instead of knex
2024-03-20 02:15:13 +01:00
Faruk AYDIN
4a99d5eab7 fix: Adjust step factory to use objection instead of knex 2024-03-20 02:03:31 +01:00
Ali BARIN
473d287c6d feat(webhooks/respond-with): accept custom headers 2024-03-19 19:21:20 +00:00
Ömer Faruk Aydın
bddd9896e4 Merge pull request #1749 from automatisch/fix/docs-change
fix: Do not explicitly define github and context for CI actions
2024-03-19 20:08:08 +01:00
Faruk AYDIN
95eb115965 fix: Do not explicitly define github and context for CI actions 2024-03-19 17:49:05 +01:00
Rıdvan Akca
ec87c7f21c feat: introduce useLazyFlows with RQ 2024-03-19 16:56:53 +03:00
dependabot[bot]
5c684cd499 chore(deps): bump follow-redirects from 1.15.3 to 1.15.6
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.3 to 1.15.6.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.3...v1.15.6)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-16 20:12:03 +00:00
91 changed files with 1226 additions and 758 deletions

View File

@@ -13,8 +13,6 @@ jobs:
uses: actions/github-script@v6 uses: actions/github-script@v6
with: with:
script: | script: |
const github = require('@actions/github');
const { context } = github;
const { pull_request } = context.payload; const { pull_request } = context.payload;
const label = 'documentation-change'; const label = 'documentation-change';

View File

@@ -31,6 +31,7 @@
"accounting": "^0.4.1", "accounting": "^0.4.1",
"ajv-formats": "^2.1.1", "ajv-formats": "^2.1.1",
"axios": "1.6.0", "axios": "1.6.0",
"basic-ftp": "^5.0.5",
"bcrypt": "^5.0.1", "bcrypt": "^5.0.1",
"bullmq": "^3.0.0", "bullmq": "^3.0.0",
"cors": "^2.8.5", "cors": "^2.8.5",

View File

@@ -0,0 +1 @@
<?xml version="1.0" encoding="utf-8"?><svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 122.88 109.36" style="enable-background:new 0 0 122.88 109.36" xml:space="preserve"><g><path d="M14.69,16.44h3.56V8.63c0-1.09,0.88-1.97,1.97-1.97h3.5V1.97C23.72,0.88,24.6,0,25.69,0h31.92c1.09,0,1.97,0.88,1.97,1.97 v4.69h43.05c1.09,0,1.97,0.88,1.97,1.97v7.82h3.59c1.09,0,1.97,0.88,1.97,1.97c0,0.14-0.01,0.28-0.04,0.42l-6.88,50.59 c-0.22,1.65-0.95,3.18-2.05,4.29c-1.1,1.1-2.54,1.78-4.25,1.78H70.95v5.86h2.12c1.54,0,2.8,1.26,2.8,2.8v5.06h10.44 c1.25,0,2.32,0.83,2.67,1.97h26.1c3.73,0,6.14,2.07,7.2,4.71c0.4,1,0.6,2.08,0.6,3.14c0,1.06-0.2,2.14-0.6,3.14 c-1.06,2.64-3.47,4.71-7.2,4.71H89.09c-0.15,1.4-1.35,2.49-2.78,2.49H72.63H38.8c-1.44,0-2.63-1.1-2.78-2.49H7.8 c-3.73,0-6.14-2.07-7.2-4.71c-0.4-1-0.6-2.08-0.6-3.14c0-1.06,0.2-2.14,0.6-3.14c1.06-2.64,3.47-4.71,7.2-4.71h28.32 c0.36-1.14,1.42-1.97,2.67-1.97h8.74v-5.06c0-1.54,1.26-2.8,2.8-2.8h1.69v-5.86H24.38c-1.71,0-3.19-0.69-4.3-1.79 c-1.11-1.11-1.83-2.66-2.01-4.33l-5.33-50.74c-0.11-1.08,0.67-2.04,1.75-2.15C14.56,16.45,14.63,16.45,14.69,16.44L14.69,16.44 L14.69,16.44L14.69,16.44z M31.38,34.43h16.29v4.58H38v3.72h8.26v4.3H38v8.71h-6.61V34.43L31.38,34.43z M50.08,34.43H70.1v5.27 h-6.72v16.05h-6.59V39.7h-6.71V34.43L50.08,34.43z M73.41,34.43h10.95c2.38,0,4.17,0.57,5.35,1.7c1.19,1.14,1.78,2.75,1.78,4.84 c0,2.15-0.65,3.84-1.94,5.05c-1.3,1.21-3.27,1.82-5.93,1.82h-3.6v7.91h-6.61V34.43L73.41,34.43z M80.03,43.52h1.61 c1.27,0,2.16-0.22,2.68-0.66c0.51-0.44,0.77-1,0.77-1.69c0-0.67-0.22-1.24-0.67-1.7c-0.44-0.47-1.28-0.7-2.51-0.7h-1.88V43.52 L80.03,43.52z M56.38,81.34h10.21v-5.79H56.38V81.34L56.38,81.34z M89.11,94.88v8.27h25.97c1.97,0,3.22-1.04,3.76-2.37 c0.22-0.54,0.33-1.15,0.33-1.76s-0.11-1.22-0.33-1.76c-0.54-1.33-1.79-2.37-3.76-2.37H89.11L89.11,94.88z M36,103.15v-8.27H7.8 c-1.97,0-3.22,1.04-3.76,2.37c-0.22,0.54-0.33,1.15-0.33,1.76s0.11,1.22,0.33,1.76c0.54,1.33,1.79,2.37,3.76,2.37H36L36,103.15z M20.23,20.38h-3.35l5.1,48.57c0.08,0.78,0.39,1.47,0.87,1.95c0.4,0.4,0.92,0.64,1.53,0.64h72.54c0.59,0,1.1-0.24,1.48-0.62 c0.49-0.49,0.82-1.22,0.93-2.03l6.6-48.51L20.23,20.38L20.23,20.38L20.23,20.38z M22.19,10.6v5.83l78.46-0.83v-5H57.61 c-1.09,0-1.97-0.88-1.97-1.97V3.94H27.66v4.69c0,1.09-0.88,1.97-1.97,1.97L22.19,10.6L22.19,10.6L22.19,10.6z"/></g></svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -0,0 +1,91 @@
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: 'Email/Username',
type: 'string',
required: true,
readOnly: false,
value: null,
placeholder: null,
description: 'Your FTP login credentials.',
docUrl: 'https://automatisch.io/docs/ftp#username',
clickToCopy: false,
},
{
key: 'password',
label: 'Password',
type: 'string',
required: true,
readOnly: false,
value: null,
placeholder: null,
description: null,
docUrl: 'https://automatisch.io/docs/ftp#password',
clickToCopy: false,
},
{
key: 'secure',
label: 'Secure',
type: 'dropdown',
required: false,
readOnly: false,
value: false,
placeholder: null,
description: null,
docUrl: 'https://automatisch.io/docs/ftp#secure',
clickToCopy: false,
options: [
{
label: 'Yes',
value: true,
},
{
label: 'No',
value: false,
},
],
},
{
key: 'port',
label: 'Port',
type: 'string',
required: false,
readOnly: false,
value: '21',
placeholder: null,
description: null,
docUrl: 'https://automatisch.io/docs/ftp#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,19 @@
import { Client } from 'basic-ftp';
const verifyCredentials = async ($) => {
const client = new Client();
client.ftp.verbose = true;
await client.access({
host: $.auth.data.host,
user: $.auth.data.username,
password: $.auth.data.password,
secure: $.auth.data.secure,
});
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: 'FTP',
key: 'ftp',
iconUrl: '{BASE_URL}/apps/ftp/assets/favicon.svg',
authDocUrl: 'https://automatisch.io/docs/apps/ftp/connection',
supportsConnections: true,
baseUrl: '',
apiBaseUrl: '',
primaryColor: '000000',
auth,
});

View File

@@ -14,24 +14,55 @@ export default defineAction({
value: '200', value: '200',
}, },
{ {
label: 'JSON body', label: 'Headers',
key: 'stringifiedJsonBody', key: 'headers',
type: 'dynamic',
required: false,
description: 'Add or remove headers as needed',
fields: [
{
label: 'Key',
key: 'key',
type: 'string',
required: true,
description: 'Header key',
variables: true,
},
{
label: 'Value',
key: 'value',
type: 'string',
required: true,
description: 'Header value',
variables: true,
},
],
},
{
label: 'Body',
key: 'body',
type: 'string', type: 'string',
required: true, required: true,
description: 'The content of the JSON body. It must be a valid JSON.', description: 'The content of the response body.',
variables: true, variables: true,
}, },
], ],
async run($) { async run($) {
const parsedStatusCode = parseInt($.step.parameters.statusCode, 10); const statusCode = parseInt($.step.parameters.statusCode, 10);
const stringifiedJsonBody = $.step.parameters.stringifiedJsonBody; const body = $.step.parameters.body;
const parsedJsonBody = JSON.parse(stringifiedJsonBody); const headers = $.step.parameters.headers.reduce((result, entry) => {
return {
...result,
[entry.key]: entry.value,
};
}, {});
$.setActionItem({ $.setActionItem({
raw: { raw: {
body: parsedJsonBody, headers,
statusCode: parsedStatusCode, body,
statusCode,
}, },
}); });
}, },

View File

@@ -4,7 +4,7 @@ import Crypto from 'crypto';
import app from '../../../../../app.js'; import app from '../../../../../app.js';
import createAuthTokenByUserId from '../../../../../helpers/create-auth-token-by-user-id.js'; import createAuthTokenByUserId from '../../../../../helpers/create-auth-token-by-user-id.js';
import { createUser } from '../../../../../../test/factories/user.js'; import { createUser } from '../../../../../../test/factories/user.js';
import getAdminAppAuthClientMock from '../../../../../../test/mocks/rest/api/v1/admin/get-app-auth-client.js'; import getAdminAppAuthClientMock from '../../../../../../test/mocks/rest/api/v1/admin/app-auth-clients/get-app-auth-client.js';
import { createAppAuthClient } from '../../../../../../test/factories/app-auth-client.js'; import { createAppAuthClient } from '../../../../../../test/factories/app-auth-client.js';
import { createRole } from '../../../../../../test/factories/role.js'; import { createRole } from '../../../../../../test/factories/role.js';
import * as license from '../../../../../helpers/license.ee.js'; import * as license from '../../../../../helpers/license.ee.js';

View File

@@ -0,0 +1,11 @@
import { renderObject } from '../../../../../helpers/renderer.js';
import AppAuthClient from '../../../../../models/app-auth-client.js';
export default async (request, response) => {
const appAuthClients = await AppAuthClient.query().orderBy(
'created_at',
'desc'
);
renderObject(response, appAuthClients);
};

View File

@@ -0,0 +1,41 @@
import { vi, 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 getAdminAppAuthClientsMock from '../../../../../../test/mocks/rest/api/v1/admin/app-auth-clients/get-app-auth-clients.js';
import { createAppAuthClient } from '../../../../../../test/factories/app-auth-client.js';
import { createRole } from '../../../../../../test/factories/role.js';
import * as license from '../../../../../helpers/license.ee.js';
describe('GET /api/v1/admin/app-auth-clients', () => {
let currentUser, currentUserRole, token;
describe('with valid license key', () => {
beforeEach(async () => {
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
currentUserRole = await createRole({ key: 'admin' });
currentUser = await createUser({ roleId: currentUserRole.id });
token = createAuthTokenByUserId(currentUser.id);
});
it('should return app auth clients', async () => {
const appAuthClientOne = await createAppAuthClient();
const appAuthClientTwo = await createAppAuthClient();
const response = await request(app)
.get('/api/v1/admin/app-auth-clients')
.set('Authorization', token)
.expect(200);
const expectedPayload = getAdminAppAuthClientsMock([
appAuthClientTwo,
appAuthClientOne,
]);
expect(response.body).toEqual(expectedPayload);
});
});
});

View File

@@ -4,7 +4,7 @@ import Crypto from 'crypto';
import app from '../../../../app.js'; import app from '../../../../app.js';
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id.js'; import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id.js';
import { createUser } from '../../../../../test/factories/user.js'; import { createUser } from '../../../../../test/factories/user.js';
import getAppAuthClientMock from '../../../../../test/mocks/rest/api/v1/admin/get-app-auth-client.js'; import getAppAuthClientMock from '../../../../../test/mocks/rest/api/v1/app-auth-clients/get-app-auth-client.js';
import { createAppAuthClient } from '../../../../../test/factories/app-auth-client.js'; import { createAppAuthClient } from '../../../../../test/factories/app-auth-client.js';
import * as license from '../../../../helpers/license.ee.js'; import * as license from '../../../../helpers/license.ee.js';

View File

@@ -0,0 +1,10 @@
import { renderObject } from '../../../../helpers/renderer.js';
import AppAuthClient from '../../../../models/app-auth-client.js';
export default async (request, response) => {
const appAuthClients = await AppAuthClient.query()
.where({ active: true })
.orderBy('created_at', 'desc');
renderObject(response, appAuthClients);
};

View File

@@ -0,0 +1,37 @@
import { vi, 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 getAppAuthClientsMock from '../../../../../test/mocks/rest/api/v1/app-auth-clients/get-app-auth-clients.js';
import { createAppAuthClient } from '../../../../../test/factories/app-auth-client.js';
import * as license from '../../../../helpers/license.ee.js';
describe('GET /api/v1/app-auth-clients', () => {
let currentUser, token;
beforeEach(async () => {
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
currentUser = await createUser();
token = createAuthTokenByUserId(currentUser.id);
});
it('should return specified app auth client info', async () => {
const appAuthClientOne = await createAppAuthClient();
const appAuthClientTwo = await createAppAuthClient();
const response = await request(app)
.get('/api/v1/app-auth-clients')
.set('Authorization', token)
.expect(200);
const expectedPayload = getAppAuthClientsMock([
appAuthClientTwo,
appAuthClientOne,
]);
expect(response.body).toEqual(expectedPayload);
});
});

View File

@@ -42,9 +42,12 @@ describe('GET /api/v1/executions', () => {
const currentUserExecutionTwo = await createExecution({ const currentUserExecutionTwo = await createExecution({
flowId: currentUserFlow.id, flowId: currentUserFlow.id,
deletedAt: new Date().toISOString(),
}); });
await currentUserExecutionTwo
.$query()
.patchAndFetch({ deletedAt: new Date().toISOString() });
await createPermission({ await createPermission({
action: 'read', action: 'read',
subject: 'Execution', subject: 'Execution',
@@ -87,9 +90,12 @@ describe('GET /api/v1/executions', () => {
const anotherUserExecutionTwo = await createExecution({ const anotherUserExecutionTwo = await createExecution({
flowId: anotherUserFlow.id, flowId: anotherUserFlow.id,
deletedAt: new Date().toISOString(),
}); });
await anotherUserExecutionTwo
.$query()
.patchAndFetch({ deletedAt: new Date().toISOString() });
await createPermission({ await createPermission({
action: 'read', action: 'read',
subject: 'Execution', subject: 'Execution',

View File

@@ -0,0 +1,17 @@
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')
.first()
.throwIfNotFound();
const dynamicFields = await step.createDynamicFields(
request.body.dynamicFieldsKey,
request.body.parameters
);
renderObject(response, dynamicFields);
};

View File

@@ -0,0 +1,169 @@
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';
import { createUser } from '../../../../../test/factories/user';
import { createFlow } from '../../../../../test/factories/flow';
import { createStep } from '../../../../../test/factories/step';
import { createPermission } from '../../../../../test/factories/permission';
import createDynamicFieldsMock from '../../../../../test/mocks/rest/api/v1/steps/create-dynamic-fields';
describe('POST /api/v1/steps/:stepId/dynamic-fields', () => {
let currentUser, currentUserRole, token;
beforeEach(async () => {
currentUser = await createUser();
currentUserRole = await currentUser.$relatedQuery('role');
token = createAuthTokenByUserId(currentUser.id);
});
it('should return dynamically created fields of the current users step', async () => {
const currentUserflow = await createFlow({ userId: currentUser.id });
const actionStep = await createStep({
flowId: currentUserflow.id,
type: 'action',
appKey: 'slack',
key: 'sendMessageToChannel',
});
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-fields`)
.set('Authorization', token)
.send({
dynamicFieldsKey: 'listFieldsAfterSendAsBot',
parameters: {
sendAsBot: true,
},
})
.expect(200);
const expectedPayload = await createDynamicFieldsMock();
expect(response.body).toEqual(expectedPayload);
});
it('should return dynamically created fields of the another users step', async () => {
const anotherUser = await createUser();
const anotherUserflow = await createFlow({ userId: anotherUser.id });
const actionStep = await createStep({
flowId: anotherUserflow.id,
type: 'action',
appKey: 'slack',
key: 'sendMessageToChannel',
});
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-fields`)
.set('Authorization', token)
.send({
dynamicFieldsKey: 'listFieldsAfterSendAsBot',
parameters: {
sendAsBot: true,
},
})
.expect(200);
const expectedPayload = await createDynamicFieldsMock();
expect(response.body).toEqual(expectedPayload);
});
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-fields`)
.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-fields`)
.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,27 @@
import { ref } from 'objection';
import ExecutionStep from '../../../../models/execution-step.js';
import { renderObject } from '../../../../helpers/renderer.js';
export default async (request, response) => {
const step = await request.currentUser.authorizedSteps
.clone()
.findOne({ 'steps.id': request.params.stepId })
.throwIfNotFound();
const previousSteps = await request.currentUser.authorizedSteps
.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');
renderObject(response, previousSteps);
};

View File

@@ -0,0 +1,173 @@
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';
import { createUser } from '../../../../../test/factories/user';
import { createFlow } from '../../../../../test/factories/flow';
import { createStep } from '../../../../../test/factories/step';
import { createExecutionStep } from '../../../../../test/factories/execution-step.js';
import { createPermission } from '../../../../../test/factories/permission';
import getPreviousStepsMock from '../../../../../test/mocks/rest/api/v1/steps/get-previous-steps';
describe('GET /api/v1/steps/:stepId/previous-steps', () => {
let currentUser, currentUserRole, token;
beforeEach(async () => {
currentUser = await createUser();
currentUserRole = await currentUser.$relatedQuery('role');
token = createAuthTokenByUserId(currentUser.id);
});
it('should return the previous steps of the specified step of the current user', async () => {
const currentUserflow = await createFlow({ userId: currentUser.id });
const triggerStep = await createStep({
flowId: currentUserflow.id,
type: 'trigger',
});
const actionStepOne = await createStep({
flowId: currentUserflow.id,
type: 'action',
});
const actionStepTwo = await createStep({
flowId: currentUserflow.id,
type: 'action',
});
const executionStepOne = await createExecutionStep({
stepId: triggerStep.id,
});
const executionStepTwo = await createExecutionStep({
stepId: actionStepOne.id,
});
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)
.get(`/api/v1/steps/${actionStepTwo.id}/previous-steps`)
.set('Authorization', token)
.expect(200);
const expectedPayload = await getPreviousStepsMock(
[triggerStep, actionStepOne],
[executionStepOne, executionStepTwo]
);
expect(response.body).toEqual(expectedPayload);
});
it('should return the previous steps of the specified step of another user', async () => {
const anotherUser = await createUser();
const anotherUserFlow = await createFlow({ userId: anotherUser.id });
const triggerStep = await createStep({
flowId: anotherUserFlow.id,
type: 'trigger',
});
const actionStepOne = await createStep({
flowId: anotherUserFlow.id,
type: 'action',
});
const actionStepTwo = await createStep({
flowId: anotherUserFlow.id,
type: 'action',
});
const executionStepOne = await createExecutionStep({
stepId: triggerStep.id,
});
const executionStepTwo = await createExecutionStep({
stepId: actionStepOne.id,
});
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)
.get(`/api/v1/steps/${actionStepTwo.id}/previous-steps`)
.set('Authorization', token)
.expect(200);
const expectedPayload = await getPreviousStepsMock(
[triggerStep, actionStepOne],
[executionStepOne, executionStepTwo]
);
expect(response.body).toEqual(expectedPayload);
});
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 notExistingFlowUUID = Crypto.randomUUID();
await request(app)
.get(`/api/v1/steps/${notExistingFlowUUID}/previous-steps`)
.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)
.get('/api/v1/steps/invalidFlowUUID/previous-steps')
.set('Authorization', token)
.expect(400);
});
});

View File

@@ -26,6 +26,4 @@ export default async (request, response) => {
} }
await handlerSync(flowId, request, response); await handlerSync(flowId, request, response);
response.sendStatus(204);
}; };

View File

@@ -1,32 +0,0 @@
import appConfig from '../../config/app.js';
import { hasValidLicense } from '../../helpers/license.ee.js';
import Config from '../../models/config.js';
const getConfig = async (_parent, params) => {
if (!(await hasValidLicense())) return {};
const defaultConfig = {
disableNotificationsPage: appConfig.disableNotificationsPage,
disableFavicon: appConfig.disableFavicon,
additionalDrawerLink: appConfig.additionalDrawerLink,
additionalDrawerLinkText: appConfig.additionalDrawerLinkText,
};
const configQuery = Config.query();
if (Array.isArray(params.keys)) {
configQuery.whereIn('key', params.keys);
}
const config = await configQuery.orderBy('key', 'asc');
return config.reduce((computedConfig, configEntry) => {
const { key, value } = configEntry;
computedConfig[key] = value?.data;
return computedConfig;
}, defaultConfig);
};
export default getConfig;

View File

@@ -1,140 +0,0 @@
import { vi, describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import app from '../../app';
import { createConfig } from '../../../test/factories/config';
import appConfig from '../../config/app';
import * as license from '../../helpers/license.ee';
describe('graphQL getConfig query', () => {
let configOne, configTwo, configThree, query;
beforeEach(async () => {
configOne = await createConfig({ key: 'configOne' });
configTwo = await createConfig({ key: 'configTwo' });
configThree = await createConfig({ key: 'configThree' });
query = `
query {
getConfig
}
`;
});
describe('and without valid license', () => {
beforeEach(async () => {
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(false);
});
describe('and correct permissions', () => {
it('should return empty config data', async () => {
const response = await request(app)
.post('/graphql')
.send({ query })
.expect(200);
const expectedResponsePayload = { data: { getConfig: {} } };
expect(response.body).toEqual(expectedResponsePayload);
});
});
});
describe('and with valid license', () => {
beforeEach(async () => {
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
});
describe('and without providing specific keys', () => {
it('should return all config data', async () => {
const response = await request(app)
.post('/graphql')
.send({ query })
.expect(200);
const expectedResponsePayload = {
data: {
getConfig: {
[configOne.key]: configOne.value.data,
[configTwo.key]: configTwo.value.data,
[configThree.key]: configThree.value.data,
disableNotificationsPage: false,
disableFavicon: false,
additionalDrawerLink: undefined,
additionalDrawerLinkText: undefined,
},
},
};
expect(response.body).toEqual(expectedResponsePayload);
});
});
describe('and with providing specific keys', () => {
it('should return all config data', async () => {
query = `
query {
getConfig(keys: ["configOne", "configTwo"])
}
`;
const response = await request(app)
.post('/graphql')
.send({ query })
.expect(200);
const expectedResponsePayload = {
data: {
getConfig: {
[configOne.key]: configOne.value.data,
[configTwo.key]: configTwo.value.data,
disableNotificationsPage: false,
disableFavicon: false,
additionalDrawerLink: undefined,
additionalDrawerLinkText: undefined,
},
},
};
expect(response.body).toEqual(expectedResponsePayload);
});
});
describe('and with different defaults', () => {
beforeEach(async () => {
vi.spyOn(appConfig, 'disableNotificationsPage', 'get').mockReturnValue(
true
);
vi.spyOn(appConfig, 'disableFavicon', 'get').mockReturnValue(true);
vi.spyOn(appConfig, 'additionalDrawerLink', 'get').mockReturnValue(
'https://automatisch.io'
);
vi.spyOn(appConfig, 'additionalDrawerLinkText', 'get').mockReturnValue(
'Automatisch'
);
});
it('should return custom config', async () => {
const response = await request(app)
.post('/graphql')
.send({ query })
.expect(200);
const expectedResponsePayload = {
data: {
getConfig: {
[configOne.key]: configOne.value.data,
[configTwo.key]: configTwo.value.data,
[configThree.key]: configThree.value.data,
disableNotificationsPage: true,
disableFavicon: true,
additionalDrawerLink: 'https://automatisch.io',
additionalDrawerLinkText: 'Automatisch',
},
},
};
expect(response.body).toEqual(expectedResponsePayload);
});
});
});
});

View File

@@ -136,7 +136,7 @@ describe('graphQL getFlow query', () => {
id: actionStep.id, id: actionStep.id,
key: 'translateText', key: 'translateText',
parameters: {}, parameters: {},
position: 1, position: 2,
status: actionStep.status, status: actionStep.status,
type: 'action', type: 'action',
webhookUrl: 'http://localhost:3000/null', webhookUrl: 'http://localhost:3000/null',
@@ -223,7 +223,7 @@ describe('graphQL getFlow query', () => {
id: actionStep.id, id: actionStep.id,
key: 'translateText', key: 'translateText',
parameters: {}, parameters: {},
position: 1, position: 2,
status: actionStep.status, status: actionStep.status,
type: 'action', type: 'action',
webhookUrl: 'http://localhost:3000/null', webhookUrl: 'http://localhost:3000/null',

View File

@@ -1,40 +0,0 @@
import Flow from '../../models/flow.js';
import paginate from '../../helpers/pagination.js';
const getFlows = 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 flowsQuery = baseQuery
.clone()
.joinRelated({
steps: true,
})
.withGraphFetched({
steps: {
connection: true,
},
})
.where((builder) => {
if (params.connectionId) {
builder.where('steps.connection_id', params.connectionId);
}
if (params.name) {
builder.where('flows.name', 'ilike', `%${params.name}%`);
}
if (params.appKey) {
builder.where('steps.app_key', params.appKey);
}
})
.groupBy('flows.id')
.orderBy('active', 'desc')
.orderBy('updated_at', 'desc');
return paginate(flowsQuery, params.limit, params.offset);
};
export default getFlows;

View File

@@ -1,16 +0,0 @@
import axios from '../../helpers/axios-with-proxy.js';
const NOTIFICATIONS_URL =
'https://notifications.automatisch.io/notifications.json';
const getNotifications = async () => {
try {
const { data: notifications = [] } = await axios.get(NOTIFICATIONS_URL);
return notifications;
} catch (err) {
return [];
}
};
export default getNotifications;

View File

@@ -1,19 +0,0 @@
import paginate from '../../helpers/pagination.js';
import User from '../../models/user.js';
const getUsers = async (_parent, params, context) => {
context.currentUser.can('read', 'User');
const usersQuery = User.query()
.leftJoinRelated({
role: true,
})
.withGraphFetched({
role: true,
})
.orderBy('full_name', 'asc');
return paginate(usersQuery, params.limit, params.offset);
};
export default getUsers;

View File

@@ -1,148 +0,0 @@
import { describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import app from '../../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';
describe('graphQL getUsers query', () => {
const query = `
query {
getUsers(limit: 10, offset: 0) {
pageInfo {
currentPage
totalPages
}
totalCount
edges {
node {
id
fullName
email
role {
id
name
}
}
}
}
}
`;
describe('and without permissions', () => {
it('should throw not authorized error', async () => {
const userWithoutPermissions = await createUser();
const token = createAuthTokenByUserId(userWithoutPermissions.id);
const response = await request(app)
.post('/graphql')
.set('Authorization', token)
.send({ query })
.expect(200);
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toEqual('Not authorized!');
});
});
describe('and with correct permissions', () => {
let role, currentUser, anotherUser, token, requestObject;
beforeEach(async () => {
role = await createRole({
key: 'sample',
name: 'sample',
});
await createPermission({
action: 'read',
subject: 'User',
roleId: role.id,
});
currentUser = await createUser({
roleId: role.id,
fullName: 'Current User',
});
anotherUser = await createUser({
roleId: role.id,
fullName: 'Another User',
});
token = createAuthTokenByUserId(currentUser.id);
requestObject = request(app).post('/graphql').set('Authorization', token);
});
it('should return users data', async () => {
const response = await requestObject.send({ query }).expect(200);
const expectedResponsePayload = {
data: {
getUsers: {
edges: [
{
node: {
email: anotherUser.email,
fullName: anotherUser.fullName,
id: anotherUser.id,
role: {
id: role.id,
name: role.name,
},
},
},
{
node: {
email: currentUser.email,
fullName: currentUser.fullName,
id: currentUser.id,
role: {
id: role.id,
name: role.name,
},
},
},
],
pageInfo: {
currentPage: 1,
totalPages: 1,
},
totalCount: 2,
},
},
};
expect(response.body).toEqual(expectedResponsePayload);
});
it('should not return users data with password', async () => {
const query = `
query {
getUsers(limit: 10, offset: 0) {
pageInfo {
currentPage
totalPages
}
totalCount
edges {
node {
id
fullName
password
}
}
}
}
`;
const response = await requestObject.send({ query }).expect(400);
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toEqual(
'Cannot query field "password" on type "User".'
);
});
});
});

View File

@@ -2,15 +2,11 @@ import getApp from './queries/get-app.js';
import getAppAuthClient from './queries/get-app-auth-client.ee.js'; import getAppAuthClient from './queries/get-app-auth-client.ee.js';
import getAppAuthClients from './queries/get-app-auth-clients.ee.js'; import getAppAuthClients from './queries/get-app-auth-clients.ee.js';
import getBillingAndUsage from './queries/get-billing-and-usage.ee.js'; import getBillingAndUsage from './queries/get-billing-and-usage.ee.js';
import getConfig from './queries/get-config.ee.js';
import getConnectedApps from './queries/get-connected-apps.js'; import getConnectedApps from './queries/get-connected-apps.js';
import getDynamicData from './queries/get-dynamic-data.js'; import getDynamicData from './queries/get-dynamic-data.js';
import getDynamicFields from './queries/get-dynamic-fields.js'; import getDynamicFields from './queries/get-dynamic-fields.js';
import getFlow from './queries/get-flow.js'; import getFlow from './queries/get-flow.js';
import getFlows from './queries/get-flows.js';
import getNotifications from './queries/get-notifications.js';
import getStepWithTestExecutions from './queries/get-step-with-test-executions.js'; import getStepWithTestExecutions from './queries/get-step-with-test-executions.js';
import getUsers from './queries/get-users.js';
import testConnection from './queries/test-connection.js'; import testConnection from './queries/test-connection.js';
const queryResolvers = { const queryResolvers = {
@@ -18,15 +14,11 @@ const queryResolvers = {
getAppAuthClient, getAppAuthClient,
getAppAuthClients, getAppAuthClients,
getBillingAndUsage, getBillingAndUsage,
getConfig,
getConnectedApps, getConnectedApps,
getDynamicData, getDynamicData,
getDynamicFields, getDynamicFields,
getFlow, getFlow,
getFlows,
getNotifications,
getStepWithTestExecutions, getStepWithTestExecutions,
getUsers,
testConnection, testConnection,
}; };

View File

@@ -5,13 +5,6 @@ type Query {
getConnectedApps(name: String): [App] getConnectedApps(name: String): [App]
testConnection(id: String!): Connection testConnection(id: String!): Connection
getFlow(id: String!): Flow getFlow(id: String!): Flow
getFlows(
limit: Int!
offset: Int!
appKey: String
connectionId: String
name: String
): FlowConnection
getStepWithTestExecutions(stepId: String!): [Step] getStepWithTestExecutions(stepId: String!): [Step]
getDynamicData( getDynamicData(
stepId: String! stepId: String!
@@ -24,9 +17,6 @@ type Query {
parameters: JSONObject parameters: JSONObject
): [SubstepArgument] ): [SubstepArgument]
getBillingAndUsage: GetBillingAndUsage getBillingAndUsage: GetBillingAndUsage
getConfig(keys: [String]): JSONObject
getNotifications: [Notification]
getUsers(limit: Int!, offset: Int!): UserConnection
} }
type Mutation { type Mutation {
@@ -257,15 +247,6 @@ type Field {
options: [SubstepArgumentOption] options: [SubstepArgumentOption]
} }
type FlowConnection {
edges: [FlowEdge]
pageInfo: PageInfo
}
type FlowEdge {
node: Flow
}
enum FlowStatus { enum FlowStatus {
paused paused
published published
@@ -304,16 +285,6 @@ type SamlAuthProvidersRoleMapping {
remoteRoleName: String remoteRoleName: String
} }
type UserConnection {
edges: [UserEdge]
pageInfo: PageInfo
totalCount: Int
}
type UserEdge {
node: User
}
input CreateConnectionInput { input CreateConnectionInput {
key: String! key: String!
appAuthClientId: String appAuthClientId: String
@@ -692,13 +663,6 @@ input UpdateAppAuthClientInput {
active: Boolean active: Boolean
} }
type Notification {
name: String
createdAt: String
documentationUrl: String
description: String
}
schema { schema {
query: Query query: Query
mutation: Mutation mutation: Mutation

View File

@@ -42,8 +42,6 @@ const isAuthenticatedRule = rule()(isAuthenticated);
export const authenticationRules = { export const authenticationRules = {
Query: { Query: {
'*': isAuthenticatedRule, '*': isAuthenticatedRule,
getConfig: allow,
getNotifications: allow,
}, },
Mutation: { Mutation: {
'*': isAuthenticatedRule, '*': isAuthenticatedRule,

View File

@@ -19,6 +19,14 @@ const authorizationList = {
action: 'read', action: 'read',
subject: 'Flow', subject: 'Flow',
}, },
'GET /api/v1/steps/:stepId/previous-steps': {
action: 'update',
subject: 'Flow',
},
'POST /api/v1/steps/:stepId/dynamic-fields': {
action: 'update',
subject: 'Flow',
},
'GET /api/v1/connections/:connectionId/flows': { 'GET /api/v1/connections/:connectionId/flows': {
action: 'read', action: 'read',
subject: 'Flow', subject: 'Flow',

View File

@@ -75,9 +75,20 @@ export default async (flowId, request, response) => {
}); });
if (actionStep.key === 'respondWith' && !response.headersSent) { if (actionStep.key === 'respondWith' && !response.headersSent) {
const { headers, statusCode, body } = actionExecutionStep.dataOut;
// we set the custom response headers
if (headers) {
for (const [key, value] of Object.entries(headers)) {
if (key) {
response.set(key, value);
}
}
}
// we send the response only if it's not sent yet. This allows us to early respond from the flow. // we send the response only if it's not sent yet. This allows us to early respond from the flow.
response.status(actionExecutionStep.dataOut.statusCode); response.status(statusCode);
response.send(actionExecutionStep.dataOut.body); response.send(body);
} }
} }
} }

View File

@@ -7,6 +7,7 @@ import Connection from './connection.js';
import ExecutionStep from './execution-step.js'; 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';
class Step extends Base { class Step extends Base {
static tableName = 'steps'; static tableName = 'steps';
@@ -196,6 +197,26 @@ class Step extends Base {
return existingArguments; return existingArguments;
} }
async createDynamicFields(dynamicFieldsKey, 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.dynamicFields.find(
(data) => data.key === dynamicFieldsKey
);
for (const parameterKey in parameters) {
const parameterValue = parameters[parameterKey];
$.step.parameters[parameterKey] = parameterValue;
}
const dynamicFields = (await command.run($)) || [];
return dynamicFields;
}
async updateWebhookUrl() { async updateWebhookUrl() {
if (this.isAction) return this; if (this.isAction) return this;

View File

@@ -3,16 +3,25 @@ import asyncHandler from 'express-async-handler';
import { authenticateUser } from '../../../../helpers/authentication.js'; import { authenticateUser } from '../../../../helpers/authentication.js';
import { authorizeAdmin } from '../../../../helpers/authorization.js'; import { authorizeAdmin } from '../../../../helpers/authorization.js';
import { checkIsEnterprise } from '../../../../helpers/check-is-enterprise.js'; import { checkIsEnterprise } from '../../../../helpers/check-is-enterprise.js';
import getAdminAppAuthClientsAction from '../../../../controllers/api/v1/admin/app-auth-clients/get-app-auth-client.js'; import getAdminAppAuthClientsAction from '../../../../controllers/api/v1/admin/app-auth-clients/get-app-auth-clients.ee.js';
import getAdminAppAuthClientAction from '../../../../controllers/api/v1/admin/app-auth-clients/get-app-auth-client.ee.js';
const router = Router(); const router = Router();
router.get( router.get(
'/:appAuthClientId', '/',
authenticateUser, authenticateUser,
authorizeAdmin, authorizeAdmin,
checkIsEnterprise, checkIsEnterprise,
asyncHandler(getAdminAppAuthClientsAction) asyncHandler(getAdminAppAuthClientsAction)
); );
router.get(
'/:appAuthClientId',
authenticateUser,
authorizeAdmin,
checkIsEnterprise,
asyncHandler(getAdminAppAuthClientAction)
);
export default router; export default router;

View File

@@ -3,9 +3,17 @@ import asyncHandler from 'express-async-handler';
import { authenticateUser } from '../../../helpers/authentication.js'; import { authenticateUser } from '../../../helpers/authentication.js';
import { checkIsEnterprise } from '../../../helpers/check-is-enterprise.js'; import { checkIsEnterprise } from '../../../helpers/check-is-enterprise.js';
import getAppAuthClientAction from '../../../controllers/api/v1/app-auth-clients/get-app-auth-client.js'; import getAppAuthClientAction from '../../../controllers/api/v1/app-auth-clients/get-app-auth-client.js';
import getAppAuthClientsAction from '../../../controllers/api/v1/app-auth-clients/get-app-auth-clients.js';
const router = Router(); const router = Router();
router.get(
'/',
authenticateUser,
checkIsEnterprise,
asyncHandler(getAppAuthClientsAction)
);
router.get( router.get(
'/:appAuthClientId', '/:appAuthClientId',
authenticateUser, authenticateUser,

View File

@@ -3,6 +3,8 @@ 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 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 createDynamicFieldsAction from '../../../controllers/api/v1/steps/create-dynamic-fields.js';
const router = Router(); const router = Router();
@@ -13,4 +15,18 @@ router.get(
asyncHandler(getConnectionAction) asyncHandler(getConnectionAction)
); );
router.get(
'/:stepId/previous-steps',
authenticateUser,
authorizeUser,
asyncHandler(getPreviousStepsAction)
);
router.post(
'/:stepId/dynamic-fields',
authenticateUser,
authorizeUser,
asyncHandler(createDynamicFieldsAction)
);
export default router; export default router;

View File

@@ -18,7 +18,7 @@ import adminSamlAuthProvidersRouter from './api/v1/admin/saml-auth-providers.ee.
import rolesRouter from './api/v1/admin/roles.ee.js'; import rolesRouter from './api/v1/admin/roles.ee.js';
import permissionsRouter from './api/v1/admin/permissions.ee.js'; import permissionsRouter from './api/v1/admin/permissions.ee.js';
import adminUsersRouter from './api/v1/admin/users.ee.js'; import adminUsersRouter from './api/v1/admin/users.ee.js';
import adminAppAuthClientsRouter from './api/v1/admin/app-auth-clients.js'; import adminAppAuthClientsRouter from './api/v1/admin/app-auth-clients.ee.js';
const router = Router(); const router = Router();

View File

@@ -1,5 +1,7 @@
import executionStepSerializer from './execution-step.js';
const stepSerializer = (step) => { const stepSerializer = (step) => {
return { let stepData = {
id: step.id, id: step.id,
type: step.type, type: step.type,
key: step.key, key: step.key,
@@ -10,6 +12,14 @@ const stepSerializer = (step) => {
position: step.position, position: step.position,
parameters: step.parameters, parameters: step.parameters,
}; };
if (step.executionSteps?.length > 0) {
stepData.executionSteps = step.executionSteps.map((executionStep) =>
executionStepSerializer(executionStep)
);
}
return stepData;
}; };
export default stepSerializer; export default stepSerializer;

View File

@@ -1,6 +1,8 @@
import { describe, it, expect, beforeEach } from 'vitest'; import { describe, it, expect, beforeEach } from 'vitest';
import { createStep } from '../../test/factories/step'; import { createStep } from '../../test/factories/step';
import { createExecutionStep } from '../../test/factories/execution-step';
import stepSerializer from './step'; import stepSerializer from './step';
import executionStepSerializer from './execution-step';
describe('stepSerializer', () => { describe('stepSerializer', () => {
let step; let step;
@@ -24,4 +26,20 @@ describe('stepSerializer', () => {
expect(stepSerializer(step)).toEqual(expectedPayload); expect(stepSerializer(step)).toEqual(expectedPayload);
}); });
it('should return step data with the execution steps', async () => {
const executionStepOne = await createExecutionStep({ stepId: step.id });
const executionStepTwo = await createExecutionStep({ stepId: step.id });
step.executionSteps = [executionStepOne, executionStepTwo];
const expectedPayload = {
executionSteps: [
executionStepSerializer(executionStepOne),
executionStepSerializer(executionStepTwo),
],
};
expect(stepSerializer(step)).toMatchObject(expectedPayload);
});
}); });

View File

@@ -17,9 +17,7 @@ export const createAppAuthClient = async (params = {}) => {
params.formattedAuthDefaults = params.formattedAuthDefaults =
params?.formattedAuthDefaults || formattedAuthDefaults; params?.formattedAuthDefaults || formattedAuthDefaults;
const appAuthClient = await AppAuthClient.query() const appAuthClient = await AppAuthClient.query().insertAndFetch(params);
.insert(params)
.returning('*');
return appAuthClient; return appAuthClient;
}; };

View File

@@ -1,9 +1,10 @@
import AppConfig from '../../src/models/app-config.js'; import AppConfig from '../../src/models/app-config.js';
import { faker } from '@faker-js/faker';
export const createAppConfig = async (params = {}) => { export const createAppConfig = async (params = {}) => {
params.key = params?.key || 'gitlab'; params.key = params?.key || faker.lorem.word();
const appConfig = await AppConfig.query().insert(params).returning('*'); const appConfig = await AppConfig.query().insertAndFetch(params);
return appConfig; return appConfig;
}; };

View File

@@ -7,7 +7,7 @@ export const createConfig = async (params = {}) => {
value: params?.value || { data: 'sampleConfig' }, value: params?.value || { data: 'sampleConfig' },
}; };
const config = await Config.query().insert(configData).returning('*'); const config = await Config.query().insertAndFetch(configData);
return config; return config;
}; };

View File

@@ -9,9 +9,7 @@ export const createExecutionStep = async (params = {}) => {
params.dataIn = params?.dataIn || { dataIn: 'dataIn' }; params.dataIn = params?.dataIn || { dataIn: 'dataIn' };
params.dataOut = params?.dataOut || { dataOut: 'dataOut' }; params.dataOut = params?.dataOut || { dataOut: 'dataOut' };
const executionStep = await ExecutionStep.query() const executionStep = await ExecutionStep.query().insertAndFetch(params);
.insert(params)
.returning('*');
return executionStep; return executionStep;
}; };

View File

@@ -4,10 +4,8 @@ import { createFlow } from './flow';
export const createExecution = async (params = {}) => { export const createExecution = async (params = {}) => {
params.flowId = params?.flowId || (await createFlow()).id; params.flowId = params?.flowId || (await createFlow()).id;
params.testRun = params?.testRun || false; params.testRun = params?.testRun || false;
params.createdAt = params?.createdAt || new Date().toISOString();
params.updatedAt = params?.updatedAt || new Date().toISOString();
const execution = await Execution.query().insert(params).returning('*'); const execution = await Execution.query().insertAndFetch(params);
return execution; return execution;
}; };

View File

@@ -7,7 +7,7 @@ export const createFlow = async (params = {}) => {
params.createdAt = params?.createdAt || new Date().toISOString(); params.createdAt = params?.createdAt || new Date().toISOString();
params.updatedAt = params?.updatedAt || new Date().toISOString(); params.updatedAt = params?.updatedAt || new Date().toISOString();
const flow = await Flow.query().insert(params).returning('*'); const flow = await Flow.query().insertAndFetch(params);
return flow; return flow;
}; };

View File

@@ -7,7 +7,7 @@ export const createPermission = async (params = {}) => {
params.subject = params?.subject || 'User'; params.subject = params?.subject || 'User';
params.conditions = params?.conditions || ['isCreator']; params.conditions = params?.conditions || ['isCreator'];
const permission = await Permission.query().insert(params).returning('*'); const permission = await Permission.query().insertAndFetch(params);
return permission; return permission;
}; };

View File

@@ -4,7 +4,7 @@ export const createRole = async (params = {}) => {
params.name = params?.name || 'Viewer'; params.name = params?.name || 'Viewer';
params.key = params?.key || 'viewer'; params.key = params?.key || 'viewer';
const role = await Role.query().insert(params).returning('*'); const role = await Role.query().insertAndFetch(params);
return role; return role;
}; };

View File

@@ -25,9 +25,9 @@ export const createSamlAuthProvider = async (params = {}) => {
params.defaultRoleId = params?.defaultRoleId || (await createRole()).id; params.defaultRoleId = params?.defaultRoleId || (await createRole()).id;
params.active = params?.active || true; params.active = params?.active || true;
const samlAuthProvider = await SamlAuthProvider.query() const samlAuthProvider = await SamlAuthProvider.query().insertAndFetch(
.insert(params) params
.returning('*'); );
return samlAuthProvider; return samlAuthProvider;
}; };

View File

@@ -5,19 +5,21 @@ export const createStep = async (params = {}) => {
params.flowId = params?.flowId || (await createFlow()).id; params.flowId = params?.flowId || (await createFlow()).id;
params.type = params?.type || 'action'; params.type = params?.type || 'action';
const lastStep = await global.knex const lastStep = await Step.query()
.table('steps') .where('flow_id', params.flowId)
.where('flowId', params.flowId) .andWhere('deleted_at', null)
.andWhere('deletedAt', '!=', null) .orderBy('position', 'desc')
.orderBy('createdAt', 'desc') .limit(1)
.first(); .first();
params.position = params?.position || (lastStep?.position || 0) + 1; params.position =
params?.position || (lastStep?.position ? lastStep.position + 1 : 1);
params.status = params?.status || 'completed'; params.status = params?.status || 'completed';
params.appKey = params.appKey =
params?.appKey || (params.type === 'action' ? 'deepl' : 'webhook'); params?.appKey || (params.type === 'action' ? 'deepl' : 'webhook');
const step = await Step.query().insert(params).returning('*'); const step = await Step.query().insertAndFetch(params);
return step; return step;
}; };

View File

@@ -15,7 +15,7 @@ export const createSubscription = async (params = {}) => {
params.nextBillDate = params.nextBillDate =
params?.nextBillDate || DateTime.now().plus({ days: 30 }).toISODate(); params?.nextBillDate || DateTime.now().plus({ days: 30 }).toISODate();
const subscription = await Subscription.query().insert(params).returning('*'); const subscription = await Subscription.query().insertAndFetch(params);
return subscription; return subscription;
}; };

View File

@@ -8,7 +8,7 @@ export const createUser = async (params = {}) => {
params.email = params?.email || faker.internet.email(); params.email = params?.email || faker.internet.email();
params.password = params?.password || faker.internet.password(); params.password = params?.password || faker.internet.password();
const user = await User.query().insert(params).returning('*'); const user = await User.query().insertAndFetch(params);
return user; return user;
}; };

View File

@@ -0,0 +1,19 @@
const getAdminAppAuthClientsMock = (appAuthClients) => {
return {
data: appAuthClients.map((appAuthClient) => ({
appConfigId: appAuthClient.appConfigId,
name: appAuthClient.name,
id: appAuthClient.id,
active: appAuthClient.active,
})),
meta: {
count: appAuthClients.length,
currentPage: null,
isArray: true,
totalPages: null,
type: 'AppAuthClient',
},
};
};
export default getAdminAppAuthClientsMock;

View File

@@ -0,0 +1,19 @@
const getAppAuthClientsMock = (appAuthClients) => {
return {
data: appAuthClients.map((appAuthClient) => ({
appConfigId: appAuthClient.appConfigId,
name: appAuthClient.name,
id: appAuthClient.id,
active: appAuthClient.active,
})),
meta: {
count: appAuthClients.length,
currentPage: null,
isArray: true,
totalPages: null,
type: 'AppAuthClient',
},
};
};
export default getAppAuthClientsMock;

View File

@@ -0,0 +1,36 @@
const createDynamicFieldsMock = async () => {
const data = [
{
label: 'Bot name',
key: 'botName',
type: 'string',
required: true,
value: 'Automatisch',
description:
'Specify the bot name which appears as a bold username above the message inside Slack. Defaults to Automatisch.',
variables: true,
},
{
label: 'Bot icon',
key: 'botIcon',
type: 'string',
required: false,
description:
'Either an image url or an emoji available to your team (surrounded by :). For example, https://example.com/icon_256.png or :robot_face:',
variables: true,
},
];
return {
data: data,
meta: {
count: data.length,
currentPage: null,
isArray: true,
totalPages: null,
type: 'Object',
},
};
};
export default createDynamicFieldsMock;

View File

@@ -0,0 +1,41 @@
const getPreviousStepsMock = async (steps, executionSteps) => {
const data = steps.map((step) => {
const filteredExecutionSteps = executionSteps.filter(
(executionStep) => executionStep.stepId === step.id
);
return {
id: step.id,
type: step.type,
key: step.key,
appKey: step.appKey,
iconUrl: step.iconUrl,
webhookUrl: step.webhookUrl,
status: step.status,
position: step.position,
parameters: step.parameters,
executionSteps: filteredExecutionSteps.map((executionStep) => ({
id: executionStep.id,
dataIn: executionStep.dataIn,
dataOut: executionStep.dataOut,
errorDetails: executionStep.errorDetails,
status: executionStep.status,
createdAt: executionStep.createdAt.getTime(),
updatedAt: executionStep.updatedAt.getTime(),
})),
};
});
return {
data: data,
meta: {
count: data.length,
currentPage: null,
isArray: true,
totalPages: null,
type: 'Step',
},
};
};
export default getPreviousStepsMock;

View File

@@ -113,6 +113,12 @@ export default defineConfig({
{ text: 'Connection', link: '/apps/formatter/connection' }, { text: 'Connection', link: '/apps/formatter/connection' },
], ],
}, },
{
text: 'FTP',
collapsible: true,
collapsed: true,
items: [{ text: 'Connection', link: '/apps/ftp/connection' }],
},
{ {
text: 'Ghost', text: 'Ghost',
collapsible: true, collapsible: true,

View File

@@ -0,0 +1,16 @@
# FTP
:::info
This page explains the steps you need to follow to set up the FTP connection in Automatisch. If any of the steps are outdated, please let us know!
:::
FTP, which stands for File Transfer Protocol, is a standard network protocol used for the transfer of computer files between a client and server on a computer network. t's commonly used to download files from a server or upload files to a server.. You need to provide the following information from Automatisch by using FTP connection.
1. Write any screen name to be displayed in Automatisch.
1. Fill host address field with the FTP host address.
1. Fill username field with the FTP username.
1. Fill password field with the FTP password.
1. Select the Secure field.
1. Fill port field with the FTP port. Default is 21.
1. Click **Submit** button on Automatisch.
1. Now, you can start using the FTP connection with Automatisch.

View File

@@ -0,0 +1 @@
<?xml version="1.0" encoding="utf-8"?><svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 122.88 109.36" style="enable-background:new 0 0 122.88 109.36" xml:space="preserve"><g><path d="M14.69,16.44h3.56V8.63c0-1.09,0.88-1.97,1.97-1.97h3.5V1.97C23.72,0.88,24.6,0,25.69,0h31.92c1.09,0,1.97,0.88,1.97,1.97 v4.69h43.05c1.09,0,1.97,0.88,1.97,1.97v7.82h3.59c1.09,0,1.97,0.88,1.97,1.97c0,0.14-0.01,0.28-0.04,0.42l-6.88,50.59 c-0.22,1.65-0.95,3.18-2.05,4.29c-1.1,1.1-2.54,1.78-4.25,1.78H70.95v5.86h2.12c1.54,0,2.8,1.26,2.8,2.8v5.06h10.44 c1.25,0,2.32,0.83,2.67,1.97h26.1c3.73,0,6.14,2.07,7.2,4.71c0.4,1,0.6,2.08,0.6,3.14c0,1.06-0.2,2.14-0.6,3.14 c-1.06,2.64-3.47,4.71-7.2,4.71H89.09c-0.15,1.4-1.35,2.49-2.78,2.49H72.63H38.8c-1.44,0-2.63-1.1-2.78-2.49H7.8 c-3.73,0-6.14-2.07-7.2-4.71c-0.4-1-0.6-2.08-0.6-3.14c0-1.06,0.2-2.14,0.6-3.14c1.06-2.64,3.47-4.71,7.2-4.71h28.32 c0.36-1.14,1.42-1.97,2.67-1.97h8.74v-5.06c0-1.54,1.26-2.8,2.8-2.8h1.69v-5.86H24.38c-1.71,0-3.19-0.69-4.3-1.79 c-1.11-1.11-1.83-2.66-2.01-4.33l-5.33-50.74c-0.11-1.08,0.67-2.04,1.75-2.15C14.56,16.45,14.63,16.45,14.69,16.44L14.69,16.44 L14.69,16.44L14.69,16.44z M31.38,34.43h16.29v4.58H38v3.72h8.26v4.3H38v8.71h-6.61V34.43L31.38,34.43z M50.08,34.43H70.1v5.27 h-6.72v16.05h-6.59V39.7h-6.71V34.43L50.08,34.43z M73.41,34.43h10.95c2.38,0,4.17,0.57,5.35,1.7c1.19,1.14,1.78,2.75,1.78,4.84 c0,2.15-0.65,3.84-1.94,5.05c-1.3,1.21-3.27,1.82-5.93,1.82h-3.6v7.91h-6.61V34.43L73.41,34.43z M80.03,43.52h1.61 c1.27,0,2.16-0.22,2.68-0.66c0.51-0.44,0.77-1,0.77-1.69c0-0.67-0.22-1.24-0.67-1.7c-0.44-0.47-1.28-0.7-2.51-0.7h-1.88V43.52 L80.03,43.52z M56.38,81.34h10.21v-5.79H56.38V81.34L56.38,81.34z M89.11,94.88v8.27h25.97c1.97,0,3.22-1.04,3.76-2.37 c0.22-0.54,0.33-1.15,0.33-1.76s-0.11-1.22-0.33-1.76c-0.54-1.33-1.79-2.37-3.76-2.37H89.11L89.11,94.88z M36,103.15v-8.27H7.8 c-1.97,0-3.22,1.04-3.76,2.37c-0.22,0.54-0.33,1.15-0.33,1.76s0.11,1.22,0.33,1.76c0.54,1.33,1.79,2.37,3.76,2.37H36L36,103.15z M20.23,20.38h-3.35l5.1,48.57c0.08,0.78,0.39,1.47,0.87,1.95c0.4,0.4,0.92,0.64,1.53,0.64h72.54c0.59,0,1.1-0.24,1.48-0.62 c0.49-0.49,0.82-1.22,0.93-2.03l6.6-48.51L20.23,20.38L20.23,20.38L20.23,20.38z M22.19,10.6v5.83l78.46-0.83v-5H57.61 c-1.09,0-1.97-0.88-1.97-1.97V3.94H27.66v4.69c0,1.09-0.88,1.97-1.97,1.97L22.19,10.6L22.19,10.6L22.19,10.6z"/></g></svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -1,35 +1,39 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useQuery } from '@apollo/client';
import { Link, useSearchParams } from 'react-router-dom'; import { Link, useSearchParams } from 'react-router-dom';
import { GET_FLOWS } from 'graphql/queries/get-flows';
import Pagination from '@mui/material/Pagination'; import Pagination from '@mui/material/Pagination';
import PaginationItem from '@mui/material/PaginationItem'; import PaginationItem from '@mui/material/PaginationItem';
import * as URLS from 'config/urls'; import * as URLS from 'config/urls';
import AppFlowRow from 'components/FlowRow'; import AppFlowRow from 'components/FlowRow';
import NoResultFound from 'components/NoResultFound'; import NoResultFound from 'components/NoResultFound';
import useFormatMessage from 'hooks/useFormatMessage'; import useFormatMessage from 'hooks/useFormatMessage';
const FLOW_PER_PAGE = 10; import useConnectionFlows from 'hooks/useConnectionFlows';
const getLimitAndOffset = (page) => ({ import useAppFlows from 'hooks/useAppFlows';
limit: FLOW_PER_PAGE,
offset: (page - 1) * FLOW_PER_PAGE,
});
function AppFlows(props) { function AppFlows(props) {
const { appKey } = props; const { appKey } = props;
const formatMessage = useFormatMessage(); const formatMessage = useFormatMessage();
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
const connectionId = searchParams.get('connectionId') || undefined; const connectionId = searchParams.get('connectionId') || undefined;
const page = parseInt(searchParams.get('page') || '', 10) || 1; const page = parseInt(searchParams.get('page') || '', 10) || 1;
const { data } = useQuery(GET_FLOWS, { const isConnectionFlowEnabled = !!connectionId;
variables: { const isAppFlowEnabled = !!appKey && !connectionId;
appKey,
connectionId, const connectionFlows = useConnectionFlows(
...getLimitAndOffset(page), { connectionId, page },
}, { enabled: isConnectionFlowEnabled },
}); );
const getFlows = data?.getFlows || {};
const { pageInfo, edges } = getFlows; const appFlows = useAppFlows({ appKey, page }, { enabled: isAppFlowEnabled });
const flows = edges?.map(({ node }) => node);
const flows = isConnectionFlowEnabled
? connectionFlows?.data?.data || []
: appFlows?.data?.data || [];
const pageInfo = isConnectionFlowEnabled
? connectionFlows?.data?.meta || []
: appFlows?.data?.meta || [];
const hasFlows = flows?.length; const hasFlows = flows?.length;
if (!hasFlows) { if (!hasFlows) {
return ( return (
<NoResultFound <NoResultFound
@@ -39,6 +43,7 @@ function AppFlows(props) {
/> />
); );
} }
return ( return (
<> <>
{flows?.map((appFlow) => ( {flows?.map((appFlow) => (

View File

@@ -1,10 +1,14 @@
import useConfig from 'hooks/useConfig'; import useAutomatischConfig from 'hooks/useAutomatischConfig';
import { LogoImage } from './style.ee'; import { LogoImage } from './style.ee';
const CustomLogo = () => { const CustomLogo = () => {
const { config, loading } = useConfig(['logo.svgData']); const { data: configData, isLoading } = useAutomatischConfig();
if (loading || !config?.['logo.svgData']) return null; const config = configData?.data;
if (isLoading || !config?.['logo.svgData']) return null;
const logoSvgData = config['logo.svgData']; const logoSvgData = config['logo.svgData'];
return ( return (
<LogoImage <LogoImage
data-test="custom-logo" data-test="custom-logo"

View File

@@ -2,6 +2,8 @@ import PropTypes from 'prop-types';
import { useMutation } from '@apollo/client'; import { useMutation } from '@apollo/client';
import DeleteIcon from '@mui/icons-material/Delete'; import DeleteIcon from '@mui/icons-material/Delete';
import IconButton from '@mui/material/IconButton'; import IconButton from '@mui/material/IconButton';
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 ConfirmationDialog from 'components/ConfirmationDialog'; import ConfirmationDialog from 'components/ConfirmationDialog';
@@ -17,9 +19,13 @@ function DeleteUserButton(props) {
}); });
const formatMessage = useFormatMessage(); const formatMessage = useFormatMessage();
const enqueueSnackbar = useEnqueueSnackbar(); const enqueueSnackbar = useEnqueueSnackbar();
const queryClient = useQueryClient();
const handleConfirm = React.useCallback(async () => { const handleConfirm = React.useCallback(async () => {
try { try {
await deleteUser(); await deleteUser();
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',
@@ -31,6 +37,7 @@ function DeleteUserButton(props) {
throw new Error('Failed while deleting!'); throw new Error('Failed while deleting!');
} }
}, [deleteUser]); }, [deleteUser]);
return ( return (
<> <>
<IconButton <IconButton

View File

@@ -15,7 +15,7 @@ import useFormatMessage from 'hooks/useFormatMessage';
import useVersion from 'hooks/useVersion'; import useVersion from 'hooks/useVersion';
import AppBar from 'components/AppBar'; import AppBar from 'components/AppBar';
import Drawer from 'components/Drawer'; import Drawer from 'components/Drawer';
import useConfig from 'hooks/useConfig'; import useAutomatischConfig from 'hooks/useAutomatischConfig';
const drawerLinks = [ const drawerLinks = [
{ {
@@ -77,11 +77,9 @@ const generateDrawerBottomLinks = async ({
export default function PublicLayout({ children }) { export default function PublicLayout({ children }) {
const version = useVersion(); const version = useVersion();
const { config, loading } = useConfig([ const { data: configData, isLoading } = useAutomatischConfig();
'disableNotificationsPage', const config = configData?.data;
'additionalDrawerLink',
'additionalDrawerLinkText',
]);
const theme = useTheme(); const theme = useTheme();
const formatMessage = useFormatMessage(); const formatMessage = useFormatMessage();
const [bottomLinks, setBottomLinks] = React.useState([]); const [bottomLinks, setBottomLinks] = React.useState([]);
@@ -102,10 +100,10 @@ export default function PublicLayout({ children }) {
setBottomLinks(newBottomLinks); setBottomLinks(newBottomLinks);
} }
if (loading) return; if (isLoading) return;
perform(); perform();
}, [config, loading, version.newVersionCount]); }, [config, isLoading, version.newVersionCount]);
return ( return (
<> <>

View File

@@ -1,12 +1,19 @@
import * as React from 'react'; import * as React from 'react';
import CustomLogo from 'components/CustomLogo/index.ee'; import CustomLogo from 'components/CustomLogo/index.ee';
import DefaultLogo from 'components/DefaultLogo'; import DefaultLogo from 'components/DefaultLogo';
import useConfig from 'hooks/useConfig'; import useAutomatischConfig from 'hooks/useAutomatischConfig';
const Logo = () => { const Logo = () => {
const { config, loading } = useConfig(['logo.svgData']); const { data: configData, isLoading } = useAutomatischConfig();
const config = configData?.data;
const logoSvgData = config?.['logo.svgData']; const logoSvgData = config?.['logo.svgData'];
if (loading && !logoSvgData) return <React.Fragment />;
if (isLoading && !logoSvgData) return <React.Fragment />;
if (logoSvgData) return <CustomLogo />; if (logoSvgData) return <CustomLogo />;
return <DefaultLogo />; return <DefaultLogo />;
}; };
export default Logo; export default Logo;

View File

@@ -1,15 +1,22 @@
import * as React from 'react'; import * as React from 'react';
import useConfig from 'hooks/useConfig';
import useAutomatischConfig from 'hooks/useAutomatischConfig';
const MetadataProvider = ({ children }) => { const MetadataProvider = ({ children }) => {
const { config } = useConfig(); const { data: configData } = useAutomatischConfig();
const config = configData?.data;
React.useEffect(() => { React.useEffect(() => {
document.title = config?.title || 'Automatisch'; document.title = config?.title || 'Automatisch';
}, [config?.title]); }, [config?.title]);
React.useEffect(() => { React.useEffect(() => {
const existingFaviconElement = document.querySelector("link[rel~='icon']"); const existingFaviconElement = document.querySelector("link[rel~='icon']");
if (config?.disableFavicon === true) { if (config?.disableFavicon === true) {
existingFaviconElement?.remove(); existingFaviconElement?.remove();
} }
if (config?.disableFavicon === false) { if (config?.disableFavicon === false) {
if (existingFaviconElement) { if (existingFaviconElement) {
existingFaviconElement.href = '/browser-tab.ico'; existingFaviconElement.href = '/browser-tab.ico';
@@ -20,7 +27,10 @@ const MetadataProvider = ({ children }) => {
newFaviconElement.href = '/browser-tab.ico'; newFaviconElement.href = '/browser-tab.ico';
} }
} }
}, [config?.disableFavicon]); }, [config?.disableFavicon]);
return <>{children}</>; return <>{children}</>;
}; };
export default MetadataProvider; export default MetadataProvider;

View File

@@ -6,7 +6,7 @@ import set from 'lodash/set';
import * as React from 'react'; import * as React from 'react';
import useAutomatischInfo from 'hooks/useAutomatischInfo'; import useAutomatischInfo from 'hooks/useAutomatischInfo';
import useConfig from 'hooks/useConfig'; import useAutomatischConfig from 'hooks/useAutomatischConfig';
import { defaultTheme, mationTheme } from 'styles/theme'; import { defaultTheme, mationTheme } from 'styles/theme';
const customizeTheme = (theme, config) => { const customizeTheme = (theme, config) => {
@@ -28,7 +28,8 @@ const ThemeProvider = ({ children, ...props }) => {
const { data: automatischInfo, isPending: isAutomatischInfoPending } = const { data: automatischInfo, isPending: isAutomatischInfoPending } =
useAutomatischInfo(); useAutomatischInfo();
const isMation = automatischInfo?.data.isMation; const isMation = automatischInfo?.data.isMation;
const { config, loading: configLoading } = useConfig(); const { data: configData, isLoading: configLoading } = useAutomatischConfig();
const config = configData?.data;
const customTheme = React.useMemo(() => { const customTheme = React.useMemo(() => {
const installationTheme = isMation ? mationTheme : defaultTheme; const installationTheme = isMation ? mationTheme : defaultTheme;
@@ -51,4 +52,5 @@ const ThemeProvider = ({ children, ...props }) => {
</BaseThemeProvider> </BaseThemeProvider>
); );
}; };
export default ThemeProvider; export default ThemeProvider;

View File

@@ -14,23 +14,23 @@ import EditIcon from '@mui/icons-material/Edit';
import TableFooter from '@mui/material/TableFooter'; import TableFooter from '@mui/material/TableFooter';
import DeleteUserButton from 'components/DeleteUserButton/index.ee'; import DeleteUserButton from 'components/DeleteUserButton/index.ee';
import ListLoader from 'components/ListLoader'; import ListLoader from 'components/ListLoader';
import useUsers from 'hooks/useUsers'; import useAdminUsers from 'hooks/useAdminUsers';
import useFormatMessage from 'hooks/useFormatMessage'; import useFormatMessage from 'hooks/useFormatMessage';
import * as URLS from 'config/urls'; import * as URLS from 'config/urls';
import TablePaginationActions from './TablePaginationActions'; import TablePaginationActions from './TablePaginationActions';
import { TablePagination } from './style'; import { TablePagination } from './style';
export default function UserList() { export default function UserList() {
const formatMessage = useFormatMessage(); const formatMessage = useFormatMessage();
const [page, setPage] = React.useState(0); const [page, setPage] = React.useState(0);
const [rowsPerPage, setRowsPerPage] = React.useState(10); const { data: usersData, isLoading } = useAdminUsers(page + 1);
const { users, pageInfo, totalCount, loading } = useUsers(page, rowsPerPage); const users = usersData?.data;
const { count } = usersData?.meta || {};
const handleChangePage = (event, newPage) => { const handleChangePage = (event, newPage) => {
setPage(newPage); setPage(newPage);
}; };
const handleChangeRowsPerPage = (event) => {
setRowsPerPage(+event.target.value);
setPage(0);
};
return ( return (
<> <>
<TableContainer component={Paper}> <TableContainer component={Paper}>
@@ -68,14 +68,14 @@ export default function UserList() {
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
{loading && ( {isLoading && (
<ListLoader <ListLoader
data-test="users-list-loader" data-test="users-list-loader"
rowsNumber={3} rowsNumber={3}
columnsNumber={2} columnsNumber={2}
/> />
)} )}
{!loading && {!isLoading &&
users.map((user) => ( users.map((user) => (
<TableRow <TableRow
key={user.id} key={user.id}
@@ -120,18 +120,16 @@ export default function UserList() {
</TableRow> </TableRow>
))} ))}
</TableBody> </TableBody>
{totalCount && ( {!isLoading && typeof count === 'number' && (
<TableFooter> <TableFooter>
<TableRow> <TableRow>
<TablePagination <TablePagination
data-total-count={totalCount} data-total-count={count}
data-rows-per-page={rowsPerPage} rowsPerPageOptions={[]}
rowsPerPageOptions={[10, 25, 50, 100]}
page={page} page={page}
count={totalCount} count={count}
onPageChange={handleChangePage} onPageChange={handleChangePage}
rowsPerPage={rowsPerPage} rowsPerPage={10}
onRowsPerPageChange={handleChangeRowsPerPage}
ActionsComponent={TablePaginationActions} ActionsComponent={TablePaginationActions}
/> />
</TableRow> </TableRow>

View File

@@ -1,6 +0,0 @@
import { gql } from '@apollo/client';
export const GET_CONFIG = gql`
query GetConfig($keys: [String]) {
getConfig(keys: $keys)
}
`;

View File

@@ -1,36 +0,0 @@
import { gql } from '@apollo/client';
export const GET_FLOWS = gql`
query GetFlows(
$limit: Int!
$offset: Int!
$appKey: String
$connectionId: String
$name: String
) {
getFlows(
limit: $limit
offset: $offset
appKey: $appKey
connectionId: $connectionId
name: $name
) {
pageInfo {
currentPage
totalPages
}
edges {
node {
id
name
createdAt
updatedAt
active
status
steps {
iconUrl
}
}
}
}
}
`;

View File

@@ -1,11 +0,0 @@
import { gql } from '@apollo/client';
export const GET_NOTIFICATIONS = gql`
query GetNotifications {
getNotifications {
name
createdAt
documentationUrl
description
}
}
`;

View File

@@ -1,23 +0,0 @@
import { gql } from '@apollo/client';
export const GET_USERS = gql`
query GetUsers($limit: Int!, $offset: Int!) {
getUsers(limit: $limit, offset: $offset) {
pageInfo {
currentPage
totalPages
}
totalCount
edges {
node {
id
fullName
email
role {
id
name
}
}
}
}
}
`;

View File

@@ -1,9 +1,9 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import api from 'helpers/api'; import api from 'helpers/api';
export default function useUser({ userId }) { export default function useAdminUser({ userId }) {
const query = useQuery({ const query = useQuery({
queryKey: ['user', userId], queryKey: ['admin', 'user', userId],
queryFn: async ({ signal }) => { queryFn: async ({ signal }) => {
const { data } = await api.get(`/v1/admin/users/${userId}`, { const { data } = await api.get(`/v1/admin/users/${userId}`, {
signal, signal,

View File

@@ -0,0 +1,17 @@
import { useQuery } from '@tanstack/react-query';
import api from 'helpers/api';
export default function useAdminUsers(page) {
const query = useQuery({
queryKey: ['admin', 'users', page],
queryFn: async ({ signal }) => {
const { data } = await api.get(`/v1/admin/users`, {
signal,
params: { page },
});
return data;
},
});
return query;
}

View File

@@ -0,0 +1,22 @@
import { useQuery } from '@tanstack/react-query';
import api from 'helpers/api';
export default function useAppFlows({ appKey, page }, { enabled }) {
const query = useQuery({
queryKey: ['appFlows', appKey, page],
queryFn: async ({ signal }) => {
const { data } = await api.get(`/v1/apps/${appKey}/flows`, {
params: {
page,
},
signal,
});
return data;
},
enabled,
});
return query;
}

View File

@@ -0,0 +1,16 @@
import { useQuery } from '@tanstack/react-query';
import api from 'helpers/api';
export default function useAutomatischConfig() {
const query = useQuery({
queryKey: ['automatisch', 'config'],
queryFn: async ({ signal }) => {
const { data } = await api.get(`/v1/automatisch/config`, {
signal,
});
return data;
},
});
return query;
}

View File

@@ -0,0 +1,16 @@
import { useQuery } from '@tanstack/react-query';
import api from 'helpers/api';
export default function useAutomatischNotifications() {
const query = useQuery({
queryKey: ['automatisch', 'notifications'],
queryFn: async ({ signal }) => {
const { data } = await api.get(`/v1/automatisch/notifications`, {
signal,
});
return data;
},
});
return query;
}

View File

@@ -1,11 +0,0 @@
import { useQuery } from '@apollo/client';
import { GET_CONFIG } from 'graphql/queries/get-config.ee';
export default function useConfig(keys) {
const { data, loading } = useQuery(GET_CONFIG, {
variables: { keys },
});
return {
config: data?.getConfig,
loading,
};
}

View File

@@ -0,0 +1,25 @@
import { useQuery } from '@tanstack/react-query';
import api from 'helpers/api';
export default function useConnectionFlows(
{ connectionId, page },
{ enabled },
) {
const query = useQuery({
queryKey: ['connectionFlows', connectionId, page],
queryFn: async ({ signal }) => {
const { data } = await api.get(`/v1/connections/${connectionId}/flows`, {
params: {
page,
},
signal,
});
return data;
},
enabled,
});
return query;
}

View File

@@ -0,0 +1,30 @@
import * as React from 'react';
import api from 'helpers/api';
import { useMutation } from '@tanstack/react-query';
export default function useLazyFlows({ flowName, page }, { onSuccess }) {
const abortControllerRef = React.useRef(new AbortController());
React.useEffect(() => {
abortControllerRef.current = new AbortController();
return () => {
abortControllerRef.current?.abort();
};
}, [flowName]);
const query = useMutation({
mutationFn: async () => {
const { data } = await api.get('/v1/flows', {
params: { name: flowName, page },
signal: abortControllerRef.current.signal,
});
return data;
},
onSuccess,
});
return query;
}

View File

@@ -1,10 +0,0 @@
import { useQuery } from '@apollo/client';
import { GET_NOTIFICATIONS } from 'graphql/queries/get-notifications';
export default function useNotifications() {
const { data, loading } = useQuery(GET_NOTIFICATIONS);
const notifications = data?.getNotifications || [];
return {
loading,
notifications,
};
}

View File

@@ -1,20 +0,0 @@
import { useQuery } from '@apollo/client';
import { GET_USERS } from 'graphql/queries/get-users';
const getLimitAndOffset = (page, rowsPerPage) => ({
limit: rowsPerPage,
offset: page * rowsPerPage,
});
export default function useUsers(page, rowsPerPage) {
const { data, loading } = useQuery(GET_USERS, {
variables: getLimitAndOffset(page, rowsPerPage),
});
const users = data?.getUsers.edges.map(({ node }) => node) || [];
const pageInfo = data?.getUsers.pageInfo;
const totalCount = data?.getUsers.totalCount;
return {
users,
pageInfo,
totalCount,
loading,
};
}

View File

@@ -1,11 +1,11 @@
import { compare } from 'compare-versions'; import { compare } from 'compare-versions';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import useNotifications from 'hooks/useNotifications'; import useAutomatischNotifications from 'hooks/useAutomatischNotifications';
import api from 'helpers/api'; import api from 'helpers/api';
export default function useVersion() { export default function useVersion() {
const { notifications } = useNotifications(); const { data: notificationsData } = useAutomatischNotifications();
const { data } = useQuery({ const { data } = useQuery({
queryKey: ['automatischVersion'], queryKey: ['automatischVersion'],
queryFn: async ({ signal }) => { queryFn: async ({ signal }) => {
@@ -17,6 +17,7 @@ export default function useVersion() {
}, },
}); });
const version = data?.data?.version; const version = data?.data?.version;
const notifications = notificationsData?.data || [];
const newVersionCount = notifications.reduce((count, notification) => { const newVersionCount = notifications.reduce((count, notification) => {
if (!version) return 0; if (!version) return 0;

View File

@@ -6,6 +6,7 @@ import MuiTextField from '@mui/material/TextField';
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar'; import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
import * as React from 'react'; import * as React from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useQueryClient } from '@tanstack/react-query';
import Can from 'components/Can'; import Can from 'components/Can';
import Container from 'components/Container'; import Container from 'components/Container';
@@ -29,6 +30,7 @@ export default function CreateUser() {
const { data, loading: isRolesLoading } = useRoles(); const { data, loading: isRolesLoading } = useRoles();
const roles = data?.data; const roles = data?.data;
const enqueueSnackbar = useEnqueueSnackbar(); const enqueueSnackbar = useEnqueueSnackbar();
const queryClient = useQueryClient();
const handleUserCreation = async (userData) => { const handleUserCreation = async (userData) => {
try { try {
@@ -44,7 +46,7 @@ export default function CreateUser() {
}, },
}, },
}); });
queryClient.invalidateQueries({ queryKey: ['admin', 'users'] });
enqueueSnackbar(formatMessage('createUser.successfullyCreated'), { enqueueSnackbar(formatMessage('createUser.successfullyCreated'), {
variant: 'success', variant: 'success',
persist: true, persist: true,

View File

@@ -7,6 +7,7 @@ import MuiTextField from '@mui/material/TextField';
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar'; import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
import * as React from 'react'; import * as React from 'react';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import { useQueryClient } from '@tanstack/react-query';
import Can from 'components/Can'; import Can from 'components/Can';
import Container from 'components/Container'; import Container from 'components/Container';
@@ -18,7 +19,7 @@ import * as URLS from 'config/urls';
import { UPDATE_USER } from 'graphql/mutations/update-user.ee'; import { UPDATE_USER } from 'graphql/mutations/update-user.ee';
import useFormatMessage from 'hooks/useFormatMessage'; import useFormatMessage from 'hooks/useFormatMessage';
import useRoles from 'hooks/useRoles.ee'; import useRoles from 'hooks/useRoles.ee';
import useUser from 'hooks/useUser'; import useAdminUser from 'hooks/useAdminUser';
function generateRoleOptions(roles) { function generateRoleOptions(roles) {
return roles?.map(({ name: label, id: value }) => ({ label, value })); return roles?.map(({ name: label, id: value }) => ({ label, value }));
@@ -28,12 +29,13 @@ export default function EditUser() {
const formatMessage = useFormatMessage(); const formatMessage = useFormatMessage();
const [updateUser, { loading }] = useMutation(UPDATE_USER); const [updateUser, { loading }] = useMutation(UPDATE_USER);
const { userId } = useParams(); const { userId } = useParams();
const { data: userData, isLoading: isUserLoading } = useUser({ userId }); const { data: userData, isLoading: isUserLoading } = useAdminUser({ userId });
const user = userData?.data; const user = userData?.data;
const { data, isLoading: isRolesLoading } = useRoles(); const { data, isLoading: isRolesLoading } = useRoles();
const roles = data?.data; const roles = data?.data;
const enqueueSnackbar = useEnqueueSnackbar(); const enqueueSnackbar = useEnqueueSnackbar();
const navigate = useNavigate(); const navigate = useNavigate();
const queryClient = useQueryClient();
const handleUserUpdate = async (userDataToUpdate) => { const handleUserUpdate = async (userDataToUpdate) => {
try { try {
@@ -49,6 +51,8 @@ export default function EditUser() {
}, },
}, },
}); });
queryClient.invalidateQueries({ queryKey: ['admin', 'users'] });
queryClient.invalidateQueries({ queryKey: ['admin', 'user', userId] });
enqueueSnackbar(formatMessage('editUser.successfullyUpdated'), { enqueueSnackbar(formatMessage('editUser.successfullyUpdated'), {
variant: 'success', variant: 'success',

View File

@@ -1,6 +1,5 @@
import * as React from 'react'; import * as React from 'react';
import { Link, useSearchParams } from 'react-router-dom'; import { Link, useSearchParams } from 'react-router-dom';
import { useLazyQuery } from '@apollo/client';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import Grid from '@mui/material/Grid'; import Grid from '@mui/material/Grid';
@@ -9,6 +8,7 @@ import CircularProgress from '@mui/material/CircularProgress';
import Divider from '@mui/material/Divider'; import Divider from '@mui/material/Divider';
import Pagination from '@mui/material/Pagination'; import Pagination from '@mui/material/Pagination';
import PaginationItem from '@mui/material/PaginationItem'; import PaginationItem from '@mui/material/PaginationItem';
import Can from 'components/Can'; import Can from 'components/Can';
import FlowRow from 'components/FlowRow'; import FlowRow from 'components/FlowRow';
import NoResultFound from 'components/NoResultFound'; import NoResultFound from 'components/NoResultFound';
@@ -17,45 +17,37 @@ import Container from 'components/Container';
import PageTitle from 'components/PageTitle'; import PageTitle from 'components/PageTitle';
import SearchInput from 'components/SearchInput'; import SearchInput from 'components/SearchInput';
import useFormatMessage from 'hooks/useFormatMessage'; import useFormatMessage from 'hooks/useFormatMessage';
import { GET_FLOWS } from 'graphql/queries/get-flows';
import * as URLS from 'config/urls'; import * as URLS from 'config/urls';
const FLOW_PER_PAGE = 10; import useLazyFlows from 'hooks/useLazyFlows';
const getLimitAndOffset = (page) => ({
limit: FLOW_PER_PAGE,
offset: (page - 1) * FLOW_PER_PAGE,
});
export default function Flows() { export default function Flows() {
const formatMessage = useFormatMessage(); const formatMessage = useFormatMessage();
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
const page = parseInt(searchParams.get('page') || '', 10) || 1; const page = parseInt(searchParams.get('page') || '', 10) || 1;
const [flowName, setFlowName] = React.useState(''); const [flowName, setFlowName] = React.useState('');
const [loading, setLoading] = React.useState(false); const [isLoading, setIsLoading] = React.useState(false);
const [getFlows, { data }] = useLazyQuery(GET_FLOWS, {
onCompleted: () => { const { data, mutate } = useLazyFlows(
setLoading(false); { flowName, page },
{
onSuccess: () => {
setIsLoading(false);
},
}, },
});
const fetchData = React.useMemo(
() =>
debounce(
(name) =>
getFlows({
variables: {
...getLimitAndOffset(page),
name,
},
}),
300,
),
[page, getFlows],
);
React.useEffect(
function fetchFlowsOnSearch() {
setLoading(true);
fetchData(flowName);
},
[fetchData, flowName],
); );
const fetchData = React.useMemo(() => debounce(mutate, 300), [mutate]);
React.useEffect(() => {
setIsLoading(true);
fetchData({ flowName, page });
return () => {
fetchData.cancel();
};
}, [fetchData, flowName, page]);
React.useEffect( React.useEffect(
function resetPageOnSearch() { function resetPageOnSearch() {
// reset search params which only consists of `page` // reset search params which only consists of `page`
@@ -63,17 +55,15 @@ export default function Flows() {
}, },
[flowName], [flowName],
); );
React.useEffect(function cancelDebounceOnUnmount() {
return () => { const flows = data?.data || [];
fetchData.cancel(); const pageInfo = data?.meta;
};
}, []);
const { pageInfo, edges } = data?.getFlows || {};
const flows = edges?.map(({ node }) => node);
const hasFlows = flows?.length; const hasFlows = flows?.length;
const onSearchChange = React.useCallback((event) => { const onSearchChange = React.useCallback((event) => {
setFlowName(event.target.value); setFlowName(event.target.value);
}, []); }, []);
return ( return (
<Box sx={{ py: 3 }}> <Box sx={{ py: 3 }}>
<Container> <Container>
@@ -116,18 +106,18 @@ export default function Flows() {
</Grid> </Grid>
<Divider sx={{ mt: [2, 0], mb: 2 }} /> <Divider sx={{ mt: [2, 0], mb: 2 }} />
{loading && ( {isLoading && (
<CircularProgress sx={{ display: 'block', margin: '20px auto' }} /> <CircularProgress sx={{ display: 'block', margin: '20px auto' }} />
)} )}
{!loading && {!isLoading &&
flows?.map((flow) => <FlowRow key={flow.id} flow={flow} />)} flows?.map((flow) => <FlowRow key={flow.id} flow={flow} />)}
{!loading && !hasFlows && ( {!isLoading && !hasFlows && (
<NoResultFound <NoResultFound
text={formatMessage('flows.noFlows')} text={formatMessage('flows.noFlows')}
to={URLS.CREATE_FLOW} to={URLS.CREATE_FLOW}
/> />
)} )}
{!loading && pageInfo && pageInfo.totalPages > 1 && ( {!isLoading && pageInfo && pageInfo.totalPages > 1 && (
<Pagination <Pagination
sx={{ display: 'flex', justifyContent: 'center', mt: 3 }} sx={{ display: 'flex', justifyContent: 'center', mt: 3 }}
page={pageInfo?.currentPage} page={pageInfo?.currentPage}

View File

@@ -9,14 +9,15 @@ import PageTitle from 'components/PageTitle';
import * as URLS from 'config/urls'; import * as URLS from 'config/urls';
import useAutomatischInfo from 'hooks/useAutomatischInfo'; import useAutomatischInfo from 'hooks/useAutomatischInfo';
import useFormatMessage from 'hooks/useFormatMessage'; import useFormatMessage from 'hooks/useFormatMessage';
import useNotifications from 'hooks/useNotifications'; import useAutomatischNotifications from 'hooks/useAutomatischNotifications';
export default function Updates() { export default function Updates() {
const navigate = useNavigate(); const navigate = useNavigate();
const formatMessage = useFormatMessage(); const formatMessage = useFormatMessage();
const { notifications } = useNotifications(); const { data: notificationsData } = useAutomatischNotifications();
const { data: automatischInfo, isPending } = useAutomatischInfo(); const { data: automatischInfo, isPending } = useAutomatischInfo();
const isMation = automatischInfo?.data.isMation; const isMation = automatischInfo?.data.isMation;
const notifications = notificationsData?.data || [];
React.useEffect( React.useEffect(
function redirectToHomepageInMation() { function redirectToHomepageInMation() {

View File

@@ -3,43 +3,44 @@ import LoadingButton from '@mui/lab/LoadingButton';
import Grid from '@mui/material/Grid'; import Grid from '@mui/material/Grid';
import Skeleton from '@mui/material/Skeleton'; import Skeleton from '@mui/material/Skeleton';
import Stack from '@mui/material/Stack'; import Stack from '@mui/material/Stack';
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
import merge from 'lodash/merge'; import merge from 'lodash/merge';
import * as React from 'react'; import * as React from 'react';
import { useQueryClient } from '@tanstack/react-query';
import ColorInput from 'components/ColorInput'; import ColorInput from 'components/ColorInput';
import Container from 'components/Container'; import Container from 'components/Container';
import Form from 'components/Form'; import Form from 'components/Form';
import PageTitle from 'components/PageTitle'; import PageTitle from 'components/PageTitle';
import TextField from 'components/TextField'; import TextField from 'components/TextField';
import { UPDATE_CONFIG } from 'graphql/mutations/update-config.ee'; import { UPDATE_CONFIG } from 'graphql/mutations/update-config.ee';
import { GET_CONFIG } from 'graphql/queries/get-config.ee';
import nestObject from 'helpers/nestObject'; import nestObject from 'helpers/nestObject';
import useConfig from 'hooks/useConfig'; import useAutomatischConfig from 'hooks/useAutomatischConfig';
import useFormatMessage from 'hooks/useFormatMessage'; import useFormatMessage from 'hooks/useFormatMessage';
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
import { import {
primaryDarkColor, primaryDarkColor,
primaryLightColor, primaryLightColor,
primaryMainColor, primaryMainColor,
} from 'styles/theme'; } from 'styles/theme';
const getPrimaryMainColor = (color) => color || primaryMainColor; const getPrimaryMainColor = (color) => color || primaryMainColor;
const getPrimaryDarkColor = (color) => color || primaryDarkColor; const getPrimaryDarkColor = (color) => color || primaryDarkColor;
const getPrimaryLightColor = (color) => color || primaryLightColor; const getPrimaryLightColor = (color) => color || primaryLightColor;
const defaultValues = { const defaultValues = {
title: 'Automatisch', title: 'Automatisch',
'palette.primary.main': primaryMainColor, 'palette.primary.main': primaryMainColor,
'palette.primary.dark': primaryDarkColor, 'palette.primary.dark': primaryDarkColor,
'palette.primary.light': primaryLightColor, 'palette.primary.light': primaryLightColor,
}; };
export default function UserInterface() { export default function UserInterface() {
const formatMessage = useFormatMessage(); const formatMessage = useFormatMessage();
const [updateConfig, { loading }] = useMutation(UPDATE_CONFIG); const [updateConfig, { loading }] = useMutation(UPDATE_CONFIG);
const { config, loading: configLoading } = useConfig([ const { data: configData, isLoading: configLoading } = useAutomatischConfig();
'title', const config = configData?.data;
'palette.primary.main', const queryClient = useQueryClient();
'palette.primary.light',
'palette.primary.dark',
'logo.svgData',
]);
const enqueueSnackbar = useEnqueueSnackbar(); const enqueueSnackbar = useEnqueueSnackbar();
const configWithDefaults = merge({}, defaultValues, nestObject(config)); const configWithDefaults = merge({}, defaultValues, nestObject(config));
const handleUserInterfaceUpdate = async (uiData) => { const handleUserInterfaceUpdate = async (uiData) => {
@@ -64,37 +65,9 @@ export default function UserInterface() {
optimisticResponse: { optimisticResponse: {
updateConfig: input, updateConfig: input,
}, },
update: async function (cache, { data: { updateConfig } }) { update: async function () {
const newConfigWithDefaults = merge({}, defaultValues, updateConfig); queryClient.invalidateQueries({
cache.writeQuery({ queryKey: ['automatisch', 'config'],
query: GET_CONFIG,
data: {
getConfig: newConfigWithDefaults,
},
});
cache.writeQuery({
query: GET_CONFIG,
data: {
getConfig: newConfigWithDefaults,
},
variables: {
keys: ['logo.svgData'],
},
});
cache.writeQuery({
query: GET_CONFIG,
data: {
getConfig: newConfigWithDefaults,
},
variables: {
keys: [
'title',
'palette.primary.main',
'palette.primary.light',
'palette.primary.dark',
'logo.svgData',
],
},
}); });
}, },
}); });

View File

@@ -1,4 +1,5 @@
import { Route, Routes as ReactRouterRoutes, Navigate } from 'react-router-dom'; import { Route, Routes as ReactRouterRoutes, Navigate } from 'react-router-dom';
import Layout from 'components/Layout'; import Layout from 'components/Layout';
import NoResultFound from 'components/NotFound'; import NoResultFound from 'components/NotFound';
import PublicLayout from 'components/PublicLayout'; import PublicLayout from 'components/PublicLayout';
@@ -19,11 +20,14 @@ import * as URLS from 'config/urls';
import settingsRoutes from './settingsRoutes'; import settingsRoutes from './settingsRoutes';
import adminSettingsRoutes from './adminSettingsRoutes'; import adminSettingsRoutes from './adminSettingsRoutes';
import Notifications from 'pages/Notifications'; import Notifications from 'pages/Notifications';
import useConfig from 'hooks/useConfig'; import useAutomatischConfig from 'hooks/useAutomatischConfig';
import useAuthentication from 'hooks/useAuthentication'; import useAuthentication from 'hooks/useAuthentication';
function Routes() { function Routes() {
const { config } = useConfig(); const { data: configData } = useAutomatischConfig();
const { isAuthenticated } = useAuthentication(); const { isAuthenticated } = useAuthentication();
const config = configData?.data;
return ( return (
<ReactRouterRoutes> <ReactRouterRoutes>
<Route <Route
@@ -147,4 +151,5 @@ function Routes() {
</ReactRouterRoutes> </ReactRouterRoutes>
); );
} }
export default <Routes />; export default <Routes />;

View File

@@ -5471,6 +5471,11 @@ basic-auth@^2.0.1, basic-auth@~2.0.1:
dependencies: dependencies:
safe-buffer "5.1.2" safe-buffer "5.1.2"
basic-ftp@^5.0.5:
version "5.0.5"
resolved "https://registry.yarnpkg.com/basic-ftp/-/basic-ftp-5.0.5.tgz#14a474f5fffecca1f4f406f1c26b18f800225ac0"
integrity sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==
batch@0.6.1: batch@0.6.1:
version "0.6.1" version "0.6.1"
resolved "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz" resolved "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz"
@@ -8379,9 +8384,9 @@ fn.name@1.x.x:
integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw== integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==
follow-redirects@^1.0.0, follow-redirects@^1.14.8, follow-redirects@^1.15.0: follow-redirects@^1.0.0, follow-redirects@^1.14.8, follow-redirects@^1.15.0:
version "1.15.3" version "1.15.6"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.3.tgz#fe2f3ef2690afce7e82ed0b44db08165b207123a" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b"
integrity sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q== integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==
forever-agent@~0.6.1: forever-agent@~0.6.1:
version "0.6.1" version "0.6.1"