Compare commits

..

28 Commits

Author SHA1 Message Date
Ali BARIN
24bf07e068 Update version to 0.13.1 2024-08-02 15:54:26 +02:00
Ali BARIN
bae234827f Merge pull request #2006 from automatisch/use-unparsed-numbers-in-compute-parameters
fix(compute-parameters): use unparsed numbers
2024-08-02 15:42:21 +02:00
Ali BARIN
81c698f45b fix(compute-parameters): use unparsed numbers 2024-08-02 12:22:03 +00:00
Ali BARIN
c9fecec575 Merge pull request #2002 from automatisch/aut-1255
feat: expose installationCompleted in GET /v1/automatisch/info
2024-08-02 11:46:48 +02:00
Ali BARIN
2f42dfdc51 Merge pull request #2001 from automatisch/remove-pwa
feat: remove PWA
2024-08-02 11:16:44 +02:00
Ali BARIN
5afd500c26 Merge pull request #2003 from automatisch/release/v0.13.0
Update version to 0.13.0
2024-08-01 18:30:55 +02:00
Faruk AYDIN
50d91405a9 Update version to 0.13.0 2024-08-01 17:32:19 +02:00
Ali BARIN
69eed65c9b Merge pull request #1991 from automatisch/AUT-1064
fix: introduce fix for overflowing inputs with long parameters names
2024-08-01 16:22:17 +02:00
kasia.oczkowska
f1355cd0ab fix: introduce fix for overflowing inputs with long parameters names 2024-08-01 15:10:41 +01:00
Ali BARIN
cc1a924c8b feat: use installationCompleted from useAutomatischInfo hook 2024-08-01 13:38:06 +00:00
Ali BARIN
02005a3f09 feat: expose installationCompleted in GET /v1/automatisch/info 2024-08-01 13:02:21 +00:00
Ali BARIN
d9219a5a48 feat: remove PWA 2024-08-01 12:28:23 +00:00
Ali BARIN
dffbdf544c Merge pull request #2000 from automatisch/AUT-1131
fix: prevent removing last filter criteria
2024-08-01 10:50:42 +02:00
kasia.oczkowska
e2dbf1a215 fix: prevent removing last filter criteria 2024-08-01 08:40:54 +01:00
Ömer Faruk Aydın
c1396b97f0 Merge pull request #1996 from automatisch/support-arrays-in-flows
feat: support arrays in flows
2024-07-30 13:26:56 +02:00
Ali BARIN
920a711c00 feat: support arrays in flows 2024-07-29 11:33:36 +00:00
Ömer Faruk Aydın
02a872a376 Merge pull request #1995 from automatisch/support-async-before-request
feat(http-client): support async beforeRequest interceptors
2024-07-27 18:41:40 +02:00
Ali BARIN
be4493710f feat(http-client): support async beforeRequest interceptors 2024-07-27 13:05:04 +00:00
Ömer Faruk Aydın
52c0c5e0c5 Merge pull request #1992 from automatisch/fix-proxy-configuration
fix(axios): update order of interceptors
2024-07-26 20:16:38 +02:00
Ali BARIN
4c639f170e fix(axios): update order of interceptors 2024-07-26 13:52:41 +00:00
Ali BARIN
509a414151 Merge pull request #1986 from automatisch/aut-1126
refactor(http-client): inherit interceptors from parent instance
2024-07-26 11:45:02 +02:00
Ali BARIN
8f3c793a69 Merge pull request #1988 from automatisch/aut-1130
feat(formatter/text): add regex support in replace transfomer
2024-07-26 11:21:12 +02:00
Ömer Faruk Aydın
a2dd9cf1b8 Merge pull request #1989 from automatisch/fix-typos-in-appwrite-docs
docs(appwrite): fix typos
2024-07-26 10:14:15 +02:00
Ömer Faruk Aydın
42285a5879 Merge pull request #1987 from automatisch/aut-1129
fix(webhook): add missing filter coverage
2024-07-26 10:13:40 +02:00
Ali BARIN
5d63fce6f0 docs(appwrite): fix typos 2024-07-25 09:18:25 +00:00
Ali BARIN
46a4c8faec feat(formatter/text): add regex support in replace transfomer 2024-07-24 17:34:14 +00:00
Ali BARIN
ba14481151 fix(webhook): add missing filter coverage 2024-07-24 16:01:28 +00:00
Ali BARIN
5a4207414d refactor(http-client): inherit interceptors from parent instance 2024-07-23 15:01:12 +00:00
18 changed files with 281 additions and 197 deletions

View File

@@ -52,7 +52,7 @@ const appConfig = {
isDev: appEnv === 'development', isDev: appEnv === 'development',
isTest: appEnv === 'test', isTest: appEnv === 'test',
isProd: appEnv === 'production', isProd: appEnv === 'production',
version: '0.12.0', version: '0.13.1',
postgresDatabase: process.env.POSTGRES_DATABASE || 'automatisch_development', postgresDatabase: process.env.POSTGRES_DATABASE || 'automatisch_development',
postgresSchema: process.env.POSTGRES_SCHEMA || 'public', postgresSchema: process.env.POSTGRES_SCHEMA || 'public',
postgresPort: parseInt(process.env.POSTGRES_PORT || '5432'), postgresPort: parseInt(process.env.POSTGRES_PORT || '5432'),

View File

@@ -1,13 +1,17 @@
import appConfig from '../../../../config/app.js'; import appConfig from '../../../../config/app.js';
import { hasValidLicense } from '../../../../helpers/license.ee.js'; import { hasValidLicense } from '../../../../helpers/license.ee.js';
import { renderObject } from '../../../../helpers/renderer.js'; import { renderObject } from '../../../../helpers/renderer.js';
import Config from '../../../../models/config.js';
export default async (request, response) => { export default async (request, response) => {
const installationCompleted = await Config.isInstallationCompleted();
const info = { const info = {
isCloud: appConfig.isCloud,
isMation: appConfig.isMation,
isEnterprise: await hasValidLicense(),
docsUrl: appConfig.docsUrl, docsUrl: appConfig.docsUrl,
installationCompleted,
isCloud: appConfig.isCloud,
isEnterprise: await hasValidLicense(),
isMation: appConfig.isMation,
}; };
renderObject(response, info); renderObject(response, info);

View File

@@ -1,12 +1,14 @@
import { vi, expect, describe, it } from 'vitest'; import { vi, expect, describe, it } from 'vitest';
import request from 'supertest'; import request from 'supertest';
import appConfig from '../../../../config/app.js'; import appConfig from '../../../../config/app.js';
import Config from '../../../../models/config.js';
import app from '../../../../app.js'; import app from '../../../../app.js';
import infoMock from '../../../../../test/mocks/rest/api/v1/automatisch/info.js'; import infoMock from '../../../../../test/mocks/rest/api/v1/automatisch/info.js';
import * as license from '../../../../helpers/license.ee.js'; import * as license from '../../../../helpers/license.ee.js';
describe('GET /api/v1/automatisch/info', () => { describe('GET /api/v1/automatisch/info', () => {
it('should return Automatisch info', async () => { it('should return Automatisch info', async () => {
vi.spyOn(Config, 'isInstallationCompleted').mockResolvedValue(true);
vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(false); vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(false);
vi.spyOn(appConfig, 'isMation', 'get').mockReturnValue(false); vi.spyOn(appConfig, 'isMation', 'get').mockReturnValue(false);
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true); vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);

View File

@@ -10,7 +10,7 @@ describe('GET /api/v1/automatisch/version', () => {
const expectedPayload = { const expectedPayload = {
data: { data: {
version: '0.12.0', version: '0.13.1',
}, },
meta: { meta: {
count: 1, count: 1,

View File

@@ -3,70 +3,100 @@ import { HttpsProxyAgent } from 'https-proxy-agent';
import { HttpProxyAgent } from 'http-proxy-agent'; import { HttpProxyAgent } from 'http-proxy-agent';
import appConfig from '../config/app.js'; import appConfig from '../config/app.js';
const config = axios.defaults; export function createInstance(customConfig = {}, { requestInterceptor, responseErrorInterceptor } = {}) {
const httpProxyUrl = appConfig.httpProxy; const config = {
const httpsProxyUrl = appConfig.httpsProxy; ...axios.defaults,
const supportsProxy = httpProxyUrl || httpsProxyUrl; ...customConfig
const noProxyEnv = appConfig.noProxy; };
const noProxyHosts = noProxyEnv ? noProxyEnv.split(',').map(host => host.trim()) : []; const httpProxyUrl = appConfig.httpProxy;
const httpsProxyUrl = appConfig.httpsProxy;
const supportsProxy = httpProxyUrl || httpsProxyUrl;
const noProxyEnv = appConfig.noProxy;
const noProxyHosts = noProxyEnv ? noProxyEnv.split(',').map(host => host.trim()) : [];
if (supportsProxy) { if (supportsProxy) {
if (httpProxyUrl) { if (httpProxyUrl) {
config.httpAgent = new HttpProxyAgent(httpProxyUrl); config.httpAgent = new HttpProxyAgent(httpProxyUrl);
}
if (httpsProxyUrl) {
config.httpsAgent = new HttpsProxyAgent(httpsProxyUrl);
}
config.proxy = false;
} }
if (httpsProxyUrl) { const instance = axios.create(config);
config.httpsAgent = new HttpsProxyAgent(httpsProxyUrl);
function shouldSkipProxy(hostname) {
return noProxyHosts.some(noProxyHost => {
return hostname.endsWith(noProxyHost) || hostname === noProxyHost;
});
};
/**
* The interceptors are executed in the reverse order they are added.
*/
instance.interceptors.request.use(
function skipProxyIfInNoProxy(requestConfig) {
const hostname = new URL(requestConfig.baseURL).hostname;
if (supportsProxy && shouldSkipProxy(hostname)) {
requestConfig.httpAgent = undefined;
requestConfig.httpsAgent = undefined;
}
return requestConfig;
},
(error) => Promise.reject(error)
);
// not always we have custom request interceptors
if (requestInterceptor) {
instance.interceptors.request.use(
async function customInterceptor(requestConfig) {
let newRequestConfig = requestConfig;
for (const interceptor of requestInterceptor) {
newRequestConfig = await interceptor(newRequestConfig);
}
return newRequestConfig;
}
);
} }
config.proxy = false; instance.interceptors.request.use(
function removeBaseUrlForAbsoluteUrls(requestConfig) {
/**
* If the URL is an absolute URL, we remove its origin out of the URL
* and set it as baseURL. This lets us streamlines the requests made by Automatisch
* and requests made by app integrations.
*/
try {
const url = new URL(requestConfig.url);
requestConfig.baseURL = url.origin;
requestConfig.url = url.pathname + url.search;
return requestConfig;
} catch (err) {
return requestConfig;
}
},
(error) => Promise.reject(error)
);
// not always we have custom response error interceptor
if (responseErrorInterceptor) {
instance.interceptors.response.use(
(response) => response,
responseErrorInterceptor
);
}
return instance;
} }
const axiosWithProxyInstance = axios.create(config); const defaultInstance = createInstance();
function shouldSkipProxy(hostname) { export default defaultInstance;
return noProxyHosts.some(noProxyHost => {
return hostname.endsWith(noProxyHost) || hostname === noProxyHost;
});
};
/**
* The interceptors are executed in the reverse order they are added.
*/
axiosWithProxyInstance.interceptors.request.use(
function skipProxyIfInNoProxy(requestConfig) {
const hostname = new URL(requestConfig.baseURL).hostname;
if (supportsProxy && shouldSkipProxy(hostname)) {
requestConfig.httpAgent = undefined;
requestConfig.httpsAgent = undefined;
}
return requestConfig;
},
undefined,
{ synchronous: true }
);
axiosWithProxyInstance.interceptors.request.use(
function removeBaseUrlForAbsoluteUrls(requestConfig) {
/**
* If the URL is an absolute URL, we remove its origin out of the URL
* and set it as baseURL. This lets us streamlines the requests made by Automatisch
* and requests made by app integrations.
*/
try {
const url = new URL(requestConfig.url);
requestConfig.baseURL = url.origin;
requestConfig.url = url.pathname + url.search;
return requestConfig;
} catch {
return requestConfig;
}
},
undefined,
{ synchronous: true}
);
export default axiosWithProxyInstance;

View File

@@ -1,6 +1,6 @@
import { beforeEach, describe, it, expect, vi } from 'vitest'; import { beforeEach, describe, it, expect, vi } from 'vitest';
describe('Axios with proxy', () => { describe('Custom default axios with proxy', () => {
beforeEach(() => { beforeEach(() => {
vi.resetModules(); vi.resetModules();
}); });
@@ -23,7 +23,13 @@ describe('Axios with proxy', () => {
expect(secondRequestInterceptor.fulfilled.name).toBe('removeBaseUrlForAbsoluteUrls'); expect(secondRequestInterceptor.fulfilled.name).toBe('removeBaseUrlForAbsoluteUrls');
}); });
describe('skipProxyIfInNoProxy', () => { it('should throw with invalid url (consisting of path alone)', async () => {
const axios = (await import('./axios-with-proxy.js')).default;
await expect(() => axios('/just-a-path')).rejects.toThrowError('Invalid URL');
});
describe('with skipProxyIfInNoProxy interceptor', () => {
let appConfig, axios; let appConfig, axios;
beforeEach(async() => { beforeEach(async() => {
appConfig = (await import('../config/app.js')).default; appConfig = (await import('../config/app.js')).default;
@@ -67,7 +73,7 @@ describe('Axios with proxy', () => {
}); });
}); });
describe('removeBaseUrlForAbsoluteUrls', () => { describe('with removeBaseUrlForAbsoluteUrls interceptor', () => {
let axios; let axios;
beforeEach(async() => { beforeEach(async() => {
axios = (await import('./axios-with-proxy.js')).default; axios = (await import('./axios-with-proxy.js')).default;
@@ -116,4 +122,48 @@ describe('Axios with proxy', () => {
expect(interceptedRequestConfig.url).toBe('/path?query=1'); expect(interceptedRequestConfig.url).toBe('/path?query=1');
}); });
}); });
describe('with extra requestInterceptors', () => {
it('should apply extra request interceptors in the middle', async () => {
const { createInstance } = await import('./axios-with-proxy.js');
const interceptor = (config) => {
config.test = true;
return config;
}
const instance = createInstance({}, {
requestInterceptor: [
interceptor
]
});
const requestInterceptors = instance.interceptors.request.handlers;
const customInterceptor = requestInterceptors[1].fulfilled;
expect(requestInterceptors.length).toBe(3);
await expect(customInterceptor({})).resolves.toStrictEqual({ test: true });
});
it('should work with a custom interceptor setting a baseURL and a request to path', async () => {
const { createInstance } = await import('./axios-with-proxy.js');
const interceptor = (config) => {
config.baseURL = 'http://localhost';
return config;
}
const instance = createInstance({}, {
requestInterceptor: [
interceptor
]
});
try {
await instance.get('/just-a-path');
} catch (error) {
expect(error.config.baseURL).toBe('http://localhost');
expect(error.config.url).toBe('/just-a-path');
}
})
});
}); });

View File

@@ -11,6 +11,7 @@ export default function computeParameters(parameters, executionSteps) {
const computedValue = parts const computedValue = parts
.map((part) => { .map((part) => {
const isVariable = part.match(variableRegExp); const isVariable = part.match(variableRegExp);
if (isVariable) { if (isVariable) {
const stepIdAndKeyPath = part.replace(/{{step.|}}/g, ''); const stepIdAndKeyPath = part.replace(/{{step.|}}/g, '');
const [stepId, ...keyPaths] = stepIdAndKeyPath.split('.'); const [stepId, ...keyPaths] = stepIdAndKeyPath.split('.');
@@ -20,17 +21,36 @@ export default function computeParameters(parameters, executionSteps) {
}); });
const data = executionStep?.dataOut; const data = executionStep?.dataOut;
const dataValue = get(data, keyPath); const dataValue = get(data, keyPath);
// Covers both arrays and objects
if (typeof dataValue === 'object') {
return JSON.stringify(dataValue);
}
return dataValue; return dataValue;
} }
return part; return part;
}) }).join('');
.join('');
return { // challenge the input to see if it is stringifies object or array
...result, try {
[key]: computedValue, const parsedValue = JSON.parse(computedValue);
};
if (typeof parsedValue === 'number') {
throw new Error('Use original unparsed value.');
}
return {
...result,
[key]: parsedValue,
};
} catch (error) {
return {
...result,
[key]: computedValue,
};
}
} }
if (Array.isArray(value)) { if (Array.isArray(value)) {

View File

@@ -1,65 +1,43 @@
import HttpError from '../../errors/http.js'; import HttpError from '../../errors/http.js';
import axios from '../axios-with-proxy.js'; import { createInstance } from '../axios-with-proxy.js';
// Mutates the `toInstance` by copying the request interceptors from `fromInstance`
const copyRequestInterceptors = (fromInstance, toInstance) => {
// Copy request interceptors
fromInstance.interceptors.request.forEach(interceptor => {
toInstance.interceptors.request.use(
interceptor.fulfilled,
interceptor.rejected,
{
synchronous: interceptor.synchronous,
runWhen: interceptor.runWhen
}
);
});
}
export default function createHttpClient({ $, baseURL, beforeRequest = [] }) { export default function createHttpClient({ $, baseURL, beforeRequest = [] }) {
const instance = axios.create({ async function interceptResponseError(error) {
baseURL, const { config, response } = error;
}); // Do not destructure `status` from `error.response` because it might not exist
const status = response?.status;
// 1. apply the beforeRequest functions from the app if (
instance.interceptors.request.use((requestConfig) => { // TODO: provide a `shouldRefreshToken` function in the app
const result = beforeRequest.reduce((newConfig, beforeRequestFunc) => { (status === 401 || status === 403) &&
return beforeRequestFunc($, newConfig); $.app.auth &&
}, requestConfig); $.app.auth.refreshToken &&
!$.app.auth.isRefreshTokenRequested
) {
$.app.auth.isRefreshTokenRequested = true;
await $.app.auth.refreshToken($);
return result; // retry the previous request before the expired token error
}); const newResponse = await instance.request(config);
$.app.auth.isRefreshTokenRequested = false;
// 2. inherit the request inceptors from the parent instance return newResponse;
copyRequestInterceptors(axios, instance);
instance.interceptors.response.use(
(response) => response,
async (error) => {
const { config, response } = error;
// Do not destructure `status` from `error.response` because it might not exist
const status = response?.status;
if (
// TODO: provide a `shouldRefreshToken` function in the app
(status === 401 || status === 403) &&
$.app.auth &&
$.app.auth.refreshToken &&
!$.app.auth.isRefreshTokenRequested
) {
$.app.auth.isRefreshTokenRequested = true;
await $.app.auth.refreshToken($);
// retry the previous request before the expired token error
const newResponse = await instance.request(config);
$.app.auth.isRefreshTokenRequested = false;
return newResponse;
}
throw new HttpError(error);
} }
);
throw new HttpError(error);
};
const instance = createInstance(
{
baseURL,
},
{
requestInterceptor: beforeRequest.map((originalBeforeRequest) => {
return async (requestConfig) => await originalBeforeRequest($, requestConfig);
}),
responseErrorInterceptor: interceptResponseError,
}
)
return instance; return instance;
} }

View File

@@ -63,6 +63,8 @@ export default async (flowId, request, response) => {
}); });
if (testRun) { if (testRun) {
response.status(204).end();
// in case of testing, we do not process the whole process. // in case of testing, we do not process the whole process.
continue; continue;
} }
@@ -74,6 +76,12 @@ export default async (flowId, request, response) => {
executionId, executionId,
}); });
if (actionStep.appKey === 'filter' && !actionExecutionStep.dataOut) {
response.status(422).end();
break;
}
if (actionStep.key === 'respondWith' && !response.headersSent) { if (actionStep.key === 'respondWith' && !response.headersSent) {
const { headers, statusCode, body } = actionExecutionStep.dataOut; const { headers, statusCode, body } = actionExecutionStep.dataOut;

View File

@@ -1,10 +1,11 @@
const infoMock = () => { const infoMock = () => {
return { return {
data: { data: {
isCloud: false,
isMation: false,
isEnterprise: true,
docsUrl: 'https://automatisch.io/docs', docsUrl: 'https://automatisch.io/docs',
installationCompleted: true,
isCloud: false,
isEnterprise: true,
isMation: false,
}, },
meta: { meta: {
count: 1, count: 1,

View File

@@ -14,7 +14,7 @@ connection in Automatisch. If any of the steps are outdated, please let us know!
7. Click on the **Select all** and then click on the **Create** button. 7. Click on the **Select all** and then click on the **Create** button.
8. Now, copy your **API key secret** and paste the key into the **API Key** field in Automatisch. 8. Now, copy your **API key secret** and paste the key into the **API Key** field in Automatisch.
9. Write any screen name to be displayed in Automatisch. 9. Write any screen name to be displayed in Automatisch.
10. You can find your project ID next to your project name. Paste the id into **Project ID** field in Automatsich. 10. You can find your project ID next to your project name. Paste the id into **Project ID** field in Automatisch.
11. If you are using self-hosted Appwrite project, you can paste the instace url into **Appwrite instance URL** field in Automatisch. 11. If you are using self-hosted Appwrite project, you can paste the instance url into **Appwrite instance URL** field in Automatisch.
12. Fill the host name field with the hostname of your instance URL. It's either `cloud.appwrite.io` or hostname of your instance URL. 12. Fill the host name field with the hostname of your instance URL. It's either `cloud.appwrite.io` or hostname of your instance URL.
13. Start using Appwrite integration with Automatisch! 13. Start using Appwrite integration with Automatisch!

View File

@@ -1,7 +1,7 @@
--- ---
favicon: /favicons/appwrite.svg favicon: /favicons/appwrite.svg
items: items:
- name: New documets - name: New documents
desc: Triggers when a new document is created. desc: Triggers when a new document is created.
--- ---

View File

@@ -8,11 +8,6 @@
name="description" name="description"
content="Build workflow automation without spending time and money. No code is required." content="Build workflow automation without spending time and money. No code is required."
/> />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!-- <!--
Notice the use of %PUBLIC_URL% in the tags above. Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build. It will be replaced with the URL of the `public` folder during the build.

View File

@@ -1,9 +0,0 @@
{
"short_name": "automatisch",
"name": "automatisch",
"description": "Build workflow automation without spending time and money. No code is required.",
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@@ -3,7 +3,6 @@ import * as React from 'react';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { useFormContext, useWatch } from 'react-hook-form'; import { useFormContext, useWatch } from 'react-hook-form';
import Stack from '@mui/material/Stack'; import Stack from '@mui/material/Stack';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import Divider from '@mui/material/Divider'; import Divider from '@mui/material/Divider';
import IconButton from '@mui/material/IconButton'; import IconButton from '@mui/material/IconButton';
@@ -12,6 +11,7 @@ import AddIcon from '@mui/icons-material/Add';
import useFormatMessage from 'hooks/useFormatMessage'; import useFormatMessage from 'hooks/useFormatMessage';
import InputCreator from 'components/InputCreator'; import InputCreator from 'components/InputCreator';
import { EditorContext } from 'contexts/Editor'; import { EditorContext } from 'contexts/Editor';
import { Grid } from '@mui/material';
const createGroupItem = () => ({ const createGroupItem = () => ({
key: '', key: '',
@@ -122,7 +122,7 @@ function FilterConditions(props) {
<React.Fragment> <React.Fragment>
<Stack sx={{ width: '100%' }} direction="column" spacing={2} mt={2}> <Stack sx={{ width: '100%' }} direction="column" spacing={2} mt={2}>
{groups?.map((group, groupIndex) => ( {groups?.map((group, groupIndex) => (
<> <React.Fragment key={groupIndex}>
{groupIndex !== 0 && <Divider />} {groupIndex !== 0 && <Divider />}
<Typography variant="subtitle2" gutterBottom> <Typography variant="subtitle2" gutterBottom>
@@ -133,19 +133,16 @@ function FilterConditions(props) {
</Typography> </Typography>
{group?.and?.map((groupItem, groupItemIndex) => ( {group?.and?.map((groupItem, groupItemIndex) => (
<Stack direction="row" spacing={2} key={`item-${groupItem.id}`}> <Grid container key={`item-${groupItem.id}`}>
<Stack <Grid
direction={{ xs: 'column', sm: 'row' }} container
spacing={{ xs: 2 }} spacing={2}
sx={{ display: 'flex', flex: 1 }} item
xs={12}
sm={11}
sx={{ order: { xs: 2, sm: 1 } }}
> >
<Box <Grid item xs={12} md={4}>
sx={{
display: 'flex',
flex: '1 0 0px',
maxWidth: ['100%', '33%'],
}}
>
<InputCreator <InputCreator
schema={createStringArgument({ schema={createStringArgument({
key: `or.${groupIndex}.and.${groupItemIndex}.key`, key: `or.${groupIndex}.and.${groupItemIndex}.key`,
@@ -155,15 +152,8 @@ function FilterConditions(props) {
stepId={stepId} stepId={stepId}
disabled={editorContext.readOnly} disabled={editorContext.readOnly}
/> />
</Box> </Grid>
<Grid item xs={12} md={4}>
<Box
sx={{
display: 'flex',
flex: '1 0 0px',
maxWidth: ['100%', '33%'],
}}
>
<InputCreator <InputCreator
schema={createDropdownArgument({ schema={createDropdownArgument({
key: `or.${groupIndex}.and.${groupItemIndex}.operator`, key: `or.${groupIndex}.and.${groupItemIndex}.operator`,
@@ -174,15 +164,8 @@ function FilterConditions(props) {
stepId={stepId} stepId={stepId}
disabled={editorContext.readOnly} disabled={editorContext.readOnly}
/> />
</Box> </Grid>
<Grid item xs={12} md={4}>
<Box
sx={{
display: 'flex',
flex: '1 0 0px',
maxWidth: ['100%', '33%'],
}}
>
<InputCreator <InputCreator
schema={createStringArgument({ schema={createStringArgument({
key: `or.${groupIndex}.and.${groupItemIndex}.value`, key: `or.${groupIndex}.and.${groupItemIndex}.value`,
@@ -192,18 +175,28 @@ function FilterConditions(props) {
stepId={stepId} stepId={stepId}
disabled={editorContext.readOnly} disabled={editorContext.readOnly}
/> />
</Box> </Grid>
</Stack> </Grid>
<Grid item xs={12} sm={1} sx={{ order: { xs: 1, sm: 2 } }}>
<IconButton <Stack justifyContent="center" alignItems="flex-end">
size="small" <IconButton
edge="start" size="small"
onClick={() => removeGroupItem(groupIndex, groupItemIndex)} edge="start"
sx={{ width: 61, height: 61 }} onClick={() =>
> removeGroupItem(groupIndex, groupItemIndex)
<RemoveIcon /> }
</IconButton> sx={{
</Stack> width: 40,
height: 40,
mb: { xs: 2, sm: 0 },
}}
disabled={groups.length === 1 && group.and.length === 1}
>
<RemoveIcon />
</IconButton>
</Stack>
</Grid>
</Grid>
))} ))}
<Stack spacing={1} direction="row"> <Stack spacing={1} direction="row">
@@ -227,7 +220,7 @@ function FilterConditions(props) {
</IconButton> </IconButton>
)} )}
</Stack> </Stack>
</> </React.Fragment>
))} ))}
</Stack> </Stack>
</React.Fragment> </React.Fragment>

View File

@@ -44,7 +44,7 @@ function InstallationForm() {
const handleOnRedirect = () => { const handleOnRedirect = () => {
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: ['automatisch', 'config'], queryKey: ['automatisch', 'info'],
}); });
}; };

View File

@@ -19,7 +19,13 @@ const process = ({ data, parentKey, index, parentLabel = '' }) => {
const value = joinBy('.', parentKey, index?.toString(), name); const value = joinBy('.', parentKey, index?.toString(), name);
if (Array.isArray(sampleValue)) { if (Array.isArray(sampleValue)) {
return sampleValue.flatMap((item, index) => const arrayItself = {
label,
value,
sampleValue: JSON.stringify(sampleValue),
};
const arrayItems = sampleValue.flatMap((item, index) =>
process({ process({
data: item, data: item,
parentKey: value, parentKey: value,
@@ -27,6 +33,9 @@ const process = ({ data, parentKey, index, parentLabel = '' }) => {
parentLabel: label, parentLabel: label,
}), }),
); );
// TODO: remove spreading
return [arrayItself, ...arrayItems];
} }
if (typeof sampleValue === 'object' && sampleValue !== null) { if (typeof sampleValue === 'object' && sampleValue !== null) {
@@ -36,6 +45,7 @@ const process = ({ data, parentKey, index, parentLabel = '' }) => {
parentLabel: label, parentLabel: label,
}); });
} }
return [ return [
{ {
label, label,

View File

@@ -29,14 +29,16 @@ import adminSettingsRoutes from './adminSettingsRoutes';
import Notifications from 'pages/Notifications'; import Notifications from 'pages/Notifications';
import useAutomatischConfig from 'hooks/useAutomatischConfig'; import useAutomatischConfig from 'hooks/useAutomatischConfig';
import useAuthentication from 'hooks/useAuthentication'; import useAuthentication from 'hooks/useAuthentication';
import useAutomatischInfo from 'hooks/useAutomatischInfo';
import Installation from 'pages/Installation'; import Installation from 'pages/Installation';
function Routes() { function Routes() {
const { data: configData, isSuccess } = useAutomatischConfig(); const { data: automatischInfo, isSuccess } = useAutomatischInfo();
const { data: configData } = useAutomatischConfig();
const { isAuthenticated } = useAuthentication(); const { isAuthenticated } = useAuthentication();
const config = configData?.data; const config = configData?.data;
const installed = isSuccess ? config?.['installation.completed'] === true : true; const installed = isSuccess ? automatischInfo.data.installationCompleted : true;
const navigate = useNavigate(); const navigate = useNavigate();
useEffect(() => { useEffect(() => {