Merge pull request #1665 from automatisch/ts-removal-in-web
refactor(web): remove typescript
This commit is contained in:
18
.eslintrc.js
18
.eslintrc.js
@@ -1,18 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
root: true,
|
|
||||||
parser: '@typescript-eslint/parser',
|
|
||||||
plugins: ['@typescript-eslint'],
|
|
||||||
extends: [
|
|
||||||
'eslint:recommended',
|
|
||||||
'plugin:@typescript-eslint/recommended',
|
|
||||||
'prettier',
|
|
||||||
],
|
|
||||||
overrides: [
|
|
||||||
{
|
|
||||||
files: ['**/*.test.ts', '**/test/**/*.ts'],
|
|
||||||
rules: {
|
|
||||||
'@typescript-eslint/ban-ts-comment': ['off'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
|||||||
- run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner."
|
- run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner."
|
||||||
- run: echo "🖥️ The workflow is now ready to test your code on the runner."
|
- run: echo "🖥️ The workflow is now ready to test your code on the runner."
|
||||||
- run: yarn --frozen-lockfile
|
- run: yarn --frozen-lockfile
|
||||||
- run: yarn lint
|
- run: cd packages/backend && yarn lint
|
||||||
- run: echo "🍏 This job's status is ${{ job.status }}."
|
- run: echo "🍏 This job's status is ${{ job.status }}."
|
||||||
start-backend-server:
|
start-backend-server:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
@@ -6,7 +6,6 @@
|
|||||||
"start": "lerna run --stream --parallel --scope=@*/{web,backend} dev",
|
"start": "lerna run --stream --parallel --scope=@*/{web,backend} dev",
|
||||||
"start:web": "lerna run --stream --scope=@*/web dev",
|
"start:web": "lerna run --stream --scope=@*/web dev",
|
||||||
"start:backend": "lerna run --stream --scope=@*/backend dev",
|
"start:backend": "lerna run --stream --scope=@*/backend dev",
|
||||||
"lint": "lerna run --no-bail --stream --parallel --scope=@*/{web,backend} lint",
|
|
||||||
"build:docs": "cd ./packages/docs && yarn install && yarn build"
|
"build:docs": "cd ./packages/docs && yarn install && yarn build"
|
||||||
},
|
},
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
@@ -21,8 +20,6 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@typescript-eslint/eslint-plugin": "^5.9.1",
|
|
||||||
"@typescript-eslint/parser": "^5.9.1",
|
|
||||||
"eslint": "^8.13.0",
|
"eslint": "^8.13.0",
|
||||||
"eslint-config-prettier": "^8.3.0",
|
"eslint-config-prettier": "^8.3.0",
|
||||||
"eslint-plugin-prettier": "^4.0.0",
|
"eslint-plugin-prettier": "^4.0.0",
|
||||||
|
@@ -11,7 +11,7 @@
|
|||||||
"start:worker": "node src/worker.js",
|
"start:worker": "node src/worker.js",
|
||||||
"pretest": "APP_ENV=test node ./test/setup/prepare-test-env.js",
|
"pretest": "APP_ENV=test node ./test/setup/prepare-test-env.js",
|
||||||
"test": "APP_ENV=test vitest run",
|
"test": "APP_ENV=test vitest run",
|
||||||
"lint": "eslint . --ignore-path ../../.eslintignore",
|
"lint": "eslint .",
|
||||||
"db:create": "node ./bin/database/create.js",
|
"db:create": "node ./bin/database/create.js",
|
||||||
"db:seed:user": "node ./bin/database/seed-user.js",
|
"db:seed:user": "node ./bin/database/seed-user.js",
|
||||||
"db:drop": "node ./bin/database/drop.js",
|
"db:drop": "node ./bin/database/drop.js",
|
||||||
@@ -95,7 +95,6 @@
|
|||||||
"url": "https://github.com/automatisch/automatisch/issues"
|
"url": "https://github.com/automatisch/automatisch/issues"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@typescript-eslint/utils": "^7.0.2",
|
|
||||||
"nodemon": "^2.0.13",
|
"nodemon": "^2.0.13",
|
||||||
"supertest": "^6.3.3",
|
"supertest": "^6.3.3",
|
||||||
"vitest": "^1.1.3"
|
"vitest": "^1.1.3"
|
||||||
|
@@ -28,8 +28,6 @@
|
|||||||
"@playwright/test": "^1.36.2"
|
"@playwright/test": "^1.36.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/eslint-plugin": "^5.9.1",
|
|
||||||
"@typescript-eslint/parser": "^5.9.1",
|
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
"eslint": "^8.13.0",
|
"eslint": "^8.13.0",
|
||||||
"eslint-config-prettier": "^8.3.0",
|
"eslint-config-prettier": "^8.3.0",
|
||||||
|
3
packages/web/.eslintignore
Normal file
3
packages/web/.eslintignore
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
node_modules
|
||||||
|
build
|
||||||
|
source
|
3
packages/web/.eslintrc.js
Normal file
3
packages/web/.eslintrc.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
module.exports = {
|
||||||
|
extends: ['react-app', 'prettier'],
|
||||||
|
};
|
6
packages/web/jsconfig.json
Normal file
6
packages/web/jsconfig.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": "src"
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
@@ -16,16 +16,9 @@
|
|||||||
"@testing-library/jest-dom": "^5.11.4",
|
"@testing-library/jest-dom": "^5.11.4",
|
||||||
"@testing-library/react": "^11.1.0",
|
"@testing-library/react": "^11.1.0",
|
||||||
"@testing-library/user-event": "^12.1.10",
|
"@testing-library/user-event": "^12.1.10",
|
||||||
"@types/jest": "^26.0.15",
|
|
||||||
"@types/lodash": "^4.14.182",
|
|
||||||
"@types/luxon": "^2.0.8",
|
|
||||||
"@types/node": "^12.0.0",
|
|
||||||
"@types/react": "^17.0.0",
|
|
||||||
"@types/react-dom": "^17.0.0",
|
|
||||||
"@types/react-window": "^1.8.5",
|
|
||||||
"@types/uuid": "^9.0.0",
|
|
||||||
"clipboard-copy": "^4.0.1",
|
"clipboard-copy": "^4.0.1",
|
||||||
"compare-versions": "^4.1.3",
|
"compare-versions": "^4.1.3",
|
||||||
|
"eslint-plugin-react": "^7.33.2",
|
||||||
"graphql": "^15.6.0",
|
"graphql": "^15.6.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"luxon": "^2.3.1",
|
"luxon": "^2.3.1",
|
||||||
@@ -42,7 +35,6 @@
|
|||||||
"slate": "^0.94.1",
|
"slate": "^0.94.1",
|
||||||
"slate-history": "^0.93.0",
|
"slate-history": "^0.93.0",
|
||||||
"slate-react": "^0.94.2",
|
"slate-react": "^0.94.2",
|
||||||
"typescript": "^4.6.3",
|
|
||||||
"uuid": "^9.0.0",
|
"uuid": "^9.0.0",
|
||||||
"web-vitals": "^1.0.1",
|
"web-vitals": "^1.0.1",
|
||||||
"yup": "^0.32.11"
|
"yup": "^0.32.11"
|
||||||
@@ -54,7 +46,7 @@
|
|||||||
"build:watch": "yarn nodemon --exec react-scripts build --watch 'src/**/*.ts' --watch 'public/**/*' --ext ts,html",
|
"build:watch": "yarn nodemon --exec react-scripts build --watch 'src/**/*.ts' --watch 'public/**/*' --ext ts,html",
|
||||||
"test": "react-scripts test",
|
"test": "react-scripts test",
|
||||||
"eject": "react-scripts eject",
|
"eject": "react-scripts eject",
|
||||||
"lint": "eslint . --ignore-path ../../.eslintignore",
|
"lint": "eslint .",
|
||||||
"prepack": "REACT_APP_GRAPHQL_URL=/graphql yarn build"
|
"prepack": "REACT_APP_GRAPHQL_URL=/graphql yarn build"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
@@ -87,5 +79,15 @@
|
|||||||
},
|
},
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"eslint-config-prettier": "^9.1.0",
|
||||||
|
"eslint-config-react-app": "^7.0.1",
|
||||||
|
"prettier": "^3.2.5"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"extends": [
|
||||||
|
"./.eslintrc.js"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -8,12 +8,10 @@ import CreateRole from 'pages/CreateRole/index.ee';
|
|||||||
import EditRole from 'pages/EditRole/index.ee';
|
import EditRole from 'pages/EditRole/index.ee';
|
||||||
import Authentication from 'pages/Authentication';
|
import Authentication from 'pages/Authentication';
|
||||||
import UserInterface from 'pages/UserInterface';
|
import UserInterface from 'pages/UserInterface';
|
||||||
|
|
||||||
import * as URLS from 'config/urls';
|
import * as URLS from 'config/urls';
|
||||||
import Can from 'components/Can';
|
import Can from 'components/Can';
|
||||||
import AdminApplications from 'pages/AdminApplications';
|
import AdminApplications from 'pages/AdminApplications';
|
||||||
import AdminApplication from 'pages/AdminApplication';
|
import AdminApplication from 'pages/AdminApplication';
|
||||||
|
|
||||||
// TODO: consider introducing redirections to `/` as fallback
|
// TODO: consider introducing redirections to `/` as fallback
|
||||||
export default (
|
export default (
|
||||||
<>
|
<>
|
@@ -1,40 +1,24 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import MenuItem from '@mui/material/MenuItem';
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
import Menu, { MenuProps } from '@mui/material/Menu';
|
import Menu from '@mui/material/Menu';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import Can from 'components/Can';
|
import Can from 'components/Can';
|
||||||
import apolloClient from 'graphql/client';
|
import apolloClient from 'graphql/client';
|
||||||
import * as URLS from 'config/urls';
|
import * as URLS from 'config/urls';
|
||||||
import useAuthentication from 'hooks/useAuthentication';
|
import useAuthentication from 'hooks/useAuthentication';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
|
function AccountDropdownMenu(props) {
|
||||||
type AccountDropdownMenuProps = {
|
|
||||||
open: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
anchorEl: MenuProps['anchorEl'];
|
|
||||||
id: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
function AccountDropdownMenu(
|
|
||||||
props: AccountDropdownMenuProps
|
|
||||||
): React.ReactElement {
|
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
const authentication = useAuthentication();
|
const authentication = useAuthentication();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const { open, onClose, anchorEl, id } = props;
|
const { open, onClose, anchorEl, id } = props;
|
||||||
|
|
||||||
const logout = async () => {
|
const logout = async () => {
|
||||||
authentication.updateToken('');
|
authentication.updateToken('');
|
||||||
await apolloClient.clearStore();
|
await apolloClient.clearStore();
|
||||||
|
|
||||||
onClose();
|
onClose();
|
||||||
|
|
||||||
navigate(URLS.LOGIN);
|
navigate(URLS.LOGIN);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu
|
<Menu
|
||||||
anchorEl={anchorEl}
|
anchorEl={anchorEl}
|
||||||
@@ -56,10 +40,7 @@ function AccountDropdownMenu(
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|
||||||
<Can I="read" a="User">
|
<Can I="read" a="User">
|
||||||
<MenuItem
|
<MenuItem component={Link} to={URLS.ADMIN_SETTINGS_DASHBOARD}>
|
||||||
component={Link}
|
|
||||||
to={URLS.ADMIN_SETTINGS_DASHBOARD}
|
|
||||||
>
|
|
||||||
{formatMessage('accountDropdownMenu.adminSettings')}
|
{formatMessage('accountDropdownMenu.adminSettings')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</Can>
|
</Can>
|
||||||
@@ -70,5 +51,4 @@ function AccountDropdownMenu(
|
|||||||
</Menu>
|
</Menu>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AccountDropdownMenu;
|
export default AccountDropdownMenu;
|
@@ -1,4 +1,3 @@
|
|||||||
import type { IApp, IField, IJSONObject } from 'types';
|
|
||||||
import LoadingButton from '@mui/lab/LoadingButton';
|
import LoadingButton from '@mui/lab/LoadingButton';
|
||||||
import Alert from '@mui/material/Alert';
|
import Alert from '@mui/material/Alert';
|
||||||
import Dialog from '@mui/material/Dialog';
|
import Dialog from '@mui/material/Dialog';
|
||||||
@@ -6,32 +5,21 @@ import DialogContent from '@mui/material/DialogContent';
|
|||||||
import DialogContentText from '@mui/material/DialogContentText';
|
import DialogContentText from '@mui/material/DialogContentText';
|
||||||
import DialogTitle from '@mui/material/DialogTitle';
|
import DialogTitle from '@mui/material/DialogTitle';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { FieldValues, SubmitHandler } from 'react-hook-form';
|
|
||||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
import AppAuthClientsDialog from 'components/AppAuthClientsDialog/index.ee';
|
import AppAuthClientsDialog from 'components/AppAuthClientsDialog/index.ee';
|
||||||
import InputCreator from 'components/InputCreator';
|
import InputCreator from 'components/InputCreator';
|
||||||
import * as URLS from 'config/urls';
|
import * as URLS from 'config/urls';
|
||||||
import useAuthenticateApp from 'hooks/useAuthenticateApp.ee';
|
import useAuthenticateApp from 'hooks/useAuthenticateApp.ee';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
import { generateExternalLink } from '../../helpers/translationValues';
|
import { generateExternalLink } from 'helpers/translationValues';
|
||||||
import { Form } from './style';
|
import { Form } from './style';
|
||||||
|
export default function AddAppConnection(props) {
|
||||||
type AddAppConnectionProps = {
|
|
||||||
onClose: (response: Record<string, unknown>) => void;
|
|
||||||
application: IApp;
|
|
||||||
connectionId?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function AddAppConnection(
|
|
||||||
props: AddAppConnectionProps
|
|
||||||
): React.ReactElement {
|
|
||||||
const { application, connectionId, onClose } = props;
|
const { application, connectionId, onClose } = props;
|
||||||
const { name, authDocUrl, key, auth } = application;
|
const { name, authDocUrl, key, auth } = application;
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
const [error, setError] = React.useState<IJSONObject | null>(null);
|
const [error, setError] = React.useState(null);
|
||||||
const [inProgress, setInProgress] = React.useState(false);
|
const [inProgress, setInProgress] = React.useState(false);
|
||||||
const hasConnection = Boolean(connectionId);
|
const hasConnection = Boolean(connectionId);
|
||||||
const useShared = searchParams.get('shared') === 'true';
|
const useShared = searchParams.get('shared') === 'true';
|
||||||
@@ -42,7 +30,6 @@ export default function AddAppConnection(
|
|||||||
appAuthClientId,
|
appAuthClientId,
|
||||||
useShared: !!appAuthClientId,
|
useShared: !!appAuthClientId,
|
||||||
});
|
});
|
||||||
|
|
||||||
React.useEffect(function relayProviderData() {
|
React.useEffect(function relayProviderData() {
|
||||||
if (window.opener) {
|
if (window.opener) {
|
||||||
window.opener.postMessage({
|
window.opener.postMessage({
|
||||||
@@ -52,51 +39,41 @@ export default function AddAppConnection(
|
|||||||
window.close();
|
window.close();
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
React.useEffect(
|
React.useEffect(
|
||||||
function initiateSharedAuthenticationForGivenAuthClient() {
|
function initiateSharedAuthenticationForGivenAuthClient() {
|
||||||
if (!appAuthClientId) return;
|
if (!appAuthClientId) return;
|
||||||
if (!authenticate) return;
|
if (!authenticate) return;
|
||||||
|
|
||||||
const asyncAuthenticate = async () => {
|
const asyncAuthenticate = async () => {
|
||||||
await authenticate();
|
await authenticate();
|
||||||
|
|
||||||
navigate(URLS.APP_CONNECTIONS(key));
|
navigate(URLS.APP_CONNECTIONS(key));
|
||||||
};
|
};
|
||||||
|
|
||||||
asyncAuthenticate();
|
asyncAuthenticate();
|
||||||
},
|
},
|
||||||
[appAuthClientId, authenticate]
|
[appAuthClientId, authenticate],
|
||||||
);
|
);
|
||||||
|
const handleClientClick = (appAuthClientId) =>
|
||||||
const handleClientClick = (appAuthClientId: string) =>
|
|
||||||
navigate(URLS.APP_ADD_CONNECTION_WITH_AUTH_CLIENT_ID(key, appAuthClientId));
|
navigate(URLS.APP_ADD_CONNECTION_WITH_AUTH_CLIENT_ID(key, appAuthClientId));
|
||||||
|
|
||||||
const handleAuthClientsDialogClose = () =>
|
const handleAuthClientsDialogClose = () =>
|
||||||
navigate(URLS.APP_CONNECTIONS(key));
|
navigate(URLS.APP_CONNECTIONS(key));
|
||||||
|
const submitHandler = React.useCallback(
|
||||||
const submitHandler: SubmitHandler<FieldValues> = React.useCallback(
|
|
||||||
async (data) => {
|
async (data) => {
|
||||||
if (!authenticate) return;
|
if (!authenticate) return;
|
||||||
|
|
||||||
setInProgress(true);
|
setInProgress(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await authenticate({
|
const response = await authenticate({
|
||||||
fields: data,
|
fields: data,
|
||||||
});
|
});
|
||||||
onClose(response as Record<string, unknown>);
|
onClose(response);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = err as IJSONObject;
|
const error = err;
|
||||||
console.log(error);
|
console.log(error);
|
||||||
setError((error.graphQLErrors as IJSONObject[])?.[0]);
|
setError(error.graphQLErrors?.[0]);
|
||||||
} finally {
|
} finally {
|
||||||
setInProgress(false);
|
setInProgress(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[authenticate]
|
[authenticate],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (useShared)
|
if (useShared)
|
||||||
return (
|
return (
|
||||||
<AppAuthClientsDialog
|
<AppAuthClientsDialog
|
||||||
@@ -105,9 +82,7 @@ export default function AddAppConnection(
|
|||||||
onClientClick={handleClientClick}
|
onClientClick={handleClientClick}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (appAuthClientId) return <React.Fragment />;
|
if (appAuthClientId) return <React.Fragment />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={true} onClose={onClose} data-test="add-app-connection-dialog">
|
<Dialog open={true} onClose={onClose} data-test="add-app-connection-dialog">
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
@@ -142,7 +117,7 @@ export default function AddAppConnection(
|
|||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogContentText tabIndex={-1} component="div">
|
<DialogContentText tabIndex={-1} component="div">
|
||||||
<Form onSubmit={submitHandler}>
|
<Form onSubmit={submitHandler}>
|
||||||
{auth?.fields?.map((field: IField) => (
|
{auth?.fields?.map((field) => (
|
||||||
<InputCreator key={field.key} schema={field} />
|
<InputCreator key={field.key} schema={field} />
|
||||||
))}
|
))}
|
||||||
|
|
@@ -1,6 +1,5 @@
|
|||||||
import { styled } from '@mui/material/styles';
|
import { styled } from '@mui/material/styles';
|
||||||
import BaseForm from 'components/Form';
|
import BaseForm from 'components/Form';
|
||||||
|
|
||||||
export const Form = styled(BaseForm)(({ theme }) => ({
|
export const Form = styled(BaseForm)(({ theme }) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
@@ -19,67 +19,52 @@ import InputLabel from '@mui/material/InputLabel';
|
|||||||
import OutlinedInput from '@mui/material/OutlinedInput';
|
import OutlinedInput from '@mui/material/OutlinedInput';
|
||||||
import FormControl from '@mui/material/FormControl';
|
import FormControl from '@mui/material/FormControl';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import type { IApp } from 'types';
|
|
||||||
|
|
||||||
import * as URLS from 'config/urls';
|
import * as URLS from 'config/urls';
|
||||||
import AppIcon from 'components/AppIcon';
|
import AppIcon from 'components/AppIcon';
|
||||||
import { GET_APPS } from 'graphql/queries/get-apps';
|
import { GET_APPS } from 'graphql/queries/get-apps';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
|
function createConnectionOrFlow(appKey, supportsConnections = false) {
|
||||||
function createConnectionOrFlow(appKey: string, supportsConnections = false) {
|
|
||||||
if (!supportsConnections) {
|
if (!supportsConnections) {
|
||||||
return URLS.CREATE_FLOW_WITH_APP(appKey);
|
return URLS.CREATE_FLOW_WITH_APP(appKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
return URLS.APP_ADD_CONNECTION(appKey);
|
return URLS.APP_ADD_CONNECTION(appKey);
|
||||||
}
|
}
|
||||||
|
export default function AddNewAppConnection(props) {
|
||||||
type AddNewAppConnectionProps = {
|
|
||||||
onClose: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function AddNewAppConnection(
|
|
||||||
props: AddNewAppConnectionProps
|
|
||||||
): React.ReactElement {
|
|
||||||
const { onClose } = props;
|
const { onClose } = props;
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('sm'));
|
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('sm'));
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
const [appName, setAppName] = React.useState<string | null>(null);
|
const [appName, setAppName] = React.useState(null);
|
||||||
const [loading, setLoading] = React.useState(false);
|
const [loading, setLoading] = React.useState(false);
|
||||||
const [getApps, { data }] = useLazyQuery(GET_APPS, {
|
const [getApps, { data }] = useLazyQuery(GET_APPS, {
|
||||||
onCompleted: () => {
|
onCompleted: () => {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const fetchData = React.useMemo(
|
const fetchData = React.useMemo(
|
||||||
() => debounce((name) => getApps({ variables: { name } }), 300),
|
() => debounce((name) => getApps({ variables: { name } }), 300),
|
||||||
[getApps]
|
[getApps],
|
||||||
);
|
);
|
||||||
|
|
||||||
React.useEffect(
|
React.useEffect(
|
||||||
function fetchAppsOnAppNameChange() {
|
function fetchAppsOnAppNameChange() {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
fetchData(appName);
|
fetchData(appName);
|
||||||
},
|
},
|
||||||
[fetchData, appName]
|
[fetchData, appName],
|
||||||
);
|
);
|
||||||
|
|
||||||
React.useEffect(function cancelDebounceOnUnmount() {
|
React.useEffect(function cancelDebounceOnUnmount() {
|
||||||
return () => {
|
return () => {
|
||||||
fetchData.cancel();
|
fetchData.cancel();
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
open={true}
|
open={true}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
maxWidth="sm"
|
maxWidth="sm"
|
||||||
fullWidth
|
fullWidth
|
||||||
data-test="add-app-connection-dialog">
|
data-test="add-app-connection-dialog"
|
||||||
|
>
|
||||||
<DialogTitle>{formatMessage('apps.addNewAppConnection')}</DialogTitle>
|
<DialogTitle>{formatMessage('apps.addNewAppConnection')}</DialogTitle>
|
||||||
|
|
||||||
<Box px={3}>
|
<Box px={3}>
|
||||||
@@ -123,7 +108,7 @@ export default function AddNewAppConnection(
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{!loading &&
|
{!loading &&
|
||||||
data?.getApps?.map((app: IApp) => (
|
data?.getApps?.map((app) => (
|
||||||
<ListItem disablePadding key={app.name} data-test="app-list-item">
|
<ListItem disablePadding key={app.name} data-test="app-list-item">
|
||||||
<ListItemButton
|
<ListItemButton
|
||||||
component={Link}
|
component={Link}
|
@@ -1,5 +1,4 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import type { IField } from 'types';
|
|
||||||
import LoadingButton from '@mui/lab/LoadingButton';
|
import LoadingButton from '@mui/lab/LoadingButton';
|
||||||
import Alert from '@mui/material/Alert';
|
import Alert from '@mui/material/Alert';
|
||||||
import Dialog from '@mui/material/Dialog';
|
import Dialog from '@mui/material/Dialog';
|
||||||
@@ -7,32 +6,12 @@ import DialogContent from '@mui/material/DialogContent';
|
|||||||
import DialogContentText from '@mui/material/DialogContentText';
|
import DialogContentText from '@mui/material/DialogContentText';
|
||||||
import DialogTitle from '@mui/material/DialogTitle';
|
import DialogTitle from '@mui/material/DialogTitle';
|
||||||
import CircularProgress from '@mui/material/CircularProgress';
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
import { FieldValues, SubmitHandler } from 'react-hook-form';
|
|
||||||
import type { UseFormProps } from 'react-hook-form';
|
|
||||||
import type { ApolloError } from '@apollo/client';
|
|
||||||
|
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
import InputCreator from 'components/InputCreator';
|
import InputCreator from 'components/InputCreator';
|
||||||
import Switch from 'components/Switch';
|
import Switch from 'components/Switch';
|
||||||
import TextField from 'components/TextField';
|
import TextField from 'components/TextField';
|
||||||
|
|
||||||
import { Form } from './style';
|
import { Form } from './style';
|
||||||
|
export default function AdminApplicationAuthClientDialog(props) {
|
||||||
type AdminApplicationAuthClientDialogProps = {
|
|
||||||
title: string;
|
|
||||||
authFields?: IField[];
|
|
||||||
defaultValues: UseFormProps['defaultValues'];
|
|
||||||
loading: boolean;
|
|
||||||
submitting: boolean;
|
|
||||||
disabled?: boolean;
|
|
||||||
error?: ApolloError;
|
|
||||||
submitHandler: SubmitHandler<FieldValues>;
|
|
||||||
onClose: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function AdminApplicationAuthClientDialog(
|
|
||||||
props: AdminApplicationAuthClientDialogProps
|
|
||||||
): React.ReactElement {
|
|
||||||
const {
|
const {
|
||||||
error,
|
error,
|
||||||
onClose,
|
onClose,
|
||||||
@@ -45,7 +24,6 @@ export default function AdminApplicationAuthClientDialog(
|
|||||||
disabled = false,
|
disabled = false,
|
||||||
} = props;
|
} = props;
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={true} onClose={onClose}>
|
<Dialog open={true} onClose={onClose}>
|
||||||
<DialogTitle>{title}</DialogTitle>
|
<DialogTitle>{title}</DialogTitle>
|
||||||
@@ -80,7 +58,7 @@ export default function AdminApplicationAuthClientDialog(
|
|||||||
label={formatMessage('authClient.inputName')}
|
label={formatMessage('authClient.inputName')}
|
||||||
fullWidth
|
fullWidth
|
||||||
/>
|
/>
|
||||||
{authFields?.map((field: IField) => (
|
{authFields?.map((field) => (
|
||||||
<InputCreator key={field.key} schema={field} />
|
<InputCreator key={field.key} schema={field} />
|
||||||
))}
|
))}
|
||||||
<LoadingButton
|
<LoadingButton
|
@@ -1,6 +1,5 @@
|
|||||||
import { styled } from '@mui/material/styles';
|
import { styled } from '@mui/material/styles';
|
||||||
import BaseForm from 'components/Form';
|
import BaseForm from 'components/Form';
|
||||||
|
|
||||||
export const Form = styled(BaseForm)(({ theme }) => ({
|
export const Form = styled(BaseForm)(({ theme }) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
@@ -7,27 +7,16 @@ import CardContent from '@mui/material/CardContent';
|
|||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import Chip from '@mui/material/Chip';
|
import Chip from '@mui/material/Chip';
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
|
|
||||||
import * as URLS from 'config/urls';
|
import * as URLS from 'config/urls';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
import useAppAuthClients from 'hooks/useAppAuthClients.ee';
|
import useAppAuthClients from 'hooks/useAppAuthClients.ee';
|
||||||
|
|
||||||
import NoResultFound from 'components/NoResultFound';
|
import NoResultFound from 'components/NoResultFound';
|
||||||
|
function AdminApplicationAuthClients(props) {
|
||||||
type AdminApplicationAuthClientsProps = {
|
|
||||||
appKey: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
function AdminApplicationAuthClients(
|
|
||||||
props: AdminApplicationAuthClientsProps
|
|
||||||
): React.ReactElement {
|
|
||||||
const { appKey } = props;
|
const { appKey } = props;
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
const { appAuthClients, loading } = useAppAuthClients({ appKey });
|
const { appAuthClients, loading } = useAppAuthClients({ appKey });
|
||||||
|
|
||||||
if (loading)
|
if (loading)
|
||||||
return <CircularProgress sx={{ display: 'block', margin: '20px auto' }} />;
|
return <CircularProgress sx={{ display: 'block', margin: '20px auto' }} />;
|
||||||
|
|
||||||
if (!appAuthClients?.length) {
|
if (!appAuthClients?.length) {
|
||||||
return (
|
return (
|
||||||
<NoResultFound
|
<NoResultFound
|
||||||
@@ -36,7 +25,6 @@ function AdminApplicationAuthClients(
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sortedAuthClients = appAuthClients.slice().sort((a, b) => {
|
const sortedAuthClients = appAuthClients.slice().sort((a, b) => {
|
||||||
if (a.id < b.id) {
|
if (a.id < b.id) {
|
||||||
return -1;
|
return -1;
|
||||||
@@ -46,7 +34,6 @@ function AdminApplicationAuthClients(
|
|||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{sortedAuthClients.map((client) => (
|
{sortedAuthClients.map((client) => (
|
||||||
@@ -67,7 +54,7 @@ function AdminApplicationAuthClients(
|
|||||||
label={formatMessage(
|
label={formatMessage(
|
||||||
client?.active
|
client?.active
|
||||||
? 'adminAppsAuthClients.statusActive'
|
? 'adminAppsAuthClients.statusActive'
|
||||||
: 'adminAppsAuthClients.statusInactive'
|
: 'adminAppsAuthClients.statusInactive',
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -85,5 +72,4 @@ function AdminApplicationAuthClients(
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AdminApplicationAuthClients;
|
export default AdminApplicationAuthClients;
|
@@ -1,24 +1,11 @@
|
|||||||
import React, { useCallback, useMemo } from 'react';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
import type { IApp } from 'types';
|
|
||||||
import { FieldValues, SubmitHandler } from 'react-hook-form';
|
|
||||||
import { useMutation } from '@apollo/client';
|
import { useMutation } from '@apollo/client';
|
||||||
import { CREATE_APP_CONFIG } from 'graphql/mutations/create-app-config';
|
import { CREATE_APP_CONFIG } from 'graphql/mutations/create-app-config';
|
||||||
import { CREATE_APP_AUTH_CLIENT } from 'graphql/mutations/create-app-auth-client';
|
import { CREATE_APP_AUTH_CLIENT } from 'graphql/mutations/create-app-auth-client';
|
||||||
|
|
||||||
import useAppConfig from 'hooks/useAppConfig.ee';
|
import useAppConfig from 'hooks/useAppConfig.ee';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
|
|
||||||
import AdminApplicationAuthClientDialog from 'components/AdminApplicationAuthClientDialog';
|
import AdminApplicationAuthClientDialog from 'components/AdminApplicationAuthClientDialog';
|
||||||
|
export default function AdminApplicationCreateAuthClient(props) {
|
||||||
type AdminApplicationCreateAuthClientProps = {
|
|
||||||
appKey: string;
|
|
||||||
application: IApp;
|
|
||||||
onClose: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function AdminApplicationCreateAuthClient(
|
|
||||||
props: AdminApplicationCreateAuthClientProps
|
|
||||||
): React.ReactElement {
|
|
||||||
const { appKey, application, onClose } = props;
|
const { appKey, application, onClose } = props;
|
||||||
const { auth } = application;
|
const { auth } = application;
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
@@ -37,10 +24,8 @@ export default function AdminApplicationCreateAuthClient(
|
|||||||
refetchQueries: ['GetAppAuthClients'],
|
refetchQueries: ['GetAppAuthClients'],
|
||||||
context: { autoSnackbar: false },
|
context: { autoSnackbar: false },
|
||||||
});
|
});
|
||||||
|
const submitHandler = async (values) => {
|
||||||
const submitHandler: SubmitHandler<FieldValues> = async (values) => {
|
|
||||||
let appConfigId = appConfig?.id;
|
let appConfigId = appConfig?.id;
|
||||||
|
|
||||||
if (!appConfigId) {
|
if (!appConfigId) {
|
||||||
const { data: appConfigData } = await createAppConfig({
|
const { data: appConfigData } = await createAppConfig({
|
||||||
variables: {
|
variables: {
|
||||||
@@ -54,9 +39,7 @@ export default function AdminApplicationCreateAuthClient(
|
|||||||
});
|
});
|
||||||
appConfigId = appConfigData.createAppConfig.id;
|
appConfigId = appConfigData.createAppConfig.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { name, active, ...formattedAuthDefaults } = values;
|
const { name, active, ...formattedAuthDefaults } = values;
|
||||||
|
|
||||||
await createAppAuthClient({
|
await createAppAuthClient({
|
||||||
variables: {
|
variables: {
|
||||||
input: {
|
input: {
|
||||||
@@ -67,17 +50,13 @@ export default function AdminApplicationCreateAuthClient(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAuthFieldsDefaultValues = useCallback(() => {
|
const getAuthFieldsDefaultValues = useCallback(() => {
|
||||||
if (!auth?.fields) {
|
if (!auth?.fields) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
const defaultValues: {
|
const defaultValues = {};
|
||||||
[key: string]: any;
|
|
||||||
} = {};
|
|
||||||
auth.fields.forEach((field) => {
|
auth.fields.forEach((field) => {
|
||||||
if (field.value || field.type !== 'string') {
|
if (field.value || field.type !== 'string') {
|
||||||
defaultValues[field.key] = field.value;
|
defaultValues[field.key] = field.value;
|
||||||
@@ -87,16 +66,14 @@ export default function AdminApplicationCreateAuthClient(
|
|||||||
});
|
});
|
||||||
return defaultValues;
|
return defaultValues;
|
||||||
}, [auth?.fields]);
|
}, [auth?.fields]);
|
||||||
|
|
||||||
const defaultValues = useMemo(
|
const defaultValues = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
name: '',
|
name: '',
|
||||||
active: false,
|
active: false,
|
||||||
...getAuthFieldsDefaultValues(),
|
...getAuthFieldsDefaultValues(),
|
||||||
}),
|
}),
|
||||||
[getAuthFieldsDefaultValues]
|
[getAuthFieldsDefaultValues],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminApplicationAuthClientDialog
|
<AdminApplicationAuthClientDialog
|
||||||
onClose={onClose}
|
onClose={onClose}
|
@@ -6,39 +6,28 @@ import Paper from '@mui/material/Paper';
|
|||||||
import Stack from '@mui/material/Stack';
|
import Stack from '@mui/material/Stack';
|
||||||
import LoadingButton from '@mui/lab/LoadingButton';
|
import LoadingButton from '@mui/lab/LoadingButton';
|
||||||
import { useMutation } from '@apollo/client';
|
import { useMutation } from '@apollo/client';
|
||||||
|
|
||||||
import { CREATE_APP_CONFIG } from 'graphql/mutations/create-app-config';
|
import { CREATE_APP_CONFIG } from 'graphql/mutations/create-app-config';
|
||||||
import { UPDATE_APP_CONFIG } from 'graphql/mutations/update-app-config';
|
import { UPDATE_APP_CONFIG } from 'graphql/mutations/update-app-config';
|
||||||
|
|
||||||
import Form from 'components/Form';
|
import Form from 'components/Form';
|
||||||
import { Switch } from './style';
|
import { Switch } from './style';
|
||||||
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
|
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
|
||||||
|
function AdminApplicationSettings(props) {
|
||||||
type AdminApplicationSettingsProps = {
|
|
||||||
appKey: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
function AdminApplicationSettings(
|
|
||||||
props: AdminApplicationSettingsProps
|
|
||||||
): React.ReactElement {
|
|
||||||
const { appConfig, loading } = useAppConfig(props.appKey);
|
const { appConfig, loading } = useAppConfig(props.appKey);
|
||||||
const [createAppConfig, { loading: loadingCreateAppConfig }] = useMutation(
|
const [createAppConfig, { loading: loadingCreateAppConfig }] = useMutation(
|
||||||
CREATE_APP_CONFIG,
|
CREATE_APP_CONFIG,
|
||||||
{
|
{
|
||||||
refetchQueries: ['GetAppConfig'],
|
refetchQueries: ['GetAppConfig'],
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
const [updateAppConfig, { loading: loadingUpdateAppConfig }] = useMutation(
|
const [updateAppConfig, { loading: loadingUpdateAppConfig }] = useMutation(
|
||||||
UPDATE_APP_CONFIG,
|
UPDATE_APP_CONFIG,
|
||||||
{
|
{
|
||||||
refetchQueries: ['GetAppConfig'],
|
refetchQueries: ['GetAppConfig'],
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
const enqueueSnackbar = useEnqueueSnackbar();
|
const enqueueSnackbar = useEnqueueSnackbar();
|
||||||
|
const handleSubmit = async (values) => {
|
||||||
const handleSubmit = async (values: any) => {
|
|
||||||
try {
|
try {
|
||||||
if (!appConfig) {
|
if (!appConfig) {
|
||||||
await createAppConfig({
|
await createAppConfig({
|
||||||
@@ -56,23 +45,21 @@ function AdminApplicationSettings(
|
|||||||
enqueueSnackbar(formatMessage('adminAppsSettings.successfullySaved'), {
|
enqueueSnackbar(formatMessage('adminAppsSettings.successfullySaved'), {
|
||||||
variant: 'success',
|
variant: 'success',
|
||||||
SnackbarProps: {
|
SnackbarProps: {
|
||||||
'data-test': 'snackbar-save-admin-apps-settings-success'
|
'data-test': 'snackbar-save-admin-apps-settings-success',
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error('Failed while saving!');
|
throw new Error('Failed while saving!');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultValues = useMemo(
|
const defaultValues = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
allowCustomConnection: appConfig?.allowCustomConnection || false,
|
allowCustomConnection: appConfig?.allowCustomConnection || false,
|
||||||
shared: appConfig?.shared || false,
|
shared: appConfig?.shared || false,
|
||||||
disabled: appConfig?.disabled || false,
|
disabled: appConfig?.disabled || false,
|
||||||
}),
|
}),
|
||||||
[appConfig]
|
[appConfig],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form
|
<Form
|
||||||
defaultValues={defaultValues}
|
defaultValues={defaultValues}
|
||||||
@@ -122,5 +109,4 @@ function AdminApplicationSettings(
|
|||||||
></Form>
|
></Form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AdminApplicationSettings;
|
export default AdminApplicationSettings;
|
@@ -1,6 +1,5 @@
|
|||||||
import { styled } from '@mui/material/styles';
|
import { styled } from '@mui/material/styles';
|
||||||
import SwitchBase from 'components/Switch';
|
import SwitchBase from 'components/Switch';
|
||||||
|
|
||||||
export const Switch = styled(SwitchBase)`
|
export const Switch = styled(SwitchBase)`
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin: 0;
|
margin: 0;
|
@@ -1,31 +1,18 @@
|
|||||||
import React, { useCallback, useMemo } from 'react';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import type { IApp } from 'types';
|
|
||||||
import { FieldValues, SubmitHandler } from 'react-hook-form';
|
|
||||||
import { useMutation } from '@apollo/client';
|
import { useMutation } from '@apollo/client';
|
||||||
import { UPDATE_APP_AUTH_CLIENT } from 'graphql/mutations/update-app-auth-client';
|
import { UPDATE_APP_AUTH_CLIENT } from 'graphql/mutations/update-app-auth-client';
|
||||||
|
|
||||||
import useAppAuthClient from 'hooks/useAppAuthClient.ee';
|
import useAppAuthClient from 'hooks/useAppAuthClient.ee';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
import AdminApplicationAuthClientDialog from 'components/AdminApplicationAuthClientDialog';
|
import AdminApplicationAuthClientDialog from 'components/AdminApplicationAuthClientDialog';
|
||||||
|
export default function AdminApplicationUpdateAuthClient(props) {
|
||||||
type AdminApplicationUpdateAuthClientProps = {
|
|
||||||
application: IApp;
|
|
||||||
onClose: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function AdminApplicationUpdateAuthClient(
|
|
||||||
props: AdminApplicationUpdateAuthClientProps
|
|
||||||
): React.ReactElement {
|
|
||||||
const { application, onClose } = props;
|
const { application, onClose } = props;
|
||||||
const { auth } = application;
|
const { auth } = application;
|
||||||
const authFields = auth?.fields?.map((field) => ({
|
const authFields = auth?.fields?.map((field) => ({
|
||||||
...field,
|
...field,
|
||||||
required: false,
|
required: false,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
|
|
||||||
const { clientId } = useParams();
|
const { clientId } = useParams();
|
||||||
const { appAuthClient, loading: loadingAuthClient } =
|
const { appAuthClient, loading: loadingAuthClient } =
|
||||||
useAppAuthClient(clientId);
|
useAppAuthClient(clientId);
|
||||||
@@ -34,8 +21,7 @@ export default function AdminApplicationUpdateAuthClient(
|
|||||||
refetchQueries: ['GetAppAuthClients'],
|
refetchQueries: ['GetAppAuthClients'],
|
||||||
context: { autoSnackbar: false },
|
context: { autoSnackbar: false },
|
||||||
});
|
});
|
||||||
|
const submitHandler = async (values) => {
|
||||||
const submitHandler: SubmitHandler<FieldValues> = async (values) => {
|
|
||||||
if (!appAuthClient) {
|
if (!appAuthClient) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -52,14 +38,11 @@ export default function AdminApplicationUpdateAuthClient(
|
|||||||
});
|
});
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAuthFieldsDefaultValues = useCallback(() => {
|
const getAuthFieldsDefaultValues = useCallback(() => {
|
||||||
if (!authFields) {
|
if (!authFields) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
const defaultValues: {
|
const defaultValues = {};
|
||||||
[key: string]: any;
|
|
||||||
} = {};
|
|
||||||
authFields.forEach((field) => {
|
authFields.forEach((field) => {
|
||||||
if (field.value || field.type !== 'string') {
|
if (field.value || field.type !== 'string') {
|
||||||
defaultValues[field.key] = field.value;
|
defaultValues[field.key] = field.value;
|
||||||
@@ -69,16 +52,14 @@ export default function AdminApplicationUpdateAuthClient(
|
|||||||
});
|
});
|
||||||
return defaultValues;
|
return defaultValues;
|
||||||
}, [auth?.fields]);
|
}, [auth?.fields]);
|
||||||
|
|
||||||
const defaultValues = useMemo(
|
const defaultValues = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
name: appAuthClient?.name || '',
|
name: appAuthClient?.name || '',
|
||||||
active: appAuthClient?.active || false,
|
active: appAuthClient?.active || false,
|
||||||
...getAuthFieldsDefaultValues(),
|
...getAuthFieldsDefaultValues(),
|
||||||
}),
|
}),
|
||||||
[appAuthClient, getAuthFieldsDefaultValues]
|
[appAuthClient, getAuthFieldsDefaultValues],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminApplicationAuthClientDialog
|
<AdminApplicationAuthClientDialog
|
||||||
onClose={onClose}
|
onClose={onClose}
|
@@ -4,42 +4,22 @@ import GroupsIcon from '@mui/icons-material/Groups';
|
|||||||
import LockIcon from '@mui/icons-material/LockPerson';
|
import LockIcon from '@mui/icons-material/LockPerson';
|
||||||
import BrushIcon from '@mui/icons-material/Brush';
|
import BrushIcon from '@mui/icons-material/Brush';
|
||||||
import AppsIcon from '@mui/icons-material/Apps';
|
import AppsIcon from '@mui/icons-material/Apps';
|
||||||
|
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import Toolbar from '@mui/material/Toolbar';
|
import Toolbar from '@mui/material/Toolbar';
|
||||||
import { useTheme } from '@mui/material/styles';
|
import { useTheme } from '@mui/material/styles';
|
||||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
||||||
import { SvgIconComponent } from '@mui/icons-material';
|
|
||||||
import AppBar from 'components/AppBar';
|
import AppBar from 'components/AppBar';
|
||||||
import Drawer from 'components/Drawer';
|
import Drawer from 'components/Drawer';
|
||||||
import * as URLS from 'config/urls';
|
import * as URLS from 'config/urls';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
import useCurrentUserAbility from 'hooks/useCurrentUserAbility';
|
import useCurrentUserAbility from 'hooks/useCurrentUserAbility';
|
||||||
|
|
||||||
type SettingsLayoutProps = {
|
|
||||||
children: React.ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
type DrawerLink = {
|
|
||||||
Icon: SvgIconComponent;
|
|
||||||
primary: string;
|
|
||||||
to: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
function createDrawerLinks({
|
function createDrawerLinks({
|
||||||
canReadRole,
|
canReadRole,
|
||||||
canReadUser,
|
canReadUser,
|
||||||
canUpdateConfig,
|
canUpdateConfig,
|
||||||
canManageSamlAuthProvider,
|
canManageSamlAuthProvider,
|
||||||
canUpdateApp,
|
canUpdateApp,
|
||||||
}: {
|
|
||||||
canReadRole: boolean;
|
|
||||||
canReadUser: boolean;
|
|
||||||
canUpdateConfig: boolean;
|
|
||||||
canManageSamlAuthProvider: boolean;
|
|
||||||
canUpdateApp: boolean;
|
|
||||||
}) {
|
}) {
|
||||||
const items = [
|
const items = [
|
||||||
canReadUser
|
canReadUser
|
||||||
@@ -82,20 +62,15 @@ function createDrawerLinks({
|
|||||||
dataTest: 'apps-drawer-link',
|
dataTest: 'apps-drawer-link',
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
].filter(Boolean) as DrawerLink[];
|
].filter(Boolean);
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
export default function SettingsLayout({ children }) {
|
||||||
export default function SettingsLayout({
|
|
||||||
children,
|
|
||||||
}: SettingsLayoutProps): React.ReactElement {
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
const currentUserAbility = useCurrentUserAbility();
|
const currentUserAbility = useCurrentUserAbility();
|
||||||
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('lg'));
|
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('lg'));
|
||||||
const [isDrawerOpen, setDrawerOpen] = React.useState(!matchSmallScreens);
|
const [isDrawerOpen, setDrawerOpen] = React.useState(!matchSmallScreens);
|
||||||
|
|
||||||
const openDrawer = () => setDrawerOpen(true);
|
const openDrawer = () => setDrawerOpen(true);
|
||||||
const closeDrawer = () => setDrawerOpen(false);
|
const closeDrawer = () => setDrawerOpen(false);
|
||||||
const drawerLinks = createDrawerLinks({
|
const drawerLinks = createDrawerLinks({
|
||||||
@@ -108,7 +83,7 @@ export default function SettingsLayout({
|
|||||||
currentUserAbility.can('create', 'SamlAuthProvider'),
|
currentUserAbility.can('create', 'SamlAuthProvider'),
|
||||||
canUpdateApp: currentUserAbility.can('update', 'App'),
|
canUpdateApp: currentUserAbility.can('update', 'App'),
|
||||||
});
|
});
|
||||||
|
const a = 123;
|
||||||
const drawerBottomLinks = [
|
const drawerBottomLinks = [
|
||||||
{
|
{
|
||||||
Icon: ArrowBackIosNewIcon,
|
Icon: ArrowBackIosNewIcon,
|
||||||
@@ -117,7 +92,6 @@ export default function SettingsLayout({
|
|||||||
dataTest: 'go-back-drawer-link',
|
dataTest: 'go-back-drawer-link',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AppBar
|
<AppBar
|
@@ -1,38 +1,28 @@
|
|||||||
import { ApolloProvider as BaseApolloProvider } from '@apollo/client';
|
import { ApolloProvider as BaseApolloProvider } from '@apollo/client';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
||||||
import { mutateAndGetClient } from 'graphql/client';
|
import { mutateAndGetClient } from 'graphql/client';
|
||||||
import useAuthentication from 'hooks/useAuthentication';
|
import useAuthentication from 'hooks/useAuthentication';
|
||||||
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
|
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
|
||||||
|
const ApolloProvider = (props) => {
|
||||||
type ApolloProviderProps = {
|
|
||||||
children: React.ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ApolloProvider = (props: ApolloProviderProps): React.ReactElement => {
|
|
||||||
const enqueueSnackbar = useEnqueueSnackbar();
|
const enqueueSnackbar = useEnqueueSnackbar();
|
||||||
const authentication = useAuthentication();
|
const authentication = useAuthentication();
|
||||||
|
|
||||||
const onError = React.useCallback(
|
const onError = React.useCallback(
|
||||||
(message) => {
|
(message) => {
|
||||||
enqueueSnackbar(message, {
|
enqueueSnackbar(message, {
|
||||||
variant: 'error',
|
variant: 'error',
|
||||||
SnackbarProps: {
|
SnackbarProps: {
|
||||||
'data-test': 'snackbar-error'
|
'data-test': 'snackbar-error',
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[enqueueSnackbar]
|
[enqueueSnackbar],
|
||||||
);
|
);
|
||||||
|
|
||||||
const client = React.useMemo(() => {
|
const client = React.useMemo(() => {
|
||||||
return mutateAndGetClient({
|
return mutateAndGetClient({
|
||||||
onError,
|
onError,
|
||||||
token: authentication.token,
|
token: authentication.token,
|
||||||
});
|
});
|
||||||
}, [onError, authentication]);
|
}, [onError, authentication]);
|
||||||
|
|
||||||
return <BaseApolloProvider client={client} {...props} />;
|
return <BaseApolloProvider client={client} {...props} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ApolloProvider;
|
export default ApolloProvider;
|
@@ -5,33 +5,22 @@ import ListItem from '@mui/material/ListItem';
|
|||||||
import ListItemButton from '@mui/material/ListItemButton';
|
import ListItemButton from '@mui/material/ListItemButton';
|
||||||
import ListItemText from '@mui/material/ListItemText';
|
import ListItemText from '@mui/material/ListItemText';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
||||||
import useAppAuthClients from 'hooks/useAppAuthClients.ee';
|
import useAppAuthClients from 'hooks/useAppAuthClients.ee';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
|
export default function AppAuthClientsDialog(props) {
|
||||||
type AppAuthClientsDialogProps = {
|
|
||||||
appKey: string;
|
|
||||||
onClientClick: (appAuthClientId: string) => void;
|
|
||||||
onClose: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function AppAuthClientsDialog(props: AppAuthClientsDialogProps) {
|
|
||||||
const { appKey, onClientClick, onClose } = props;
|
const { appKey, onClientClick, onClose } = props;
|
||||||
const { appAuthClients } = useAppAuthClients({ appKey, active: true });
|
const { appAuthClients } = useAppAuthClients({ appKey, active: true });
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
|
|
||||||
React.useEffect(
|
React.useEffect(
|
||||||
function autoAuthenticateSingleClient() {
|
function autoAuthenticateSingleClient() {
|
||||||
if (appAuthClients?.length === 1) {
|
if (appAuthClients?.length === 1) {
|
||||||
onClientClick(appAuthClients[0].id);
|
onClientClick(appAuthClients[0].id);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[appAuthClients]
|
[appAuthClients],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!appAuthClients?.length || appAuthClients?.length === 1)
|
if (!appAuthClients?.length || appAuthClients?.length === 1)
|
||||||
return <React.Fragment />;
|
return <React.Fragment />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog onClose={onClose} open={true}>
|
<Dialog onClose={onClose} open={true}>
|
||||||
<DialogTitle>{formatMessage('appAuthClientsDialog.title')}</DialogTitle>
|
<DialogTitle>{formatMessage('appAuthClientsDialog.title')}</DialogTitle>
|
@@ -2,49 +2,31 @@ import AccountCircleIcon from '@mui/icons-material/AccountCircle';
|
|||||||
import MenuIcon from '@mui/icons-material/Menu';
|
import MenuIcon from '@mui/icons-material/Menu';
|
||||||
import MenuOpenIcon from '@mui/icons-material/MenuOpen';
|
import MenuOpenIcon from '@mui/icons-material/MenuOpen';
|
||||||
import MuiAppBar from '@mui/material/AppBar';
|
import MuiAppBar from '@mui/material/AppBar';
|
||||||
import type { ContainerProps } from '@mui/material/Container';
|
|
||||||
import IconButton from '@mui/material/IconButton';
|
import IconButton from '@mui/material/IconButton';
|
||||||
import Toolbar from '@mui/material/Toolbar';
|
import Toolbar from '@mui/material/Toolbar';
|
||||||
import { useTheme } from '@mui/material/styles';
|
import { useTheme } from '@mui/material/styles';
|
||||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
||||||
import AccountDropdownMenu from 'components/AccountDropdownMenu';
|
import AccountDropdownMenu from 'components/AccountDropdownMenu';
|
||||||
import Container from 'components/Container';
|
import Container from 'components/Container';
|
||||||
import Logo from 'components/Logo/index';
|
import Logo from 'components/Logo/index';
|
||||||
import TrialStatusBadge from 'components/TrialStatusBadge/index.ee';
|
import TrialStatusBadge from 'components/TrialStatusBadge/index.ee';
|
||||||
import * as URLS from 'config/urls';
|
import * as URLS from 'config/urls';
|
||||||
|
|
||||||
import { Link } from './style';
|
import { Link } from './style';
|
||||||
|
|
||||||
type AppBarProps = {
|
|
||||||
drawerOpen: boolean;
|
|
||||||
onDrawerOpen: () => void;
|
|
||||||
onDrawerClose: () => void;
|
|
||||||
maxWidth?: ContainerProps['maxWidth'];
|
|
||||||
};
|
|
||||||
|
|
||||||
const accountMenuId = 'account-menu';
|
const accountMenuId = 'account-menu';
|
||||||
|
export default function AppBar(props) {
|
||||||
export default function AppBar(props: AppBarProps): React.ReactElement {
|
|
||||||
const { drawerOpen, onDrawerOpen, onDrawerClose, maxWidth = false } = props;
|
const { drawerOpen, onDrawerOpen, onDrawerClose, maxWidth = false } = props;
|
||||||
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('md'));
|
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
|
|
||||||
const [accountMenuAnchorElement, setAccountMenuAnchorElement] =
|
const [accountMenuAnchorElement, setAccountMenuAnchorElement] =
|
||||||
React.useState<null | HTMLElement>(null);
|
React.useState(null);
|
||||||
|
|
||||||
const isMenuOpen = Boolean(accountMenuAnchorElement);
|
const isMenuOpen = Boolean(accountMenuAnchorElement);
|
||||||
|
const handleAccountMenuOpen = (event) => {
|
||||||
const handleAccountMenuOpen = (event: React.MouseEvent<HTMLElement>) => {
|
|
||||||
setAccountMenuAnchorElement(event.currentTarget);
|
setAccountMenuAnchorElement(event.currentTarget);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAccountMenuClose = () => {
|
const handleAccountMenuClose = () => {
|
||||||
setAccountMenuAnchorElement(null);
|
setAccountMenuAnchorElement(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MuiAppBar data-test="app-bar">
|
<MuiAppBar data-test="app-bar">
|
||||||
<Container maxWidth={maxWidth} disableGutters>
|
<Container maxWidth={maxWidth} disableGutters>
|
@@ -1,6 +1,5 @@
|
|||||||
import { styled } from '@mui/material/styles';
|
import { styled } from '@mui/material/styles';
|
||||||
import { Link as RouterLink } from 'react-router-dom';
|
import { Link as RouterLink } from 'react-router-dom';
|
||||||
|
|
||||||
export const Link = styled(RouterLink)(() => ({
|
export const Link = styled(RouterLink)(() => ({
|
||||||
textDecoration: 'none',
|
textDecoration: 'none',
|
||||||
color: 'inherit',
|
color: 'inherit',
|
@@ -1,29 +1,10 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import Menu from '@mui/material/Menu';
|
import Menu from '@mui/material/Menu';
|
||||||
import type { PopoverProps } from '@mui/material/Popover';
|
|
||||||
import MenuItem from '@mui/material/MenuItem';
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
import type { IConnection } from 'types';
|
|
||||||
|
|
||||||
import * as URLS from 'config/urls';
|
import * as URLS from 'config/urls';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
|
export default function ContextMenu(props) {
|
||||||
type Action = {
|
|
||||||
type: 'test' | 'reconnect' | 'delete' | 'viewFlows';
|
|
||||||
};
|
|
||||||
|
|
||||||
type ContextMenuProps = {
|
|
||||||
appKey: string;
|
|
||||||
connection: IConnection;
|
|
||||||
onClose: () => void;
|
|
||||||
onMenuItemClick: (event: React.MouseEvent, action: Action) => void;
|
|
||||||
anchorEl: PopoverProps['anchorEl'];
|
|
||||||
disableReconnection: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function ContextMenu(
|
|
||||||
props: ContextMenuProps
|
|
||||||
): React.ReactElement {
|
|
||||||
const {
|
const {
|
||||||
appKey,
|
appKey,
|
||||||
connection,
|
connection,
|
||||||
@@ -33,18 +14,15 @@ export default function ContextMenu(
|
|||||||
disableReconnection,
|
disableReconnection,
|
||||||
} = props;
|
} = props;
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
|
|
||||||
const createActionHandler = React.useCallback(
|
const createActionHandler = React.useCallback(
|
||||||
(action: Action) => {
|
(action) => {
|
||||||
return function clickHandler(event: React.MouseEvent) {
|
return function clickHandler(event) {
|
||||||
onMenuItemClick(event, action);
|
onMenuItemClick(event, action);
|
||||||
|
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[onMenuItemClick, onClose]
|
[onMenuItemClick, onClose],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu
|
<Menu
|
||||||
open={true}
|
open={true}
|
||||||
@@ -70,7 +48,7 @@ export default function ContextMenu(
|
|||||||
to={URLS.APP_RECONNECT_CONNECTION(
|
to={URLS.APP_RECONNECT_CONNECTION(
|
||||||
appKey,
|
appKey,
|
||||||
connection.id,
|
connection.id,
|
||||||
connection.appAuthClientId
|
connection.appAuthClientId,
|
||||||
)}
|
)}
|
||||||
onClick={createActionHandler({ type: 'reconnect' })}
|
onClick={createActionHandler({ type: 'reconnect' })}
|
||||||
>
|
>
|
@@ -10,26 +10,18 @@ import Stack from '@mui/material/Stack';
|
|||||||
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
|
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
||||||
import type { IConnection } from 'types';
|
|
||||||
import ConnectionContextMenu from 'components/AppConnectionContextMenu';
|
import ConnectionContextMenu from 'components/AppConnectionContextMenu';
|
||||||
import { DELETE_CONNECTION } from 'graphql/mutations/delete-connection';
|
import { DELETE_CONNECTION } from 'graphql/mutations/delete-connection';
|
||||||
import { TEST_CONNECTION } from 'graphql/queries/test-connection';
|
import { TEST_CONNECTION } from 'graphql/queries/test-connection';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
import { CardContent, Typography } from './style';
|
import { CardContent, Typography } from './style';
|
||||||
|
const countTranslation = (value) => (
|
||||||
type AppConnectionRowProps = {
|
|
||||||
connection: IConnection;
|
|
||||||
};
|
|
||||||
|
|
||||||
const countTranslation = (value: React.ReactNode) => (
|
|
||||||
<>
|
<>
|
||||||
<Typography variant="body1">{value}</Typography>
|
<Typography variant="body1">{value}</Typography>
|
||||||
<br />
|
<br />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
function AppConnectionRow(props) {
|
||||||
function AppConnectionRow(props: AppConnectionRowProps): React.ReactElement {
|
|
||||||
const enqueueSnackbar = useEnqueueSnackbar();
|
const enqueueSnackbar = useEnqueueSnackbar();
|
||||||
const [verificationVisible, setVerificationVisible] = React.useState(false);
|
const [verificationVisible, setVerificationVisible] = React.useState(false);
|
||||||
const [testConnection, { called: testCalled, loading: testLoading }] =
|
const [testConnection, { called: testCalled, loading: testLoading }] =
|
||||||
@@ -43,7 +35,6 @@ function AppConnectionRow(props: AppConnectionRowProps): React.ReactElement {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
const [deleteConnection] = useMutation(DELETE_CONNECTION);
|
const [deleteConnection] = useMutation(DELETE_CONNECTION);
|
||||||
|
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
const {
|
const {
|
||||||
id,
|
id,
|
||||||
@@ -54,17 +45,14 @@ function AppConnectionRow(props: AppConnectionRowProps): React.ReactElement {
|
|||||||
flowCount,
|
flowCount,
|
||||||
reconnectable,
|
reconnectable,
|
||||||
} = props.connection;
|
} = props.connection;
|
||||||
|
const contextButtonRef = React.useRef(null);
|
||||||
const contextButtonRef = React.useRef<SVGSVGElement | null>(null);
|
const [anchorEl, setAnchorEl] = React.useState(null);
|
||||||
const [anchorEl, setAnchorEl] = React.useState<SVGSVGElement | null>(null);
|
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
setAnchorEl(null);
|
setAnchorEl(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onContextMenuClick = () => setAnchorEl(contextButtonRef.current);
|
const onContextMenuClick = () => setAnchorEl(contextButtonRef.current);
|
||||||
const onContextMenuAction = React.useCallback(
|
const onContextMenuAction = React.useCallback(
|
||||||
async (event, action: { [key: string]: string }) => {
|
async (event, action) => {
|
||||||
if (action.type === 'delete') {
|
if (action.type === 'delete') {
|
||||||
await deleteConnection({
|
await deleteConnection({
|
||||||
variables: { input: { id } },
|
variables: { input: { id } },
|
||||||
@@ -73,13 +61,11 @@ function AppConnectionRow(props: AppConnectionRowProps): React.ReactElement {
|
|||||||
__typename: 'Connection',
|
__typename: 'Connection',
|
||||||
id,
|
id,
|
||||||
});
|
});
|
||||||
|
|
||||||
cache.evict({
|
cache.evict({
|
||||||
id: connectionCacheId,
|
id: connectionCacheId,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
enqueueSnackbar(formatMessage('connection.deletedMessage'), {
|
enqueueSnackbar(formatMessage('connection.deletedMessage'), {
|
||||||
variant: 'success',
|
variant: 'success',
|
||||||
SnackbarProps: {
|
SnackbarProps: {
|
||||||
@@ -91,13 +77,11 @@ function AppConnectionRow(props: AppConnectionRowProps): React.ReactElement {
|
|||||||
testConnection({ variables: { id } });
|
testConnection({ variables: { id } });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[deleteConnection, id, testConnection, formatMessage, enqueueSnackbar]
|
[deleteConnection, id, testConnection, formatMessage, enqueueSnackbar],
|
||||||
);
|
);
|
||||||
|
|
||||||
const relativeCreatedAt = DateTime.fromMillis(
|
const relativeCreatedAt = DateTime.fromMillis(
|
||||||
parseInt(createdAt, 10)
|
parseInt(createdAt, 10),
|
||||||
).toRelative();
|
).toRelative();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Card sx={{ my: 2 }} data-test="app-connection-row">
|
<Card sx={{ my: 2 }} data-test="app-connection-row">
|
||||||
@@ -125,14 +109,17 @@ function AppConnectionRow(props: AppConnectionRowProps): React.ReactElement {
|
|||||||
</Typography>
|
</Typography>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{verificationVisible && testCalled && !testLoading && verified && (
|
{verificationVisible &&
|
||||||
<>
|
testCalled &&
|
||||||
<CheckCircleIcon fontSize="small" color="success" />
|
!testLoading &&
|
||||||
<Typography variant="caption">
|
verified && (
|
||||||
{formatMessage('connection.testSuccessful')}
|
<>
|
||||||
</Typography>
|
<CheckCircleIcon fontSize="small" color="success" />
|
||||||
</>
|
<Typography variant="caption">
|
||||||
)}
|
{formatMessage('connection.testSuccessful')}
|
||||||
|
</Typography>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{verificationVisible &&
|
{verificationVisible &&
|
||||||
testCalled &&
|
testCalled &&
|
||||||
!testLoading &&
|
!testLoading &&
|
||||||
@@ -179,5 +166,4 @@ function AppConnectionRow(props: AppConnectionRowProps): React.ReactElement {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AppConnectionRow;
|
export default AppConnectionRow;
|
@@ -1,7 +1,6 @@
|
|||||||
import { styled } from '@mui/material/styles';
|
import { styled } from '@mui/material/styles';
|
||||||
import MuiCardContent from '@mui/material/CardContent';
|
import MuiCardContent from '@mui/material/CardContent';
|
||||||
import MuiTypography from '@mui/material/Typography';
|
import MuiTypography from '@mui/material/Typography';
|
||||||
|
|
||||||
export const CardContent = styled(MuiCardContent)(({ theme }) => ({
|
export const CardContent = styled(MuiCardContent)(({ theme }) => ({
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
gridTemplateRows: 'auto',
|
gridTemplateRows: 'auto',
|
||||||
@@ -9,7 +8,6 @@ export const CardContent = styled(MuiCardContent)(({ theme }) => ({
|
|||||||
gridColumnGap: theme.spacing(2),
|
gridColumnGap: theme.spacing(2),
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const Typography = styled(MuiTypography)(() => ({
|
export const Typography = styled(MuiTypography)(() => ({
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
display: 'inline-block',
|
display: 'inline-block',
|
@@ -1,29 +1,18 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useQuery } from '@apollo/client';
|
import { useQuery } from '@apollo/client';
|
||||||
|
|
||||||
import type { IConnection } from 'types';
|
|
||||||
import { GET_APP_CONNECTIONS } from 'graphql/queries/get-app-connections';
|
import { GET_APP_CONNECTIONS } from 'graphql/queries/get-app-connections';
|
||||||
import AppConnectionRow from 'components/AppConnectionRow';
|
import AppConnectionRow from 'components/AppConnectionRow';
|
||||||
import NoResultFound from 'components/NoResultFound';
|
import NoResultFound from 'components/NoResultFound';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
import * as URLS from 'config/urls';
|
import * as URLS from 'config/urls';
|
||||||
|
export default function AppConnections(props) {
|
||||||
type AppConnectionsProps = {
|
|
||||||
appKey: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function AppConnections(
|
|
||||||
props: AppConnectionsProps
|
|
||||||
): React.ReactElement {
|
|
||||||
const { appKey } = props;
|
const { appKey } = props;
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
const { data } = useQuery(GET_APP_CONNECTIONS, {
|
const { data } = useQuery(GET_APP_CONNECTIONS, {
|
||||||
variables: { key: appKey },
|
variables: { key: appKey },
|
||||||
});
|
});
|
||||||
const appConnections: IConnection[] = data?.getApp?.connections || [];
|
const appConnections = data?.getApp?.connections || [];
|
||||||
|
|
||||||
const hasConnections = appConnections?.length;
|
const hasConnections = appConnections?.length;
|
||||||
|
|
||||||
if (!hasConnections) {
|
if (!hasConnections) {
|
||||||
return (
|
return (
|
||||||
<NoResultFound
|
<NoResultFound
|
||||||
@@ -33,10 +22,9 @@ export default function AppConnections(
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{appConnections.map((appConnection: IConnection) => (
|
{appConnections.map((appConnection) => (
|
||||||
<AppConnectionRow key={appConnection.id} connection={appConnection} />
|
<AppConnectionRow key={appConnection.id} connection={appConnection} />
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
@@ -3,25 +3,16 @@ import { Link, useSearchParams } from 'react-router-dom';
|
|||||||
import { GET_FLOWS } from 'graphql/queries/get-flows';
|
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';
|
||||||
import type { IFlow } from 'types';
|
|
||||||
|
|
||||||
type AppFlowsProps = {
|
|
||||||
appKey: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const FLOW_PER_PAGE = 10;
|
const FLOW_PER_PAGE = 10;
|
||||||
|
const getLimitAndOffset = (page) => ({
|
||||||
const getLimitAndOffset = (page: number) => ({
|
|
||||||
limit: FLOW_PER_PAGE,
|
limit: FLOW_PER_PAGE,
|
||||||
offset: (page - 1) * FLOW_PER_PAGE,
|
offset: (page - 1) * FLOW_PER_PAGE,
|
||||||
});
|
});
|
||||||
|
export default function AppFlows(props) {
|
||||||
export default function AppFlows(props: AppFlowsProps): React.ReactElement {
|
|
||||||
const { appKey } = props;
|
const { appKey } = props;
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
@@ -36,10 +27,8 @@ export default function AppFlows(props: AppFlowsProps): React.ReactElement {
|
|||||||
});
|
});
|
||||||
const getFlows = data?.getFlows || {};
|
const getFlows = data?.getFlows || {};
|
||||||
const { pageInfo, edges } = getFlows;
|
const { pageInfo, edges } = getFlows;
|
||||||
|
const flows = edges?.map(({ node }) => node);
|
||||||
const flows: IFlow[] = edges?.map(({ node }: { node: IFlow }) => node);
|
|
||||||
const hasFlows = flows?.length;
|
const hasFlows = flows?.length;
|
||||||
|
|
||||||
if (!hasFlows) {
|
if (!hasFlows) {
|
||||||
return (
|
return (
|
||||||
<NoResultFound
|
<NoResultFound
|
||||||
@@ -49,10 +38,9 @@ export default function AppFlows(props: AppFlowsProps): React.ReactElement {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{flows?.map((appFlow: IFlow) => (
|
{flows?.map((appFlow) => (
|
||||||
<AppFlowRow key={appFlow.id} flow={appFlow} />
|
<AppFlowRow key={appFlow.id} flow={appFlow} />
|
||||||
))}
|
))}
|
||||||
|
|
@@ -1,25 +1,11 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import Avatar from '@mui/material/Avatar';
|
import Avatar from '@mui/material/Avatar';
|
||||||
import type { AvatarProps } from '@mui/material/Avatar';
|
const inlineImgStyle = {
|
||||||
|
|
||||||
type AppIconProps = {
|
|
||||||
name?: string;
|
|
||||||
url?: string;
|
|
||||||
color?: string;
|
|
||||||
variant?: AvatarProps['variant'];
|
|
||||||
};
|
|
||||||
|
|
||||||
const inlineImgStyle: React.CSSProperties = {
|
|
||||||
objectFit: 'contain',
|
objectFit: 'contain',
|
||||||
};
|
};
|
||||||
|
export default function AppIcon(props) {
|
||||||
export default function AppIcon(
|
|
||||||
props: AppIconProps & AvatarProps
|
|
||||||
): React.ReactElement {
|
|
||||||
const { name, url, color, sx = {}, variant = 'square', ...restProps } = props;
|
const { name, url, color, sx = {}, variant = 'square', ...restProps } = props;
|
||||||
|
|
||||||
const initialLetter = name?.[0];
|
const initialLetter = name?.[0];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Avatar
|
<Avatar
|
||||||
component="span"
|
component="span"
|
@@ -4,30 +4,19 @@ import Card from '@mui/material/Card';
|
|||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import CardActionArea from '@mui/material/CardActionArea';
|
import CardActionArea from '@mui/material/CardActionArea';
|
||||||
import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos';
|
import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos';
|
||||||
|
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
import AppIcon from 'components/AppIcon';
|
import AppIcon from 'components/AppIcon';
|
||||||
import type { IApp } from 'types';
|
|
||||||
|
|
||||||
import { CardContent, Typography } from './style';
|
import { CardContent, Typography } from './style';
|
||||||
|
const countTranslation = (value) => (
|
||||||
type AppRowProps = {
|
|
||||||
application: IApp;
|
|
||||||
url: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const countTranslation = (value: React.ReactNode) => (
|
|
||||||
<>
|
<>
|
||||||
<Typography variant="body1">{value}</Typography>
|
<Typography variant="body1">{value}</Typography>
|
||||||
<br />
|
<br />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
function AppRow(props) {
|
||||||
function AppRow(props: AppRowProps): React.ReactElement {
|
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
const { name, primaryColor, iconUrl, connectionCount, flowCount } =
|
const { name, primaryColor, iconUrl, connectionCount, flowCount } =
|
||||||
props.application;
|
props.application;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link to={props.url} data-test="app-row">
|
<Link to={props.url} data-test="app-row">
|
||||||
<Card sx={{ mb: 1 }}>
|
<Card sx={{ mb: 1 }}>
|
||||||
@@ -76,5 +65,4 @@ function AppRow(props: AppRowProps): React.ReactElement {
|
|||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AppRow;
|
export default AppRow;
|
@@ -1,7 +1,6 @@
|
|||||||
import { styled } from '@mui/material/styles';
|
import { styled } from '@mui/material/styles';
|
||||||
import MuiCardContent from '@mui/material/CardContent';
|
import MuiCardContent from '@mui/material/CardContent';
|
||||||
import MuiTypography from '@mui/material/Typography';
|
import MuiTypography from '@mui/material/Typography';
|
||||||
|
|
||||||
export const CardContent = styled(MuiCardContent)(({ theme }) => ({
|
export const CardContent = styled(MuiCardContent)(({ theme }) => ({
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
gridTemplateRows: 'auto',
|
gridTemplateRows: 'auto',
|
||||||
@@ -9,7 +8,6 @@ export const CardContent = styled(MuiCardContent)(({ theme }) => ({
|
|||||||
gridColumnGap: theme.spacing(2),
|
gridColumnGap: theme.spacing(2),
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const Typography = styled(MuiTypography)(() => ({
|
export const Typography = styled(MuiTypography)(() => ({
|
||||||
'&.MuiTypography-h6': {
|
'&.MuiTypography-h6': {
|
||||||
textTransform: 'capitalize',
|
textTransform: 'capitalize',
|
||||||
@@ -17,7 +15,6 @@ export const Typography = styled(MuiTypography)(() => ({
|
|||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
display: 'inline-block',
|
display: 'inline-block',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const DesktopOnlyBreakline = styled('br')(({ theme }) => ({
|
export const DesktopOnlyBreakline = styled('br')(({ theme }) => ({
|
||||||
[theme.breakpoints.down('sm')]: {
|
[theme.breakpoints.down('sm')]: {
|
||||||
display: 'none',
|
display: 'none',
|
7
packages/web/src/components/Can/index.jsx
Normal file
7
packages/web/src/components/Can/index.jsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { Can as OriginalCan } from '@casl/react';
|
||||||
|
import * as React from 'react';
|
||||||
|
import useCurrentUserAbility from 'hooks/useCurrentUserAbility';
|
||||||
|
export default function Can(props) {
|
||||||
|
const currentUserAbility = useCurrentUserAbility();
|
||||||
|
return <OriginalCan ability={currentUserAbility} {...props} />;
|
||||||
|
}
|
@@ -1,22 +0,0 @@
|
|||||||
import { Can as OriginalCan } from '@casl/react';
|
|
||||||
import * as React from 'react';
|
|
||||||
|
|
||||||
import useCurrentUserAbility from 'hooks/useCurrentUserAbility';
|
|
||||||
|
|
||||||
type CanProps = {
|
|
||||||
I: string;
|
|
||||||
a: string;
|
|
||||||
passThrough?: boolean;
|
|
||||||
children: React.ReactNode | ((isAllowed: boolean) => React.ReactNode);
|
|
||||||
} | {
|
|
||||||
I: string;
|
|
||||||
an: string;
|
|
||||||
passThrough?: boolean;
|
|
||||||
children: React.ReactNode | ((isAllowed: boolean) => React.ReactNode);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function Can(props: CanProps) {
|
|
||||||
const currentUserAbility = useCurrentUserAbility();
|
|
||||||
|
|
||||||
return (<OriginalCan ability={currentUserAbility} {...props} />);
|
|
||||||
};
|
|
@@ -2,18 +2,13 @@ import * as React from 'react';
|
|||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import Alert from '@mui/material/Alert';
|
import Alert from '@mui/material/Alert';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
|
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
|
|
||||||
export default function CheckoutCompletedAlert() {
|
export default function CheckoutCompletedAlert() {
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const state = location.state as { checkoutCompleted: boolean };
|
const state = location.state;
|
||||||
|
|
||||||
const checkoutCompleted = state?.checkoutCompleted;
|
const checkoutCompleted = state?.checkoutCompleted;
|
||||||
|
|
||||||
if (!checkoutCompleted) return <React.Fragment />;
|
if (!checkoutCompleted) return <React.Fragment />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Alert
|
<Alert
|
||||||
severity="success"
|
severity="success"
|
@@ -7,49 +7,22 @@ import ListItem from '@mui/material/ListItem';
|
|||||||
import TextField from '@mui/material/TextField';
|
import TextField from '@mui/material/TextField';
|
||||||
import Autocomplete from '@mui/material/Autocomplete';
|
import Autocomplete from '@mui/material/Autocomplete';
|
||||||
import Chip from '@mui/material/Chip';
|
import Chip from '@mui/material/Chip';
|
||||||
import type { IApp, IStep, ISubstep, ITrigger, IAction } from 'types';
|
|
||||||
|
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
import useApps from 'hooks/useApps';
|
import useApps from 'hooks/useApps';
|
||||||
import { EditorContext } from 'contexts/Editor';
|
import { EditorContext } from 'contexts/Editor';
|
||||||
import FlowSubstepTitle from 'components/FlowSubstepTitle';
|
import FlowSubstepTitle from 'components/FlowSubstepTitle';
|
||||||
|
const optionGenerator = (app) => ({
|
||||||
type ChooseAppAndEventSubstepProps = {
|
label: app.name,
|
||||||
substep: ISubstep;
|
value: app.key,
|
||||||
expanded?: boolean;
|
|
||||||
onExpand: () => void;
|
|
||||||
onCollapse: () => void;
|
|
||||||
onChange: ({ step }: { step: IStep }) => void;
|
|
||||||
onSubmit: () => void;
|
|
||||||
step: IStep;
|
|
||||||
};
|
|
||||||
|
|
||||||
const optionGenerator = (app: {
|
|
||||||
name: string;
|
|
||||||
key: string;
|
|
||||||
}): { label: string; value: string } => ({
|
|
||||||
label: app.name as string,
|
|
||||||
value: app.key as string,
|
|
||||||
});
|
});
|
||||||
|
const eventOptionGenerator = (app) => ({
|
||||||
const eventOptionGenerator = (app: {
|
label: app.name,
|
||||||
name: string;
|
value: app.key,
|
||||||
key: string;
|
type: app?.type,
|
||||||
type?: string;
|
|
||||||
}): { label: string; value: string; type: string } => ({
|
|
||||||
label: app.name as string,
|
|
||||||
value: app.key as string,
|
|
||||||
type: app?.type as string,
|
|
||||||
});
|
});
|
||||||
|
const getOption = (options, selectedOptionValue) =>
|
||||||
const getOption = <T extends { value: string }>(
|
options.find((option) => option.value === selectedOptionValue);
|
||||||
options: T[],
|
function ChooseAppAndEventSubstep(props) {
|
||||||
selectedOptionValue?: string
|
|
||||||
) => options.find((option) => option.value === selectedOptionValue);
|
|
||||||
|
|
||||||
function ChooseAppAndEventSubstep(
|
|
||||||
props: ChooseAppAndEventSubstepProps
|
|
||||||
): React.ReactElement {
|
|
||||||
const {
|
const {
|
||||||
substep,
|
substep,
|
||||||
expanded = false,
|
expanded = false,
|
||||||
@@ -59,49 +32,38 @@ function ChooseAppAndEventSubstep(
|
|||||||
onSubmit,
|
onSubmit,
|
||||||
onChange,
|
onChange,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
const editorContext = React.useContext(EditorContext);
|
const editorContext = React.useContext(EditorContext);
|
||||||
|
|
||||||
const isTrigger = step.type === 'trigger';
|
const isTrigger = step.type === 'trigger';
|
||||||
const isAction = step.type === 'action';
|
const isAction = step.type === 'action';
|
||||||
|
|
||||||
const { apps } = useApps({
|
const { apps } = useApps({
|
||||||
onlyWithTriggers: isTrigger,
|
onlyWithTriggers: isTrigger,
|
||||||
onlyWithActions: isAction,
|
onlyWithActions: isAction,
|
||||||
});
|
});
|
||||||
const app = apps?.find((currentApp: IApp) => currentApp.key === step.appKey);
|
const app = apps?.find((currentApp) => currentApp.key === step.appKey);
|
||||||
|
|
||||||
const appOptions = React.useMemo(
|
const appOptions = React.useMemo(
|
||||||
() => apps?.map((app) => optionGenerator(app)) || [],
|
() => apps?.map((app) => optionGenerator(app)) || [],
|
||||||
[apps]
|
[apps],
|
||||||
);
|
);
|
||||||
const actionsOrTriggers: Array<ITrigger | IAction> =
|
const actionsOrTriggers = (isTrigger ? app?.triggers : app?.actions) || [];
|
||||||
(isTrigger ? app?.triggers : app?.actions) || [];
|
|
||||||
const actionOrTriggerOptions = React.useMemo(
|
const actionOrTriggerOptions = React.useMemo(
|
||||||
() => actionsOrTriggers.map((trigger) => eventOptionGenerator(trigger)),
|
() => actionsOrTriggers.map((trigger) => eventOptionGenerator(trigger)),
|
||||||
[app?.key]
|
[app?.key],
|
||||||
);
|
);
|
||||||
const selectedActionOrTrigger = actionsOrTriggers.find(
|
const selectedActionOrTrigger = actionsOrTriggers.find(
|
||||||
(actionOrTrigger: IAction | ITrigger) => actionOrTrigger.key === step?.key
|
(actionOrTrigger) => actionOrTrigger.key === step?.key,
|
||||||
);
|
);
|
||||||
|
const isWebhook = isTrigger && selectedActionOrTrigger?.type === 'webhook';
|
||||||
const isWebhook =
|
|
||||||
isTrigger && (selectedActionOrTrigger as ITrigger)?.type === 'webhook';
|
|
||||||
|
|
||||||
const { name } = substep;
|
const { name } = substep;
|
||||||
|
const valid = !!step.key && !!step.appKey;
|
||||||
const valid: boolean = !!step.key && !!step.appKey;
|
|
||||||
|
|
||||||
// placeholders
|
// placeholders
|
||||||
const onEventChange = React.useCallback(
|
const onEventChange = React.useCallback(
|
||||||
(event: React.SyntheticEvent, selectedOption: unknown) => {
|
(event, selectedOption) => {
|
||||||
if (typeof selectedOption === 'object') {
|
if (typeof selectedOption === 'object') {
|
||||||
// TODO: try to simplify type casting below.
|
// TODO: try to simplify type casting below.
|
||||||
const typedSelectedOption = selectedOption as { value: string };
|
const typedSelectedOption = selectedOption;
|
||||||
const option: { value: string } = typedSelectedOption;
|
const option = typedSelectedOption;
|
||||||
const eventKey = option?.value as string;
|
const eventKey = option?.value;
|
||||||
|
|
||||||
if (step.key !== eventKey) {
|
if (step.key !== eventKey) {
|
||||||
onChange({
|
onChange({
|
||||||
step: {
|
step: {
|
||||||
@@ -112,17 +74,15 @@ function ChooseAppAndEventSubstep(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[step, onChange]
|
[step, onChange],
|
||||||
);
|
);
|
||||||
|
|
||||||
const onAppChange = React.useCallback(
|
const onAppChange = React.useCallback(
|
||||||
(event: React.SyntheticEvent, selectedOption: unknown) => {
|
(event, selectedOption) => {
|
||||||
if (typeof selectedOption === 'object') {
|
if (typeof selectedOption === 'object') {
|
||||||
// TODO: try to simplify type casting below.
|
// TODO: try to simplify type casting below.
|
||||||
const typedSelectedOption = selectedOption as { value: string };
|
const typedSelectedOption = selectedOption;
|
||||||
const option: { value: string } = typedSelectedOption;
|
const option = typedSelectedOption;
|
||||||
const appKey = option?.value as string;
|
const appKey = option?.value;
|
||||||
|
|
||||||
if (step.appKey !== appKey) {
|
if (step.appKey !== appKey) {
|
||||||
onChange({
|
onChange({
|
||||||
step: {
|
step: {
|
||||||
@@ -135,11 +95,9 @@ function ChooseAppAndEventSubstep(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[step, onChange]
|
[step, onChange],
|
||||||
);
|
);
|
||||||
|
|
||||||
const onToggle = expanded ? onCollapse : onExpand;
|
const onToggle = expanded ? onCollapse : onExpand;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<FlowSubstepTitle
|
<FlowSubstepTitle
|
||||||
@@ -200,7 +158,7 @@ function ChooseAppAndEventSubstep(
|
|||||||
{isWebhook && (
|
{isWebhook && (
|
||||||
<Chip
|
<Chip
|
||||||
label={formatMessage(
|
label={formatMessage(
|
||||||
'flowEditor.instantTriggerType'
|
'flowEditor.instantTriggerType',
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -237,11 +195,11 @@ function ChooseAppAndEventSubstep(
|
|||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isTrigger && (selectedActionOrTrigger as ITrigger)?.pollInterval && (
|
{isTrigger && selectedActionOrTrigger?.pollInterval && (
|
||||||
<TextField
|
<TextField
|
||||||
label={formatMessage('flowEditor.pollIntervalLabel')}
|
label={formatMessage('flowEditor.pollIntervalLabel')}
|
||||||
value={formatMessage('flowEditor.pollIntervalValue', {
|
value={formatMessage('flowEditor.pollIntervalValue', {
|
||||||
minutes: (selectedActionOrTrigger as ITrigger)?.pollInterval,
|
minutes: selectedActionOrTrigger?.pollInterval,
|
||||||
})}
|
})}
|
||||||
sx={{ mt: 2 }}
|
sx={{ mt: 2 }}
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -264,5 +222,4 @@ function ChooseAppAndEventSubstep(
|
|||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ChooseAppAndEventSubstep;
|
export default ChooseAppAndEventSubstep;
|
@@ -5,8 +5,6 @@ import Collapse from '@mui/material/Collapse';
|
|||||||
import ListItem from '@mui/material/ListItem';
|
import ListItem from '@mui/material/ListItem';
|
||||||
import TextField from '@mui/material/TextField';
|
import TextField from '@mui/material/TextField';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
||||||
import type { IApp, IConnection, IStep, ISubstep } from 'types';
|
|
||||||
import AddAppConnection from 'components/AddAppConnection';
|
import AddAppConnection from 'components/AddAppConnection';
|
||||||
import AppAuthClientsDialog from 'components/AppAuthClientsDialog/index.ee';
|
import AppAuthClientsDialog from 'components/AppAuthClientsDialog/index.ee';
|
||||||
import FlowSubstepTitle from 'components/FlowSubstepTitle';
|
import FlowSubstepTitle from 'components/FlowSubstepTitle';
|
||||||
@@ -16,34 +14,15 @@ import { GET_APP_CONNECTIONS } from 'graphql/queries/get-app-connections';
|
|||||||
import { TEST_CONNECTION } from 'graphql/queries/test-connection';
|
import { TEST_CONNECTION } from 'graphql/queries/test-connection';
|
||||||
import useAuthenticateApp from 'hooks/useAuthenticateApp.ee';
|
import useAuthenticateApp from 'hooks/useAuthenticateApp.ee';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
|
|
||||||
type ChooseConnectionSubstepProps = {
|
|
||||||
application: IApp;
|
|
||||||
substep: ISubstep;
|
|
||||||
expanded?: boolean;
|
|
||||||
onExpand: () => void;
|
|
||||||
onCollapse: () => void;
|
|
||||||
onChange: ({ step }: { step: IStep }) => void;
|
|
||||||
onSubmit: () => void;
|
|
||||||
step: IStep;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ADD_CONNECTION_VALUE = 'ADD_CONNECTION';
|
const ADD_CONNECTION_VALUE = 'ADD_CONNECTION';
|
||||||
const ADD_SHARED_CONNECTION_VALUE = 'ADD_SHARED_CONNECTION';
|
const ADD_SHARED_CONNECTION_VALUE = 'ADD_SHARED_CONNECTION';
|
||||||
|
const optionGenerator = (connection) => ({
|
||||||
const optionGenerator = (
|
label: connection?.formattedData?.screenName ?? 'Unnamed',
|
||||||
connection: IConnection
|
value: connection?.id,
|
||||||
): { label: string; value: string } => ({
|
|
||||||
label: (connection?.formattedData?.screenName as string) ?? 'Unnamed',
|
|
||||||
value: connection?.id as string,
|
|
||||||
});
|
});
|
||||||
|
const getOption = (options, connectionId) =>
|
||||||
const getOption = (options: Record<string, unknown>[], connectionId?: string) =>
|
|
||||||
options.find((connection) => connection.value === connectionId) || null;
|
options.find((connection) => connection.value === connectionId) || null;
|
||||||
|
function ChooseConnectionSubstep(props) {
|
||||||
function ChooseConnectionSubstep(
|
|
||||||
props: ChooseConnectionSubstepProps
|
|
||||||
): React.ReactElement {
|
|
||||||
const {
|
const {
|
||||||
substep,
|
substep,
|
||||||
expanded = false,
|
expanded = false,
|
||||||
@@ -78,7 +57,6 @@ function ChooseConnectionSubstep(
|
|||||||
id: connection?.id,
|
id: connection?.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (connection?.id) {
|
if (connection?.id) {
|
||||||
testConnection({
|
testConnection({
|
||||||
@@ -89,42 +67,34 @@ function ChooseConnectionSubstep(
|
|||||||
}
|
}
|
||||||
// intentionally no dependencies for initial test
|
// intentionally no dependencies for initial test
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const connectionOptions = React.useMemo(() => {
|
const connectionOptions = React.useMemo(() => {
|
||||||
const appWithConnections = data?.getApp as IApp;
|
const appWithConnections = data?.getApp;
|
||||||
const options =
|
const options =
|
||||||
appWithConnections?.connections?.map((connection) =>
|
appWithConnections?.connections?.map((connection) =>
|
||||||
optionGenerator(connection)
|
optionGenerator(connection),
|
||||||
) || [];
|
) || [];
|
||||||
|
|
||||||
if (!appConfig || appConfig.canCustomConnect) {
|
if (!appConfig || appConfig.canCustomConnect) {
|
||||||
options.push({
|
options.push({
|
||||||
label: formatMessage('chooseConnectionSubstep.addNewConnection'),
|
label: formatMessage('chooseConnectionSubstep.addNewConnection'),
|
||||||
value: ADD_CONNECTION_VALUE,
|
value: ADD_CONNECTION_VALUE,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (appConfig?.canConnect) {
|
if (appConfig?.canConnect) {
|
||||||
options.push({
|
options.push({
|
||||||
label: formatMessage('chooseConnectionSubstep.addNewSharedConnection'),
|
label: formatMessage('chooseConnectionSubstep.addNewSharedConnection'),
|
||||||
value: ADD_SHARED_CONNECTION_VALUE,
|
value: ADD_SHARED_CONNECTION_VALUE,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return options;
|
return options;
|
||||||
}, [data, formatMessage, appConfig]);
|
}, [data, formatMessage, appConfig]);
|
||||||
|
const handleClientClick = async (appAuthClientId) => {
|
||||||
const handleClientClick = async (appAuthClientId: string) => {
|
|
||||||
try {
|
try {
|
||||||
const response = await authenticate?.({
|
const response = await authenticate?.({
|
||||||
appAuthClientId,
|
appAuthClientId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const connectionId = response?.createConnection.id;
|
const connectionId = response?.createConnection.id;
|
||||||
|
|
||||||
if (connectionId) {
|
if (connectionId) {
|
||||||
await refetch();
|
await refetch();
|
||||||
|
|
||||||
onChange({
|
onChange({
|
||||||
step: {
|
step: {
|
||||||
...step,
|
...step,
|
||||||
@@ -140,18 +110,13 @@ function ChooseConnectionSubstep(
|
|||||||
setShowAddSharedConnectionDialog(false);
|
setShowAddSharedConnectionDialog(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const { name } = substep;
|
const { name } = substep;
|
||||||
|
|
||||||
const handleAddConnectionClose = React.useCallback(
|
const handleAddConnectionClose = React.useCallback(
|
||||||
async (response) => {
|
async (response) => {
|
||||||
setShowAddConnectionDialog(false);
|
setShowAddConnectionDialog(false);
|
||||||
|
|
||||||
const connectionId = response?.createConnection.id;
|
const connectionId = response?.createConnection.id;
|
||||||
|
|
||||||
if (connectionId) {
|
if (connectionId) {
|
||||||
await refetch();
|
await refetch();
|
||||||
|
|
||||||
onChange({
|
onChange({
|
||||||
step: {
|
step: {
|
||||||
...step,
|
...step,
|
||||||
@@ -162,27 +127,23 @@ function ChooseConnectionSubstep(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[onChange, refetch, step]
|
[onChange, refetch, step],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleChange = React.useCallback(
|
const handleChange = React.useCallback(
|
||||||
(event: React.SyntheticEvent, selectedOption: unknown) => {
|
(event, selectedOption) => {
|
||||||
if (typeof selectedOption === 'object') {
|
if (typeof selectedOption === 'object') {
|
||||||
// TODO: try to simplify type casting below.
|
// TODO: try to simplify type casting below.
|
||||||
const typedSelectedOption = selectedOption as { value: string };
|
const typedSelectedOption = selectedOption;
|
||||||
const option: { value: string } = typedSelectedOption;
|
const option = typedSelectedOption;
|
||||||
const connectionId = option?.value as string;
|
const connectionId = option?.value;
|
||||||
|
|
||||||
if (connectionId === ADD_CONNECTION_VALUE) {
|
if (connectionId === ADD_CONNECTION_VALUE) {
|
||||||
setShowAddConnectionDialog(true);
|
setShowAddConnectionDialog(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (connectionId === ADD_SHARED_CONNECTION_VALUE) {
|
if (connectionId === ADD_SHARED_CONNECTION_VALUE) {
|
||||||
setShowAddSharedConnectionDialog(true);
|
setShowAddSharedConnectionDialog(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (connectionId !== step.connection?.id) {
|
if (connectionId !== step.connection?.id) {
|
||||||
onChange({
|
onChange({
|
||||||
step: {
|
step: {
|
||||||
@@ -195,9 +156,8 @@ function ChooseConnectionSubstep(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[step, onChange]
|
[step, onChange],
|
||||||
);
|
);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (step.connection?.id) {
|
if (step.connection?.id) {
|
||||||
retestConnection({
|
retestConnection({
|
||||||
@@ -205,9 +165,7 @@ function ChooseConnectionSubstep(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [step.connection?.id, retestConnection]);
|
}, [step.connection?.id, retestConnection]);
|
||||||
|
|
||||||
const onToggle = expanded ? onCollapse : onExpand;
|
const onToggle = expanded ? onCollapse : onExpand;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<FlowSubstepTitle
|
<FlowSubstepTitle
|
||||||
@@ -235,7 +193,7 @@ function ChooseConnectionSubstep(
|
|||||||
<TextField
|
<TextField
|
||||||
{...params}
|
{...params}
|
||||||
label={formatMessage(
|
label={formatMessage(
|
||||||
'chooseConnectionSubstep.chooseConnection'
|
'chooseConnectionSubstep.chooseConnection',
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -279,5 +237,4 @@ function ChooseConnectionSubstep(
|
|||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ChooseConnectionSubstep;
|
export default ChooseConnectionSubstep;
|
@@ -1,19 +1,8 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ButtonProps } from '@mui/material/Button';
|
|
||||||
import { Button } from './style';
|
import { Button } from './style';
|
||||||
|
|
||||||
const BG_IMAGE_FALLBACK =
|
const BG_IMAGE_FALLBACK =
|
||||||
'linear-gradient(45deg, #ccc 25%, transparent 25%), linear-gradient(135deg, #ccc 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #ccc 75%), linear-gradient(135deg, transparent 75%, #ccc 75%) /*! @noflip */';
|
'linear-gradient(45deg, #ccc 25%, transparent 25%), linear-gradient(135deg, #ccc 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #ccc 75%), linear-gradient(135deg, transparent 75%, #ccc 75%) /*! @noflip */';
|
||||||
|
const ColorButton = (props) => {
|
||||||
export type ColorButtonProps = Omit<ButtonProps, 'children'> & {
|
|
||||||
bgColor: string;
|
|
||||||
isBgColorValid: boolean;
|
|
||||||
disablePopover: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ColorButtonElement = (props: ColorButtonProps) => JSX.Element;
|
|
||||||
|
|
||||||
const ColorButton = (props: ColorButtonProps) => {
|
|
||||||
const {
|
const {
|
||||||
bgColor,
|
bgColor,
|
||||||
className,
|
className,
|
||||||
@@ -21,7 +10,6 @@ const ColorButton = (props: ColorButtonProps) => {
|
|||||||
isBgColorValid,
|
isBgColorValid,
|
||||||
...restButtonProps
|
...restButtonProps
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
data-test="color-button"
|
data-test="color-button"
|
||||||
@@ -36,5 +24,4 @@ const ColorButton = (props: ColorButtonProps) => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ColorButton;
|
export default ColorButton;
|
@@ -1,6 +1,5 @@
|
|||||||
import MuiButton from '@mui/material/Button';
|
import MuiButton from '@mui/material/Button';
|
||||||
import { styled } from '@mui/material/styles';
|
import { styled } from '@mui/material/styles';
|
||||||
|
|
||||||
export const Button = styled(MuiButton)(() => ({
|
export const Button = styled(MuiButton)(() => ({
|
||||||
backgroundSize: '8px 8px',
|
backgroundSize: '8px 8px',
|
||||||
backgroundPosition: '0 0, 4px 0, 4px -4px, 0px 4px',
|
backgroundPosition: '0 0, 4px 0, 4px -4px, 0px 4px',
|
||||||
@@ -12,4 +11,4 @@ export const Button = styled(MuiButton)(() => ({
|
|||||||
aspectRatio: '1 / 1',
|
aspectRatio: '1 / 1',
|
||||||
height: '24px',
|
height: '24px',
|
||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
})) as typeof MuiButton;
|
}));
|
@@ -1,15 +1,8 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Controller, useFormContext } from 'react-hook-form';
|
import { Controller, useFormContext } from 'react-hook-form';
|
||||||
import { MuiColorInput, MuiColorInputProps } from 'mui-color-input';
|
import { MuiColorInput } from 'mui-color-input';
|
||||||
import ColorButton from './ColorButton';
|
import ColorButton from './ColorButton';
|
||||||
|
export default function ColorInput(props) {
|
||||||
type ColorInputProps = {
|
|
||||||
shouldUnregister?: boolean;
|
|
||||||
name: string;
|
|
||||||
'data-test'?: string;
|
|
||||||
} & Partial<MuiColorInputProps>;
|
|
||||||
|
|
||||||
export default function ColorInput(props: ColorInputProps): React.ReactElement {
|
|
||||||
const { control } = useFormContext();
|
const { control } = useFormContext();
|
||||||
const {
|
const {
|
||||||
required,
|
required,
|
||||||
@@ -18,7 +11,6 @@ export default function ColorInput(props: ColorInputProps): React.ReactElement {
|
|||||||
disabled = false,
|
disabled = false,
|
||||||
...textFieldProps
|
...textFieldProps
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Controller
|
<Controller
|
||||||
rules={{ required }}
|
rules={{ required }}
|
@@ -2,16 +2,11 @@ import * as React from 'react';
|
|||||||
import { useTheme } from '@mui/material/styles';
|
import { useTheme } from '@mui/material/styles';
|
||||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
import type { ButtonProps } from '@mui/material/Button';
|
|
||||||
|
|
||||||
import { IconButton } from './style';
|
import { IconButton } from './style';
|
||||||
|
export default function ConditionalIconButton(props) {
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
|
||||||
export default function ConditionalIconButton(props: any): React.ReactElement {
|
|
||||||
const { icon, ...buttonProps } = props;
|
const { icon, ...buttonProps } = props;
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('md'));
|
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
|
|
||||||
if (matchSmallScreens) {
|
if (matchSmallScreens) {
|
||||||
return (
|
return (
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -27,6 +22,5 @@ export default function ConditionalIconButton(props: any): React.ReactElement {
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
return <Button {...buttonProps} />;
|
||||||
return <Button {...(buttonProps as ButtonProps)} />;
|
|
||||||
}
|
}
|
@@ -1,6 +1,5 @@
|
|||||||
import { styled } from '@mui/material/styles';
|
import { styled } from '@mui/material/styles';
|
||||||
import MuiIconButton, { iconButtonClasses } from '@mui/material/IconButton';
|
import MuiIconButton, { iconButtonClasses } from '@mui/material/IconButton';
|
||||||
|
|
||||||
export const IconButton = styled(MuiIconButton)`
|
export const IconButton = styled(MuiIconButton)`
|
||||||
&.${iconButtonClasses.colorPrimary}:not(.${iconButtonClasses.disabled}) {
|
&.${iconButtonClasses.colorPrimary}:not(.${iconButtonClasses.disabled}) {
|
||||||
background: ${({ theme }) => theme.palette.primary.main};
|
background: ${({ theme }) => theme.palette.primary.main};
|
||||||
@@ -10,4 +9,4 @@ export const IconButton = styled(MuiIconButton)`
|
|||||||
background: ${({ theme }) => theme.palette.primary.dark};
|
background: ${({ theme }) => theme.palette.primary.dark};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
` as typeof MuiIconButton;
|
`;
|
@@ -5,19 +5,7 @@ import DialogActions from '@mui/material/DialogActions';
|
|||||||
import DialogContent from '@mui/material/DialogContent';
|
import DialogContent from '@mui/material/DialogContent';
|
||||||
import DialogContentText from '@mui/material/DialogContentText';
|
import DialogContentText from '@mui/material/DialogContentText';
|
||||||
import DialogTitle from '@mui/material/DialogTitle';
|
import DialogTitle from '@mui/material/DialogTitle';
|
||||||
|
export default function ConfirmationDialog(props) {
|
||||||
type ConfirmationDialogProps = {
|
|
||||||
onClose: () => void;
|
|
||||||
onConfirm: () => void;
|
|
||||||
title: React.ReactNode;
|
|
||||||
description: React.ReactNode;
|
|
||||||
cancelButtonChildren: React.ReactNode;
|
|
||||||
confirmButtionChildren: React.ReactNode;
|
|
||||||
open?: boolean;
|
|
||||||
'data-test'?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ConfirmationDialog(props: ConfirmationDialogProps) {
|
|
||||||
const {
|
const {
|
||||||
onClose,
|
onClose,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
@@ -30,31 +18,26 @@ export default function ConfirmationDialog(props: ConfirmationDialogProps) {
|
|||||||
const dataTest = props['data-test'];
|
const dataTest = props['data-test'];
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onClose={onClose} data-test={dataTest}>
|
<Dialog open={open} onClose={onClose} data-test={dataTest}>
|
||||||
{title && (
|
{title && <DialogTitle>{title}</DialogTitle>}
|
||||||
<DialogTitle>
|
|
||||||
{title}
|
|
||||||
</DialogTitle>
|
|
||||||
)}
|
|
||||||
{description && (
|
{description && (
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogContentText>
|
<DialogContentText>{description}</DialogContentText>
|
||||||
{description}
|
|
||||||
</DialogContentText>
|
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
{(cancelButtonChildren && onClose) && (
|
{cancelButtonChildren && onClose && (
|
||||||
<Button
|
<Button onClick={onClose} data-test="confirmation-cancel-button">
|
||||||
onClick={onClose}
|
{cancelButtonChildren}
|
||||||
data-test="confirmation-cancel-button">{cancelButtonChildren}</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(confirmButtionChildren && onConfirm) && (
|
{confirmButtionChildren && onConfirm && (
|
||||||
<Button
|
<Button
|
||||||
onClick={onConfirm}
|
onClick={onConfirm}
|
||||||
color="error"
|
color="error"
|
||||||
data-test="confirmation-confirm-button">
|
data-test="confirmation-confirm-button"
|
||||||
|
>
|
||||||
{confirmButtionChildren}
|
{confirmButtionChildren}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
8
packages/web/src/components/Container/index.jsx
Normal file
8
packages/web/src/components/Container/index.jsx
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import MuiContainer from '@mui/material/Container';
|
||||||
|
export default function Container(props) {
|
||||||
|
return <MuiContainer {...props} />;
|
||||||
|
}
|
||||||
|
Container.defaultProps = {
|
||||||
|
maxWidth: 'lg',
|
||||||
|
};
|
@@ -1,10 +0,0 @@
|
|||||||
import * as React from 'react';
|
|
||||||
import MuiContainer, { ContainerProps } from '@mui/material/Container';
|
|
||||||
|
|
||||||
export default function Container(props: ContainerProps): React.ReactElement {
|
|
||||||
return <MuiContainer {...props} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
Container.defaultProps = {
|
|
||||||
maxWidth: 'lg',
|
|
||||||
};
|
|
@@ -1,39 +1,19 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Controller, useFormContext } from 'react-hook-form';
|
import { Controller, useFormContext } from 'react-hook-form';
|
||||||
import FormHelperText from '@mui/material/FormHelperText';
|
import FormHelperText from '@mui/material/FormHelperText';
|
||||||
import Autocomplete, {
|
import Autocomplete, { createFilterOptions } from '@mui/material/Autocomplete';
|
||||||
AutocompleteProps,
|
|
||||||
createFilterOptions,
|
|
||||||
} from '@mui/material/Autocomplete';
|
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import type { IFieldDropdownOption } from 'types';
|
const getOption = (options, value) =>
|
||||||
|
|
||||||
interface ControlledAutocompleteProps
|
|
||||||
extends AutocompleteProps<IFieldDropdownOption, boolean, boolean, boolean> {
|
|
||||||
shouldUnregister?: boolean;
|
|
||||||
name: string;
|
|
||||||
required?: boolean;
|
|
||||||
showOptionValue?: boolean;
|
|
||||||
description?: string;
|
|
||||||
dependsOn?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const getOption = (options: readonly IFieldDropdownOption[], value: string) =>
|
|
||||||
options.find((option) => option.value === value) || null;
|
options.find((option) => option.value === value) || null;
|
||||||
|
|
||||||
// Enables filtering by value in autocomplete dropdown
|
// Enables filtering by value in autocomplete dropdown
|
||||||
const filterOptions = createFilterOptions<IFieldDropdownOption>({
|
const filterOptions = createFilterOptions({
|
||||||
stringify: ({ label, value }) => `
|
stringify: ({ label, value }) => `
|
||||||
${label}
|
${label}
|
||||||
${value}
|
${value}
|
||||||
`,
|
`,
|
||||||
});
|
});
|
||||||
|
function ControlledAutocomplete(props) {
|
||||||
function ControlledAutocomplete(
|
|
||||||
props: ControlledAutocompleteProps
|
|
||||||
): React.ReactElement {
|
|
||||||
const { control, watch, setValue, resetField } = useFormContext();
|
const { control, watch, setValue, resetField } = useFormContext();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
required = false,
|
required = false,
|
||||||
name,
|
name,
|
||||||
@@ -47,23 +27,19 @@ function ControlledAutocomplete(
|
|||||||
showOptionValue,
|
showOptionValue,
|
||||||
...autocompleteProps
|
...autocompleteProps
|
||||||
} = props;
|
} = props;
|
||||||
|
let dependsOnValues = [];
|
||||||
let dependsOnValues: unknown[] = [];
|
|
||||||
if (dependsOn?.length) {
|
if (dependsOn?.length) {
|
||||||
dependsOnValues = watch(dependsOn);
|
dependsOnValues = watch(dependsOn);
|
||||||
}
|
}
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const hasDependencies = dependsOnValues.length;
|
const hasDependencies = dependsOnValues.length;
|
||||||
const allDepsSatisfied = dependsOnValues.every(Boolean);
|
const allDepsSatisfied = dependsOnValues.every(Boolean);
|
||||||
|
|
||||||
if (hasDependencies && !allDepsSatisfied) {
|
if (hasDependencies && !allDepsSatisfied) {
|
||||||
// Reset the field if any dependency is not satisfied
|
// Reset the field if any dependency is not satisfied
|
||||||
setValue(name, null);
|
setValue(name, null);
|
||||||
resetField(name);
|
resetField(name);
|
||||||
}
|
}
|
||||||
}, dependsOnValues);
|
}, dependsOnValues);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Controller
|
<Controller
|
||||||
rules={{ required }}
|
rules={{ required }}
|
||||||
@@ -89,20 +65,18 @@ function ControlledAutocomplete(
|
|||||||
filterOptions={filterOptions}
|
filterOptions={filterOptions}
|
||||||
value={getOption(options, field.value)}
|
value={getOption(options, field.value)}
|
||||||
onChange={(event, selectedOption, reason, details) => {
|
onChange={(event, selectedOption, reason, details) => {
|
||||||
const typedSelectedOption =
|
const typedSelectedOption = selectedOption;
|
||||||
selectedOption as IFieldDropdownOption;
|
|
||||||
if (
|
if (
|
||||||
typedSelectedOption !== null &&
|
typedSelectedOption !== null &&
|
||||||
Object.prototype.hasOwnProperty.call(
|
Object.prototype.hasOwnProperty.call(
|
||||||
typedSelectedOption,
|
typedSelectedOption,
|
||||||
'value'
|
'value',
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
controllerOnChange(typedSelectedOption.value);
|
controllerOnChange(typedSelectedOption.value);
|
||||||
} else {
|
} else {
|
||||||
controllerOnChange(typedSelectedOption);
|
controllerOnChange(typedSelectedOption);
|
||||||
}
|
}
|
||||||
|
|
||||||
onChange?.(event, selectedOption, reason, details);
|
onChange?.(event, selectedOption, reason, details);
|
||||||
}}
|
}}
|
||||||
onBlur={(...args) => {
|
onBlur={(...args) => {
|
||||||
@@ -139,5 +113,4 @@ function ControlledAutocomplete(
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ControlledAutocomplete;
|
export default ControlledAutocomplete;
|
@@ -1,16 +1,7 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Controller, useFormContext } from 'react-hook-form';
|
import { Controller, useFormContext } from 'react-hook-form';
|
||||||
import Checkbox, { CheckboxProps } from '@mui/material/Checkbox';
|
import Checkbox from '@mui/material/Checkbox';
|
||||||
|
export default function ControlledCheckbox(props) {
|
||||||
type ControlledCheckboxProps = {
|
|
||||||
name: string;
|
|
||||||
defaultValue?: boolean;
|
|
||||||
dataTest?: string;
|
|
||||||
} & Omit<CheckboxProps, 'defaultValue'>;
|
|
||||||
|
|
||||||
export default function ControlledCheckbox(
|
|
||||||
props: ControlledCheckboxProps
|
|
||||||
): React.ReactElement {
|
|
||||||
const { control } = useFormContext();
|
const { control } = useFormContext();
|
||||||
const {
|
const {
|
||||||
required,
|
required,
|
||||||
@@ -22,7 +13,6 @@ export default function ControlledCheckbox(
|
|||||||
dataTest,
|
dataTest,
|
||||||
...checkboxProps
|
...checkboxProps
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Controller
|
<Controller
|
||||||
rules={{ required }}
|
rules={{ required }}
|
@@ -1,17 +1,6 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Controller as RHFController, useFormContext } from 'react-hook-form';
|
import { Controller as RHFController, useFormContext } from 'react-hook-form';
|
||||||
|
function Controller(props) {
|
||||||
interface ControllerProps {
|
|
||||||
defaultValue?: string;
|
|
||||||
name: string;
|
|
||||||
required?: boolean;
|
|
||||||
shouldUnregister?: boolean;
|
|
||||||
children: React.ReactElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
function Controller(
|
|
||||||
props: ControllerProps
|
|
||||||
): React.ReactElement {
|
|
||||||
const { control } = useFormContext();
|
const { control } = useFormContext();
|
||||||
const {
|
const {
|
||||||
defaultValue = '',
|
defaultValue = '',
|
||||||
@@ -20,7 +9,6 @@ function Controller(
|
|||||||
shouldUnregister,
|
shouldUnregister,
|
||||||
children,
|
children,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RHFController
|
<RHFController
|
||||||
rules={{ required }}
|
rules={{ required }}
|
||||||
@@ -28,11 +16,8 @@ function Controller(
|
|||||||
control={control}
|
control={control}
|
||||||
defaultValue={defaultValue}
|
defaultValue={defaultValue}
|
||||||
shouldUnregister={shouldUnregister ?? false}
|
shouldUnregister={shouldUnregister ?? false}
|
||||||
render={({
|
render={({ field }) => React.cloneElement(children, { field })}
|
||||||
field,
|
|
||||||
}) => React.cloneElement(children, { field })}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Controller;
|
export default Controller;
|
@@ -2,27 +2,11 @@ import Paper from '@mui/material/Paper';
|
|||||||
import Popper from '@mui/material/Popper';
|
import Popper from '@mui/material/Popper';
|
||||||
import Tab from '@mui/material/Tab';
|
import Tab from '@mui/material/Tab';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
||||||
import type { IFieldDropdownOption } from 'types';
|
|
||||||
import Suggestions from 'components/PowerInput/Suggestions';
|
import Suggestions from 'components/PowerInput/Suggestions';
|
||||||
import TabPanel from 'components/TabPanel';
|
import TabPanel from 'components/TabPanel';
|
||||||
|
|
||||||
import Options from './Options';
|
import Options from './Options';
|
||||||
import { Tabs } from './style';
|
import { Tabs } from './style';
|
||||||
|
const CustomOptions = (props) => {
|
||||||
interface CustomOptionsProps {
|
|
||||||
open: boolean;
|
|
||||||
anchorEl: any;
|
|
||||||
data: any;
|
|
||||||
options: readonly IFieldDropdownOption[];
|
|
||||||
onSuggestionClick: any;
|
|
||||||
onOptionClick: (event: React.MouseEvent, option: any) => void;
|
|
||||||
onTabChange: (tabIndex: 0 | 1) => void;
|
|
||||||
label?: string;
|
|
||||||
initialTabIndex?: 0 | 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const CustomOptions = (props: CustomOptionsProps) => {
|
|
||||||
const {
|
const {
|
||||||
open,
|
open,
|
||||||
anchorEl,
|
anchorEl,
|
||||||
@@ -34,24 +18,18 @@ const CustomOptions = (props: CustomOptionsProps) => {
|
|||||||
label,
|
label,
|
||||||
initialTabIndex,
|
initialTabIndex,
|
||||||
} = props;
|
} = props;
|
||||||
|
const [activeTabIndex, setActiveTabIndex] = React.useState(undefined);
|
||||||
const [activeTabIndex, setActiveTabIndex] = React.useState<
|
|
||||||
number | undefined
|
|
||||||
>(undefined);
|
|
||||||
|
|
||||||
React.useEffect(
|
React.useEffect(
|
||||||
function applyInitialActiveTabIndex() {
|
function applyInitialActiveTabIndex() {
|
||||||
setActiveTabIndex((currentActiveTabIndex) => {
|
setActiveTabIndex((currentActiveTabIndex) => {
|
||||||
if (currentActiveTabIndex === undefined) {
|
if (currentActiveTabIndex === undefined) {
|
||||||
return initialTabIndex;
|
return initialTabIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
return currentActiveTabIndex;
|
return currentActiveTabIndex;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[initialTabIndex]
|
[initialTabIndex],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popper
|
<Popper
|
||||||
open={open}
|
open={open}
|
||||||
@@ -91,5 +69,4 @@ const CustomOptions = (props: CustomOptionsProps) => {
|
|||||||
</Popper>
|
</Popper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CustomOptions;
|
export default CustomOptions;
|
@@ -1,35 +1,23 @@
|
|||||||
import type { IFieldDropdownOption } from 'types';
|
|
||||||
import ListItemButton from '@mui/material/ListItemButton';
|
import ListItemButton from '@mui/material/ListItemButton';
|
||||||
import ListItemText from '@mui/material/ListItemText';
|
import ListItemText from '@mui/material/ListItemText';
|
||||||
import throttle from 'lodash/throttle';
|
import throttle from 'lodash/throttle';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { FixedSizeList, ListChildComponentProps } from 'react-window';
|
import { FixedSizeList } from 'react-window';
|
||||||
|
|
||||||
import { Typography } from '@mui/material';
|
import { Typography } from '@mui/material';
|
||||||
import SearchInput from 'components/SearchInput';
|
import SearchInput from 'components/SearchInput';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
import { SearchInputWrapper } from './style';
|
import { SearchInputWrapper } from './style';
|
||||||
|
|
||||||
interface OptionsProps {
|
|
||||||
data: readonly IFieldDropdownOption[];
|
|
||||||
onOptionClick: (event: React.MouseEvent, option: any) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SHORT_LIST_LENGTH = 4;
|
const SHORT_LIST_LENGTH = 4;
|
||||||
const LIST_ITEM_HEIGHT = 64;
|
const LIST_ITEM_HEIGHT = 64;
|
||||||
|
const computeListHeight = (currentLength) => {
|
||||||
const computeListHeight = (currentLength: number) => {
|
|
||||||
const numberOfRenderedItems = Math.min(SHORT_LIST_LENGTH, currentLength);
|
const numberOfRenderedItems = Math.min(SHORT_LIST_LENGTH, currentLength);
|
||||||
return LIST_ITEM_HEIGHT * numberOfRenderedItems;
|
return LIST_ITEM_HEIGHT * numberOfRenderedItems;
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderItemFactory =
|
const renderItemFactory =
|
||||||
({ onOptionClick }: Pick<OptionsProps, 'onOptionClick'>) =>
|
({ onOptionClick }) =>
|
||||||
(props: ListChildComponentProps) => {
|
(props) => {
|
||||||
const { index, style, data } = props;
|
const { index, style, data } = props;
|
||||||
|
|
||||||
const suboption = data[index];
|
const suboption = data[index];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListItemButton
|
<ListItemButton
|
||||||
sx={{ pl: 4 }}
|
sx={{ pl: 4 }}
|
||||||
@@ -56,55 +44,45 @@ const renderItemFactory =
|
|||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
const Options = (props) => {
|
||||||
const Options = (props: OptionsProps) => {
|
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
const { data, onOptionClick } = props;
|
const { data, onOptionClick } = props;
|
||||||
const [filteredData, setFilteredData] =
|
const [filteredData, setFilteredData] = React.useState(data);
|
||||||
React.useState<readonly IFieldDropdownOption[]>(data);
|
|
||||||
|
|
||||||
React.useEffect(
|
React.useEffect(
|
||||||
function syncOptions() {
|
function syncOptions() {
|
||||||
setFilteredData((filteredData) => {
|
setFilteredData((filteredData) => {
|
||||||
if (filteredData.length === 0 && filteredData.length !== data.length) {
|
if (filteredData.length === 0 && filteredData.length !== data.length) {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
return filteredData;
|
return filteredData;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[data]
|
[data],
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderItem = React.useMemo(
|
const renderItem = React.useMemo(
|
||||||
() =>
|
() =>
|
||||||
renderItemFactory({
|
renderItemFactory({
|
||||||
onOptionClick,
|
onOptionClick,
|
||||||
}),
|
}),
|
||||||
[onOptionClick]
|
[onOptionClick],
|
||||||
);
|
);
|
||||||
|
|
||||||
const onSearchChange = React.useMemo(
|
const onSearchChange = React.useMemo(
|
||||||
() =>
|
() =>
|
||||||
throttle((event: React.ChangeEvent) => {
|
throttle((event) => {
|
||||||
const search = (event.target as HTMLInputElement).value.toLowerCase();
|
const search = event.target.value.toLowerCase();
|
||||||
|
|
||||||
if (!search) {
|
if (!search) {
|
||||||
setFilteredData(data);
|
setFilteredData(data);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newFilteredData = data.filter((option) =>
|
const newFilteredData = data.filter((option) =>
|
||||||
`${option.label}\n${option.value}`
|
`${option.label}\n${option.value}`
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.includes(search.toLowerCase())
|
.includes(search.toLowerCase()),
|
||||||
);
|
);
|
||||||
|
|
||||||
setFilteredData(newFilteredData);
|
setFilteredData(newFilteredData);
|
||||||
}, 400),
|
}, 400),
|
||||||
[data]
|
[data],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SearchInputWrapper>
|
<SearchInputWrapper>
|
||||||
@@ -130,5 +108,4 @@ const Options = (props: OptionsProps) => {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Options;
|
export default Options;
|
@@ -2,20 +2,15 @@ import * as React from 'react';
|
|||||||
import { useController, useFormContext } from 'react-hook-form';
|
import { useController, useFormContext } from 'react-hook-form';
|
||||||
import { IconButton } from '@mui/material';
|
import { IconButton } from '@mui/material';
|
||||||
import FormHelperText from '@mui/material/FormHelperText';
|
import FormHelperText from '@mui/material/FormHelperText';
|
||||||
import { AutocompleteProps } from '@mui/material/Autocomplete';
|
|
||||||
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
|
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
|
||||||
import ClearIcon from '@mui/icons-material/Clear';
|
import ClearIcon from '@mui/icons-material/Clear';
|
||||||
import type { IFieldDropdownOption } from 'types';
|
|
||||||
import { ActionButtonsWrapper } from './style';
|
import { ActionButtonsWrapper } from './style';
|
||||||
|
|
||||||
import ClickAwayListener from '@mui/base/ClickAwayListener';
|
import ClickAwayListener from '@mui/base/ClickAwayListener';
|
||||||
import InputLabel from '@mui/material/InputLabel';
|
import InputLabel from '@mui/material/InputLabel';
|
||||||
import { createEditor } from 'slate';
|
import { createEditor } from 'slate';
|
||||||
import { Editable, ReactEditor } from 'slate-react';
|
import { Editable, ReactEditor } from 'slate-react';
|
||||||
|
|
||||||
import Slate from 'components/Slate';
|
import Slate from 'components/Slate';
|
||||||
import Element from 'components/Slate/Element';
|
import Element from 'components/Slate/Element';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
serialize,
|
serialize,
|
||||||
deserialize,
|
deserialize,
|
||||||
@@ -30,32 +25,10 @@ import {
|
|||||||
InputLabelWrapper,
|
InputLabelWrapper,
|
||||||
ChildrenWrapper,
|
ChildrenWrapper,
|
||||||
} from 'components/PowerInput/style';
|
} from 'components/PowerInput/style';
|
||||||
import { VariableElement } from 'components/Slate/types';
|
|
||||||
import CustomOptions from './CustomOptions';
|
import CustomOptions from './CustomOptions';
|
||||||
import { processStepWithExecutions } from 'components/PowerInput/data';
|
import { processStepWithExecutions } from 'components/PowerInput/data';
|
||||||
import { StepExecutionsContext } from 'contexts/StepExecutions';
|
import { StepExecutionsContext } from 'contexts/StepExecutions';
|
||||||
|
function ControlledCustomAutocomplete(props) {
|
||||||
interface ControlledCustomAutocompleteProps
|
|
||||||
extends AutocompleteProps<IFieldDropdownOption, boolean, boolean, boolean> {
|
|
||||||
showOptionValue?: boolean;
|
|
||||||
dependsOn?: string[];
|
|
||||||
|
|
||||||
defaultValue?: string;
|
|
||||||
name: string;
|
|
||||||
label?: string;
|
|
||||||
type?: string;
|
|
||||||
required?: boolean;
|
|
||||||
readOnly?: boolean;
|
|
||||||
description?: string;
|
|
||||||
docUrl?: string;
|
|
||||||
clickToCopy?: boolean;
|
|
||||||
disabled?: boolean;
|
|
||||||
shouldUnregister?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ControlledCustomAutocomplete(
|
|
||||||
props: ControlledCustomAutocompleteProps
|
|
||||||
): React.ReactElement {
|
|
||||||
const {
|
const {
|
||||||
defaultValue = '',
|
defaultValue = '',
|
||||||
name,
|
name,
|
||||||
@@ -83,63 +56,48 @@ function ControlledCustomAutocomplete(
|
|||||||
} = field;
|
} = field;
|
||||||
const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
|
const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
|
||||||
const [isInitialValueSet, setInitialValue] = React.useState(false);
|
const [isInitialValueSet, setInitialValue] = React.useState(false);
|
||||||
const [isSingleChoice, setSingleChoice] = React.useState<boolean | undefined>(
|
const [isSingleChoice, setSingleChoice] = React.useState(undefined);
|
||||||
undefined
|
|
||||||
);
|
|
||||||
const priorStepsWithExecutions = React.useContext(StepExecutionsContext);
|
const priorStepsWithExecutions = React.useContext(StepExecutionsContext);
|
||||||
const editorRef = React.useRef<HTMLDivElement | null>(null);
|
const editorRef = React.useRef(null);
|
||||||
const renderElement = React.useCallback(
|
const renderElement = React.useCallback(
|
||||||
(props) => <Element {...props} disabled={disabled} />,
|
(props) => <Element {...props} disabled={disabled} />,
|
||||||
[disabled]
|
[disabled],
|
||||||
);
|
);
|
||||||
const [editor] = React.useState(() => customizeEditor(createEditor()));
|
const [editor] = React.useState(() => customizeEditor(createEditor()));
|
||||||
const [showVariableSuggestions, setShowVariableSuggestions] =
|
const [showVariableSuggestions, setShowVariableSuggestions] =
|
||||||
React.useState(false);
|
React.useState(false);
|
||||||
|
let dependsOnValues = [];
|
||||||
let dependsOnValues: unknown[] = [];
|
|
||||||
if (dependsOn?.length) {
|
if (dependsOn?.length) {
|
||||||
dependsOnValues = watch(dependsOn);
|
dependsOnValues = watch(dependsOn);
|
||||||
}
|
}
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const ref = ReactEditor.toDOMNode(editor, editor);
|
const ref = ReactEditor.toDOMNode(editor, editor);
|
||||||
|
|
||||||
resizeObserver.observe(ref);
|
resizeObserver.observe(ref);
|
||||||
|
|
||||||
return () => resizeObserver.unobserve(ref);
|
return () => resizeObserver.unobserve(ref);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const promoteValue = () => {
|
const promoteValue = () => {
|
||||||
const serializedValue = serialize(editor.children);
|
const serializedValue = serialize(editor.children);
|
||||||
controllerOnChange(serializedValue);
|
controllerOnChange(serializedValue);
|
||||||
};
|
};
|
||||||
|
|
||||||
const resizeObserver = React.useMemo(function syncCustomOptionsPosition() {
|
const resizeObserver = React.useMemo(function syncCustomOptionsPosition() {
|
||||||
return new ResizeObserver(() => {
|
return new ResizeObserver(() => {
|
||||||
forceUpdate();
|
forceUpdate();
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const hasDependencies = dependsOnValues.length;
|
const hasDependencies = dependsOnValues.length;
|
||||||
|
|
||||||
if (hasDependencies) {
|
if (hasDependencies) {
|
||||||
// Reset the field when a dependent has been updated
|
// Reset the field when a dependent has been updated
|
||||||
resetEditor(editor);
|
resetEditor(editor);
|
||||||
}
|
}
|
||||||
}, dependsOnValues);
|
}, dependsOnValues);
|
||||||
|
|
||||||
React.useEffect(
|
React.useEffect(
|
||||||
function updateInitialValue() {
|
function updateInitialValue() {
|
||||||
const hasOptions = options.length;
|
const hasOptions = options.length;
|
||||||
const isOptionsLoaded = loading === false;
|
const isOptionsLoaded = loading === false;
|
||||||
if (!isInitialValueSet && hasOptions && isOptionsLoaded) {
|
if (!isInitialValueSet && hasOptions && isOptionsLoaded) {
|
||||||
setInitialValue(true);
|
setInitialValue(true);
|
||||||
|
const option = options.find((option) => option.value === value);
|
||||||
const option: IFieldDropdownOption | undefined = options.find(
|
|
||||||
(option) => option.value === value
|
|
||||||
);
|
|
||||||
|
|
||||||
if (option) {
|
if (option) {
|
||||||
overrideEditorValue(editor, { option, focus: false });
|
overrideEditorValue(editor, { option, focus: false });
|
||||||
setSingleChoice(true);
|
setSingleChoice(true);
|
||||||
@@ -148,70 +106,56 @@ function ControlledCustomAutocomplete(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[isInitialValueSet, options, loading]
|
[isInitialValueSet, options, loading],
|
||||||
);
|
);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!showVariableSuggestions && value !== serialize(editor.children)) {
|
if (!showVariableSuggestions && value !== serialize(editor.children)) {
|
||||||
promoteValue();
|
promoteValue();
|
||||||
}
|
}
|
||||||
}, [showVariableSuggestions]);
|
}, [showVariableSuggestions]);
|
||||||
|
const hideSuggestionsOnShift = (event) => {
|
||||||
const hideSuggestionsOnShift = (
|
|
||||||
event: React.KeyboardEvent<HTMLInputElement>
|
|
||||||
) => {
|
|
||||||
if (event.code === 'Tab') {
|
if (event.code === 'Tab') {
|
||||||
setShowVariableSuggestions(false);
|
setShowVariableSuggestions(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const handleKeyDown = (event) => {
|
||||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
|
||||||
hideSuggestionsOnShift(event);
|
hideSuggestionsOnShift(event);
|
||||||
if (event.code === 'Tab') {
|
if (event.code === 'Tab') {
|
||||||
promoteValue();
|
promoteValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isSingleChoice && event.code !== 'Tab') {
|
if (isSingleChoice && event.code !== 'Tab') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const stepsWithVariables = React.useMemo(() => {
|
const stepsWithVariables = React.useMemo(() => {
|
||||||
return processStepWithExecutions(priorStepsWithExecutions);
|
return processStepWithExecutions(priorStepsWithExecutions);
|
||||||
}, [priorStepsWithExecutions]);
|
}, [priorStepsWithExecutions]);
|
||||||
|
|
||||||
const handleVariableSuggestionClick = React.useCallback(
|
const handleVariableSuggestionClick = React.useCallback(
|
||||||
(variable: Pick<VariableElement, 'name' | 'value'>) => {
|
(variable) => {
|
||||||
insertVariable(editor, variable, stepsWithVariables);
|
insertVariable(editor, variable, stepsWithVariables);
|
||||||
},
|
},
|
||||||
[stepsWithVariables]
|
[stepsWithVariables],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleOptionClick = React.useCallback(
|
const handleOptionClick = React.useCallback(
|
||||||
(event: React.MouseEvent, option: IFieldDropdownOption) => {
|
(event, option) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
overrideEditorValue(editor, { option, focus: false });
|
overrideEditorValue(editor, { option, focus: false });
|
||||||
setShowVariableSuggestions(false);
|
setShowVariableSuggestions(false);
|
||||||
setSingleChoice(true);
|
setSingleChoice(true);
|
||||||
},
|
},
|
||||||
[stepsWithVariables]
|
[stepsWithVariables],
|
||||||
);
|
);
|
||||||
|
const handleClearButtonClick = (event) => {
|
||||||
const handleClearButtonClick = (event: React.MouseEvent) => {
|
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
resetEditor(editor);
|
resetEditor(editor);
|
||||||
promoteValue();
|
promoteValue();
|
||||||
setSingleChoice(undefined);
|
setSingleChoice(undefined);
|
||||||
};
|
};
|
||||||
|
const reset = (tabIndex) => {
|
||||||
const reset = (tabIndex: 0 | 1) => {
|
|
||||||
const isOptions = tabIndex === 0;
|
const isOptions = tabIndex === 0;
|
||||||
|
|
||||||
setSingleChoice(isOptions);
|
setSingleChoice(isOptions);
|
||||||
|
|
||||||
resetEditor(editor, { focus: true });
|
resetEditor(editor, { focus: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Slate
|
<Slate
|
||||||
editor={editor}
|
editor={editor}
|
||||||
@@ -313,5 +257,4 @@ function ControlledCustomAutocomplete(
|
|||||||
</Slate>
|
</Slate>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ControlledCustomAutocomplete;
|
export default ControlledCustomAutocomplete;
|
@@ -1,18 +1,15 @@
|
|||||||
import { styled } from '@mui/material/styles';
|
import { styled } from '@mui/material/styles';
|
||||||
import Stack from '@mui/material/Stack';
|
import Stack from '@mui/material/Stack';
|
||||||
import MuiTabs from '@mui/material/Tabs';
|
import MuiTabs from '@mui/material/Tabs';
|
||||||
|
|
||||||
export const ActionButtonsWrapper = styled(Stack)`
|
export const ActionButtonsWrapper = styled(Stack)`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0;
|
right: 0;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Tabs = styled(MuiTabs)`
|
export const Tabs = styled(MuiTabs)`
|
||||||
border-bottom: 1px solid ${({ theme }) => theme.palette.divider};
|
border-bottom: 1px solid ${({ theme }) => theme.palette.divider};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const SearchInputWrapper = styled('div')`
|
export const SearchInputWrapper = styled('div')`
|
||||||
padding: ${({ theme }) => theme.spacing(0, 2, 2, 2)};
|
padding: ${({ theme }) => theme.spacing(0, 2, 2, 2)};
|
||||||
`;
|
`;
|
@@ -1,13 +1,9 @@
|
|||||||
import useConfig from 'hooks/useConfig';
|
import useConfig from 'hooks/useConfig';
|
||||||
import { LogoImage } from './style.ee';
|
import { LogoImage } from './style.ee';
|
||||||
|
|
||||||
const CustomLogo = () => {
|
const CustomLogo = () => {
|
||||||
const { config, loading } = useConfig(['logo.svgData']);
|
const { config, loading } = useConfig(['logo.svgData']);
|
||||||
|
|
||||||
if (loading || !config?.['logo.svgData']) return null;
|
if (loading || !config?.['logo.svgData']) return null;
|
||||||
|
const logoSvgData = config['logo.svgData'];
|
||||||
const logoSvgData = config['logo.svgData'] as string;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LogoImage
|
<LogoImage
|
||||||
data-test="custom-logo"
|
data-test="custom-logo"
|
||||||
@@ -15,5 +11,4 @@ const CustomLogo = () => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CustomLogo;
|
export default CustomLogo;
|
@@ -1,5 +1,4 @@
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
export const LogoImage = styled('img')(() => ({
|
export const LogoImage = styled('img')(() => ({
|
||||||
maxWidth: 200,
|
maxWidth: 200,
|
||||||
maxHeight: 22,
|
maxHeight: 22,
|
@@ -1,22 +1,16 @@
|
|||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import MationLogo from 'components/MationLogo';
|
import MationLogo from 'components/MationLogo';
|
||||||
import useAutomatischInfo from 'hooks/useAutomatischInfo';
|
import useAutomatischInfo from 'hooks/useAutomatischInfo';
|
||||||
|
|
||||||
const DefaultLogo = () => {
|
const DefaultLogo = () => {
|
||||||
const { isMation, loading } = useAutomatischInfo();
|
const { isMation, loading } = useAutomatischInfo();
|
||||||
|
|
||||||
if (loading) return <React.Fragment />;
|
if (loading) return <React.Fragment />;
|
||||||
|
|
||||||
if (isMation) return <MationLogo />;
|
if (isMation) return <MationLogo />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Typography variant="h6" component="h1" data-test="typography-logo" noWrap>
|
<Typography variant="h6" component="h1" data-test="typography-logo" noWrap>
|
||||||
<FormattedMessage id="brandText" />
|
<FormattedMessage id="brandText" />
|
||||||
</Typography>
|
</Typography>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DefaultLogo;
|
export default DefaultLogo;
|
@@ -1,7 +1,6 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useMutation } from '@apollo/client';
|
import { useMutation } from '@apollo/client';
|
||||||
|
|
||||||
import * as URLS from 'config/urls';
|
import * as URLS from 'config/urls';
|
||||||
import ConfirmationDialog from 'components/ConfirmationDialog';
|
import ConfirmationDialog from 'components/ConfirmationDialog';
|
||||||
import apolloClient from 'graphql/client';
|
import apolloClient from 'graphql/client';
|
||||||
@@ -9,27 +8,18 @@ import { DELETE_CURRENT_USER } from 'graphql/mutations/delete-current-user.ee';
|
|||||||
import useAuthentication from 'hooks/useAuthentication';
|
import useAuthentication from 'hooks/useAuthentication';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
import useCurrentUser from 'hooks/useCurrentUser';
|
import useCurrentUser from 'hooks/useCurrentUser';
|
||||||
|
export default function DeleteAccountDialog(props) {
|
||||||
type DeleteAccountDialogProps = {
|
|
||||||
onClose: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function DeleteAccountDialog(props: DeleteAccountDialogProps) {
|
|
||||||
const [deleteCurrentUser] = useMutation(DELETE_CURRENT_USER);
|
const [deleteCurrentUser] = useMutation(DELETE_CURRENT_USER);
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
const currentUser = useCurrentUser();
|
const currentUser = useCurrentUser();
|
||||||
const authentication = useAuthentication();
|
const authentication = useAuthentication();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const handleConfirm = React.useCallback(async () => {
|
const handleConfirm = React.useCallback(async () => {
|
||||||
await deleteCurrentUser();
|
await deleteCurrentUser();
|
||||||
|
|
||||||
authentication.updateToken('');
|
authentication.updateToken('');
|
||||||
await apolloClient.clearStore();
|
await apolloClient.clearStore();
|
||||||
|
|
||||||
navigate(URLS.LOGIN);
|
navigate(URLS.LOGIN);
|
||||||
}, [deleteCurrentUser, currentUser]);
|
}, [deleteCurrentUser, currentUser]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ConfirmationDialog
|
<ConfirmationDialog
|
||||||
title={formatMessage('deleteAccountDialog.title')}
|
title={formatMessage('deleteAccountDialog.title')}
|
@@ -3,18 +3,11 @@ import DeleteIcon from '@mui/icons-material/Delete';
|
|||||||
import IconButton from '@mui/material/IconButton';
|
import IconButton from '@mui/material/IconButton';
|
||||||
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
|
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
||||||
import Can from 'components/Can';
|
import Can from 'components/Can';
|
||||||
import ConfirmationDialog from 'components/ConfirmationDialog';
|
import ConfirmationDialog from 'components/ConfirmationDialog';
|
||||||
import { DELETE_ROLE } from 'graphql/mutations/delete-role.ee';
|
import { DELETE_ROLE } from 'graphql/mutations/delete-role.ee';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
|
export default function DeleteRoleButton(props) {
|
||||||
type DeleteRoleButtonProps = {
|
|
||||||
disabled?: boolean;
|
|
||||||
roleId: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function DeleteRoleButton(props: DeleteRoleButtonProps) {
|
|
||||||
const { disabled, roleId } = props;
|
const { disabled, roleId } = props;
|
||||||
const [showConfirmation, setShowConfirmation] = React.useState(false);
|
const [showConfirmation, setShowConfirmation] = React.useState(false);
|
||||||
const [deleteRole] = useMutation(DELETE_ROLE, {
|
const [deleteRole] = useMutation(DELETE_ROLE, {
|
||||||
@@ -23,23 +16,20 @@ export default function DeleteRoleButton(props: DeleteRoleButtonProps) {
|
|||||||
});
|
});
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
const enqueueSnackbar = useEnqueueSnackbar();
|
const enqueueSnackbar = useEnqueueSnackbar();
|
||||||
|
|
||||||
const handleConfirm = React.useCallback(async () => {
|
const handleConfirm = React.useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
await deleteRole();
|
await deleteRole();
|
||||||
|
|
||||||
setShowConfirmation(false);
|
setShowConfirmation(false);
|
||||||
enqueueSnackbar(formatMessage('deleteRoleButton.successfullyDeleted'), {
|
enqueueSnackbar(formatMessage('deleteRoleButton.successfullyDeleted'), {
|
||||||
variant: 'success',
|
variant: 'success',
|
||||||
SnackbarProps: {
|
SnackbarProps: {
|
||||||
'data-test': 'snackbar-delete-role-success'
|
'data-test': 'snackbar-delete-role-success',
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error('Failed while deleting!');
|
throw new Error('Failed while deleting!');
|
||||||
}
|
}
|
||||||
}, [deleteRole]);
|
}, [deleteRole]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Can I="delete" a="Role" passThrough>
|
<Can I="delete" a="Role" passThrough>
|
@@ -3,16 +3,10 @@ import DeleteIcon from '@mui/icons-material/Delete';
|
|||||||
import IconButton from '@mui/material/IconButton';
|
import IconButton from '@mui/material/IconButton';
|
||||||
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';
|
||||||
import { DELETE_USER } from 'graphql/mutations/delete-user.ee';
|
import { DELETE_USER } from 'graphql/mutations/delete-user.ee';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
|
export default function DeleteUserButton(props) {
|
||||||
type DeleteUserButtonProps = {
|
|
||||||
userId: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function DeleteUserButton(props: DeleteUserButtonProps) {
|
|
||||||
const { userId } = props;
|
const { userId } = props;
|
||||||
const [showConfirmation, setShowConfirmation] = React.useState(false);
|
const [showConfirmation, setShowConfirmation] = React.useState(false);
|
||||||
const [deleteUser] = useMutation(DELETE_USER, {
|
const [deleteUser] = useMutation(DELETE_USER, {
|
||||||
@@ -21,26 +15,27 @@ export default function DeleteUserButton(props: DeleteUserButtonProps) {
|
|||||||
});
|
});
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
const enqueueSnackbar = useEnqueueSnackbar();
|
const enqueueSnackbar = useEnqueueSnackbar();
|
||||||
|
|
||||||
const handleConfirm = React.useCallback(async () => {
|
const handleConfirm = React.useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
await deleteUser();
|
await deleteUser();
|
||||||
|
|
||||||
setShowConfirmation(false);
|
setShowConfirmation(false);
|
||||||
enqueueSnackbar(formatMessage('deleteUserButton.successfullyDeleted'), {
|
enqueueSnackbar(formatMessage('deleteUserButton.successfullyDeleted'), {
|
||||||
variant: 'success',
|
variant: 'success',
|
||||||
SnackbarProps: {
|
SnackbarProps: {
|
||||||
'data-test': 'snackbar-delete-user-success'
|
'data-test': 'snackbar-delete-user-success',
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error('Failed while deleting!');
|
throw new Error('Failed while deleting!');
|
||||||
}
|
}
|
||||||
}, [deleteUser]);
|
}, [deleteUser]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<IconButton data-test="delete-button" onClick={() => setShowConfirmation(true)} size="small">
|
<IconButton
|
||||||
|
data-test="delete-button"
|
||||||
|
onClick={() => setShowConfirmation(true)}
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
<DeleteIcon />
|
<DeleteIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|
@@ -1,46 +1,26 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useTheme } from '@mui/material/styles';
|
import { useTheme } from '@mui/material/styles';
|
||||||
import { SwipeableDrawerProps } from '@mui/material/SwipeableDrawer';
|
|
||||||
import Toolbar from '@mui/material/Toolbar';
|
import Toolbar from '@mui/material/Toolbar';
|
||||||
import List from '@mui/material/List';
|
import List from '@mui/material/List';
|
||||||
import Divider from '@mui/material/Divider';
|
import Divider from '@mui/material/Divider';
|
||||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||||
import Badge from '@mui/material/Badge';
|
import Badge from '@mui/material/Badge';
|
||||||
|
|
||||||
import ListItemLink from 'components/ListItemLink';
|
import ListItemLink from 'components/ListItemLink';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
import { Drawer as BaseDrawer } from './style';
|
import { Drawer as BaseDrawer } from './style';
|
||||||
|
|
||||||
const iOS =
|
const iOS =
|
||||||
typeof navigator !== 'undefined' &&
|
typeof navigator !== 'undefined' &&
|
||||||
/iPad|iPhone|iPod/.test(navigator.userAgent);
|
/iPad|iPhone|iPod/.test(navigator.userAgent);
|
||||||
|
export default function Drawer(props) {
|
||||||
type DrawerLink = {
|
|
||||||
Icon: React.ElementType;
|
|
||||||
primary: string;
|
|
||||||
to: string;
|
|
||||||
target?: '_blank';
|
|
||||||
badgeContent?: React.ReactNode;
|
|
||||||
dataTest?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type DrawerProps = {
|
|
||||||
links: DrawerLink[];
|
|
||||||
bottomLinks?: DrawerLink[];
|
|
||||||
} & SwipeableDrawerProps;
|
|
||||||
|
|
||||||
export default function Drawer(props: DrawerProps): React.ReactElement {
|
|
||||||
const { links = [], bottomLinks = [], ...drawerProps } = props;
|
const { links = [], bottomLinks = [], ...drawerProps } = props;
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('md'));
|
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
|
const closeOnClick = (event) => {
|
||||||
const closeOnClick = (event: React.SyntheticEvent) => {
|
|
||||||
if (matchSmallScreens) {
|
if (matchSmallScreens) {
|
||||||
props.onClose(event);
|
props.onClose(event);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseDrawer
|
<BaseDrawer
|
||||||
{...drawerProps}
|
{...drawerProps}
|
||||||
@@ -84,7 +64,7 @@ export default function Drawer(props: DrawerProps): React.ReactElement {
|
|||||||
target={target}
|
target={target}
|
||||||
data-test={dataTest}
|
data-test={dataTest}
|
||||||
/>
|
/>
|
||||||
)
|
),
|
||||||
)}
|
)}
|
||||||
</List>
|
</List>
|
||||||
</BaseDrawer>
|
</BaseDrawer>
|
@@ -1,10 +1,8 @@
|
|||||||
import { styled, Theme, CSSObject } from '@mui/material/styles';
|
import { styled } from '@mui/material/styles';
|
||||||
import { drawerClasses } from '@mui/material/Drawer';
|
import { drawerClasses } from '@mui/material/Drawer';
|
||||||
import MuiSwipeableDrawer from '@mui/material/SwipeableDrawer';
|
import MuiSwipeableDrawer from '@mui/material/SwipeableDrawer';
|
||||||
|
|
||||||
const drawerWidth = 300;
|
const drawerWidth = 300;
|
||||||
|
const openedMixin = (theme) => ({
|
||||||
const openedMixin = (theme: Theme): CSSObject => ({
|
|
||||||
transition: theme.transitions.create('width', {
|
transition: theme.transitions.create('width', {
|
||||||
easing: theme.transitions.easing.sharp,
|
easing: theme.transitions.easing.sharp,
|
||||||
duration: theme.transitions.duration.enteringScreen,
|
duration: theme.transitions.duration.enteringScreen,
|
||||||
@@ -15,8 +13,7 @@ const openedMixin = (theme: Theme): CSSObject => ({
|
|||||||
width: drawerWidth,
|
width: drawerWidth,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const closedMixin = (theme) => ({
|
||||||
const closedMixin = (theme: Theme): CSSObject => ({
|
|
||||||
transition: theme.transitions.create('width', {
|
transition: theme.transitions.create('width', {
|
||||||
easing: theme.transitions.easing.sharp,
|
easing: theme.transitions.easing.sharp,
|
||||||
duration: theme.transitions.duration.leavingScreen,
|
duration: theme.transitions.duration.leavingScreen,
|
||||||
@@ -27,7 +24,6 @@ const closedMixin = (theme: Theme): CSSObject => ({
|
|||||||
width: `calc(${theme.spacing(9)} + 1px)`,
|
width: `calc(${theme.spacing(9)} + 1px)`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const Drawer = styled(MuiSwipeableDrawer)(({ theme, open }) => ({
|
export const Drawer = styled(MuiSwipeableDrawer)(({ theme, open }) => ({
|
||||||
width: drawerWidth,
|
width: drawerWidth,
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
@@ -7,35 +7,13 @@ import Box from '@mui/material/Box';
|
|||||||
import IconButton from '@mui/material/IconButton';
|
import IconButton from '@mui/material/IconButton';
|
||||||
import RemoveIcon from '@mui/icons-material/Remove';
|
import RemoveIcon from '@mui/icons-material/Remove';
|
||||||
import AddIcon from '@mui/icons-material/Add';
|
import AddIcon from '@mui/icons-material/Add';
|
||||||
|
|
||||||
import { IFieldDynamic } from 'types';
|
|
||||||
import InputCreator from 'components/InputCreator';
|
import InputCreator from 'components/InputCreator';
|
||||||
import { EditorContext } from 'contexts/Editor';
|
import { EditorContext } from 'contexts/Editor';
|
||||||
|
function DynamicField(props) {
|
||||||
interface DynamicFieldProps {
|
|
||||||
onChange?: (value: string) => void;
|
|
||||||
onBlur?: (value: string) => void;
|
|
||||||
defaultValue?: Record<string, unknown>[];
|
|
||||||
name: string;
|
|
||||||
label: string;
|
|
||||||
type?: string;
|
|
||||||
required?: boolean;
|
|
||||||
readOnly?: boolean;
|
|
||||||
description?: string;
|
|
||||||
docUrl?: string;
|
|
||||||
clickToCopy?: boolean;
|
|
||||||
disabled?: boolean;
|
|
||||||
fields: IFieldDynamic['fields'];
|
|
||||||
shouldUnregister?: boolean;
|
|
||||||
stepId?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function DynamicField(props: DynamicFieldProps): React.ReactElement {
|
|
||||||
const { label, description, fields, name, defaultValue, stepId } = props;
|
const { label, description, fields, name, defaultValue, stepId } = props;
|
||||||
const { control, setValue, getValues } = useFormContext();
|
const { control, setValue, getValues } = useFormContext();
|
||||||
const fieldsValue = useWatch({ control, name }) as Record<string, unknown>[];
|
const fieldsValue = useWatch({ control, name });
|
||||||
const editorContext = React.useContext(EditorContext);
|
const editorContext = React.useContext(EditorContext);
|
||||||
|
|
||||||
const createEmptyItem = React.useCallback(() => {
|
const createEmptyItem = React.useCallback(() => {
|
||||||
return fields.reduce((previousValue, field) => {
|
return fields.reduce((previousValue, field) => {
|
||||||
return {
|
return {
|
||||||
@@ -45,43 +23,35 @@ function DynamicField(props: DynamicFieldProps): React.ReactElement {
|
|||||||
};
|
};
|
||||||
}, {});
|
}, {});
|
||||||
}, [fields]);
|
}, [fields]);
|
||||||
|
|
||||||
const addItem = React.useCallback(() => {
|
const addItem = React.useCallback(() => {
|
||||||
const values = getValues(name);
|
const values = getValues(name);
|
||||||
|
|
||||||
if (!values) {
|
if (!values) {
|
||||||
setValue(name, [createEmptyItem()]);
|
setValue(name, [createEmptyItem()]);
|
||||||
} else {
|
} else {
|
||||||
setValue(name, values.concat(createEmptyItem()));
|
setValue(name, values.concat(createEmptyItem()));
|
||||||
}
|
}
|
||||||
}, [getValues, createEmptyItem]);
|
}, [getValues, createEmptyItem]);
|
||||||
|
|
||||||
const removeItem = React.useCallback(
|
const removeItem = React.useCallback(
|
||||||
(index) => {
|
(index) => {
|
||||||
if (fieldsValue.length === 1) return;
|
if (fieldsValue.length === 1) return;
|
||||||
|
|
||||||
const newFieldsValue = fieldsValue.filter(
|
const newFieldsValue = fieldsValue.filter(
|
||||||
(fieldValue, fieldIndex) => fieldIndex !== index
|
(fieldValue, fieldIndex) => fieldIndex !== index,
|
||||||
);
|
);
|
||||||
|
|
||||||
setValue(name, newFieldsValue);
|
setValue(name, newFieldsValue);
|
||||||
},
|
},
|
||||||
[fieldsValue]
|
[fieldsValue],
|
||||||
);
|
);
|
||||||
|
|
||||||
React.useEffect(
|
React.useEffect(
|
||||||
function addInitialGroupWhenEmpty() {
|
function addInitialGroupWhenEmpty() {
|
||||||
const fieldValues = getValues(name);
|
const fieldValues = getValues(name);
|
||||||
|
|
||||||
if (!fieldValues && defaultValue) {
|
if (!fieldValues && defaultValue) {
|
||||||
setValue(name, defaultValue);
|
setValue(name, defaultValue);
|
||||||
} else if (!fieldValues) {
|
} else if (!fieldValues) {
|
||||||
setValue(name, [createEmptyItem()]);
|
setValue(name, [createEmptyItem()]);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[createEmptyItem, defaultValue]
|
[createEmptyItem, defaultValue],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<Typography variant="subtitle2">{label}</Typography>
|
<Typography variant="subtitle2">{label}</Typography>
|
||||||
@@ -137,5 +107,4 @@ function DynamicField(props: DynamicFieldProps): React.ReactElement {
|
|||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DynamicField;
|
export default DynamicField;
|
@@ -1,61 +1,40 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import type { TypographyProps } from '@mui/material/Typography';
|
|
||||||
import EditIcon from '@mui/icons-material/Edit';
|
import EditIcon from '@mui/icons-material/Edit';
|
||||||
|
|
||||||
import { Box, TextField } from './style';
|
import { Box, TextField } from './style';
|
||||||
|
|
||||||
type EditableTypographyProps = TypographyProps & {
|
|
||||||
children: string;
|
|
||||||
onConfirm?: (value: string) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const noop = () => null;
|
const noop = () => null;
|
||||||
|
function EditableTypography(props) {
|
||||||
function EditableTypography(props: EditableTypographyProps) {
|
|
||||||
const { children, onConfirm = noop, sx, ...typographyProps } = props;
|
const { children, onConfirm = noop, sx, ...typographyProps } = props;
|
||||||
const [editing, setEditing] = React.useState(false);
|
const [editing, setEditing] = React.useState(false);
|
||||||
|
|
||||||
const handleClick = React.useCallback(() => {
|
const handleClick = React.useCallback(() => {
|
||||||
setEditing((editing) => !editing);
|
setEditing((editing) => !editing);
|
||||||
}, []);
|
}, []);
|
||||||
|
const handleTextFieldClick = React.useCallback((event) => {
|
||||||
const handleTextFieldClick = React.useCallback(
|
event.stopPropagation();
|
||||||
(event: React.SyntheticEvent) => {
|
}, []);
|
||||||
event.stopPropagation();
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleTextFieldKeyDown = React.useCallback(
|
const handleTextFieldKeyDown = React.useCallback(
|
||||||
async (event: React.KeyboardEvent<HTMLInputElement>) => {
|
async (event) => {
|
||||||
const target = event.target as HTMLInputElement;
|
const target = event.target;
|
||||||
if (event.key === 'Enter') {
|
if (event.key === 'Enter') {
|
||||||
if (target.value !== children) {
|
if (target.value !== children) {
|
||||||
await onConfirm(target.value);
|
await onConfirm(target.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
setEditing(false);
|
setEditing(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[children]
|
[children],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleTextFieldBlur = React.useCallback(
|
const handleTextFieldBlur = React.useCallback(
|
||||||
async (event: React.FocusEvent<HTMLInputElement>) => {
|
async (event) => {
|
||||||
const value = event.target.value;
|
const value = event.target.value;
|
||||||
|
|
||||||
if (value !== children) {
|
if (value !== children) {
|
||||||
await onConfirm(value);
|
await onConfirm(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
setEditing(false);
|
setEditing(false);
|
||||||
},
|
},
|
||||||
[onConfirm, children]
|
[onConfirm, children],
|
||||||
);
|
);
|
||||||
|
|
||||||
let component = <Typography {...typographyProps}>{children}</Typography>;
|
let component = <Typography {...typographyProps}>{children}</Typography>;
|
||||||
|
|
||||||
if (editing) {
|
if (editing) {
|
||||||
component = (
|
component = (
|
||||||
<TextField
|
<TextField
|
||||||
@@ -68,7 +47,6 @@ function EditableTypography(props: EditableTypographyProps) {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={sx} onClick={handleClick} editing={editing}>
|
<Box sx={sx} onClick={handleClick} editing={editing}>
|
||||||
<EditIcon sx={{ mr: 1 }} />
|
<EditIcon sx={{ mr: 1 }} />
|
||||||
@@ -77,5 +55,4 @@ function EditableTypography(props: EditableTypographyProps) {
|
|||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default EditableTypography;
|
export default EditableTypography;
|
@@ -2,23 +2,17 @@ import { styled } from '@mui/material/styles';
|
|||||||
import MuiBox from '@mui/material/Box';
|
import MuiBox from '@mui/material/Box';
|
||||||
import MuiTextField from '@mui/material/TextField';
|
import MuiTextField from '@mui/material/TextField';
|
||||||
import { inputClasses } from '@mui/material/Input';
|
import { inputClasses } from '@mui/material/Input';
|
||||||
|
const boxShouldForwardProp = (prop) => !['editing'].includes(prop);
|
||||||
type BoxProps = {
|
|
||||||
editing?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const boxShouldForwardProp = (prop: string) => !['editing'].includes(prop);
|
|
||||||
export const Box = styled(MuiBox, {
|
export const Box = styled(MuiBox, {
|
||||||
shouldForwardProp: boxShouldForwardProp,
|
shouldForwardProp: boxShouldForwardProp,
|
||||||
})<BoxProps>`
|
})`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
width: 300px;
|
width: 300px;
|
||||||
height: 33px;
|
height: 33px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
${({ editing }) => editing && `border-bottom: 1px dashed #000;`}
|
${({ editing }) => editing && 'border-bottom: 1px dashed #000;'}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const TextField = styled(MuiTextField)({
|
export const TextField = styled(MuiTextField)({
|
||||||
width: '100%',
|
width: '100%',
|
||||||
[`.${inputClasses.root}:before, .${inputClasses.root}:after, .${inputClasses.root}:hover`]:
|
[`.${inputClasses.root}:before, .${inputClasses.root}:after, .${inputClasses.root}:hover`]:
|
@@ -3,33 +3,24 @@ import { useMutation } from '@apollo/client';
|
|||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import IconButton from '@mui/material/IconButton';
|
import IconButton from '@mui/material/IconButton';
|
||||||
import AddIcon from '@mui/icons-material/Add';
|
import AddIcon from '@mui/icons-material/Add';
|
||||||
import type { IFlow, IStep } from 'types';
|
|
||||||
|
|
||||||
import { GET_FLOW } from 'graphql/queries/get-flow';
|
import { GET_FLOW } from 'graphql/queries/get-flow';
|
||||||
import { CREATE_STEP } from 'graphql/mutations/create-step';
|
import { CREATE_STEP } from 'graphql/mutations/create-step';
|
||||||
import { UPDATE_STEP } from 'graphql/mutations/update-step';
|
import { UPDATE_STEP } from 'graphql/mutations/update-step';
|
||||||
import FlowStep from 'components/FlowStep';
|
import FlowStep from 'components/FlowStep';
|
||||||
|
function updateHandlerFactory(flowId, previousStepId) {
|
||||||
type EditorProps = {
|
return function createStepUpdateHandler(cache, mutationResult) {
|
||||||
flow: IFlow;
|
|
||||||
};
|
|
||||||
|
|
||||||
function updateHandlerFactory(flowId: string, previousStepId: string) {
|
|
||||||
return function createStepUpdateHandler(cache: any, mutationResult: any) {
|
|
||||||
const { data } = mutationResult;
|
const { data } = mutationResult;
|
||||||
const { createStep: createdStep } = data;
|
const { createStep: createdStep } = data;
|
||||||
const { getFlow: flow } = cache.readQuery({
|
const { getFlow: flow } = cache.readQuery({
|
||||||
query: GET_FLOW,
|
query: GET_FLOW,
|
||||||
variables: { id: flowId },
|
variables: { id: flowId },
|
||||||
});
|
});
|
||||||
const steps = flow.steps.reduce((steps: any[], currentStep: any) => {
|
const steps = flow.steps.reduce((steps, currentStep) => {
|
||||||
if (currentStep.id === previousStepId) {
|
if (currentStep.id === previousStepId) {
|
||||||
return [...steps, currentStep, createdStep];
|
return [...steps, currentStep, createdStep];
|
||||||
}
|
}
|
||||||
|
|
||||||
return [...steps, currentStep];
|
return [...steps, currentStep];
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
cache.writeQuery({
|
cache.writeQuery({
|
||||||
query: GET_FLOW,
|
query: GET_FLOW,
|
||||||
variables: { id: flowId },
|
variables: { id: flowId },
|
||||||
@@ -37,26 +28,20 @@ function updateHandlerFactory(flowId: string, previousStepId: string) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
export default function Editor(props) {
|
||||||
export default function Editor(props: EditorProps): React.ReactElement {
|
|
||||||
const [updateStep] = useMutation(UPDATE_STEP);
|
const [updateStep] = useMutation(UPDATE_STEP);
|
||||||
const [createStep, { loading: creationInProgress }] = useMutation(
|
const [createStep, { loading: creationInProgress }] = useMutation(
|
||||||
CREATE_STEP,
|
CREATE_STEP,
|
||||||
{
|
{
|
||||||
refetchQueries: ['GetFlow'],
|
refetchQueries: ['GetFlow'],
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const { flow } = props;
|
const { flow } = props;
|
||||||
const [triggerStep] = flow.steps;
|
const [triggerStep] = flow.steps;
|
||||||
|
const [currentStepId, setCurrentStepId] = React.useState(triggerStep.id);
|
||||||
const [currentStepId, setCurrentStepId] = React.useState<string | null>(
|
|
||||||
triggerStep.id
|
|
||||||
);
|
|
||||||
|
|
||||||
const onStepChange = React.useCallback(
|
const onStepChange = React.useCallback(
|
||||||
(step: any) => {
|
(step) => {
|
||||||
const mutationInput: Record<string, unknown> = {
|
const mutationInput = {
|
||||||
id: step.id,
|
id: step.id,
|
||||||
key: step.key,
|
key: step.key,
|
||||||
parameters: step.parameters,
|
parameters: step.parameters,
|
||||||
@@ -67,16 +52,13 @@ export default function Editor(props: EditorProps): React.ReactElement {
|
|||||||
id: flow.id,
|
id: flow.id,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
if (step.appKey) {
|
if (step.appKey) {
|
||||||
mutationInput.appKey = step.appKey;
|
mutationInput.appKey = step.appKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateStep({ variables: { input: mutationInput } });
|
updateStep({ variables: { input: mutationInput } });
|
||||||
},
|
},
|
||||||
[updateStep, flow.id]
|
[updateStep, flow.id],
|
||||||
);
|
);
|
||||||
|
|
||||||
const addStep = React.useCallback(
|
const addStep = React.useCallback(
|
||||||
async (previousStepId) => {
|
async (previousStepId) => {
|
||||||
const mutationInput = {
|
const mutationInput = {
|
||||||
@@ -87,24 +69,20 @@ export default function Editor(props: EditorProps): React.ReactElement {
|
|||||||
id: flow.id,
|
id: flow.id,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const createdStep = await createStep({
|
const createdStep = await createStep({
|
||||||
variables: { input: mutationInput },
|
variables: { input: mutationInput },
|
||||||
update: updateHandlerFactory(flow.id, previousStepId),
|
update: updateHandlerFactory(flow.id, previousStepId),
|
||||||
});
|
});
|
||||||
const createdStepId = createdStep.data.createStep.id;
|
const createdStepId = createdStep.data.createStep.id;
|
||||||
|
|
||||||
setCurrentStepId(createdStepId);
|
setCurrentStepId(createdStepId);
|
||||||
},
|
},
|
||||||
[createStep, flow.id]
|
[createStep, flow.id],
|
||||||
);
|
);
|
||||||
|
const openNextStep = React.useCallback((nextStep) => {
|
||||||
const openNextStep = React.useCallback((nextStep: IStep) => {
|
|
||||||
return () => {
|
return () => {
|
||||||
setCurrentStepId(nextStep?.id);
|
setCurrentStepId(nextStep?.id);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
display="flex"
|
display="flex"
|
@@ -8,7 +8,6 @@ import Tooltip from '@mui/material/Tooltip';
|
|||||||
import IconButton from '@mui/material/IconButton';
|
import IconButton from '@mui/material/IconButton';
|
||||||
import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew';
|
import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew';
|
||||||
import Snackbar from '@mui/material/Snackbar';
|
import Snackbar from '@mui/material/Snackbar';
|
||||||
|
|
||||||
import { EditorProvider } from 'contexts/Editor';
|
import { EditorProvider } from 'contexts/Editor';
|
||||||
import EditableTypography from 'components/EditableTypography';
|
import EditableTypography from 'components/EditableTypography';
|
||||||
import Container from 'components/Container';
|
import Container from 'components/Container';
|
||||||
@@ -17,19 +16,16 @@ import useFormatMessage from 'hooks/useFormatMessage';
|
|||||||
import { UPDATE_FLOW_STATUS } from 'graphql/mutations/update-flow-status';
|
import { UPDATE_FLOW_STATUS } from 'graphql/mutations/update-flow-status';
|
||||||
import { UPDATE_FLOW } from 'graphql/mutations/update-flow';
|
import { UPDATE_FLOW } from 'graphql/mutations/update-flow';
|
||||||
import { GET_FLOW } from 'graphql/queries/get-flow';
|
import { GET_FLOW } from 'graphql/queries/get-flow';
|
||||||
import type { IFlow } from 'types';
|
|
||||||
import * as URLS from 'config/urls';
|
import * as URLS from 'config/urls';
|
||||||
|
export default function EditorLayout() {
|
||||||
export default function EditorLayout(): React.ReactElement {
|
|
||||||
const { flowId } = useParams();
|
const { flowId } = useParams();
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
const [updateFlow] = useMutation(UPDATE_FLOW);
|
const [updateFlow] = useMutation(UPDATE_FLOW);
|
||||||
const [updateFlowStatus] = useMutation(UPDATE_FLOW_STATUS);
|
const [updateFlowStatus] = useMutation(UPDATE_FLOW_STATUS);
|
||||||
const { data, loading } = useQuery(GET_FLOW, { variables: { id: flowId } });
|
const { data, loading } = useQuery(GET_FLOW, { variables: { id: flowId } });
|
||||||
const flow: IFlow = data?.getFlow;
|
const flow = data?.getFlow;
|
||||||
|
|
||||||
const onFlowNameUpdate = React.useCallback(
|
const onFlowNameUpdate = React.useCallback(
|
||||||
async (name: string) => {
|
async (name) => {
|
||||||
await updateFlow({
|
await updateFlow({
|
||||||
variables: {
|
variables: {
|
||||||
input: {
|
input: {
|
||||||
@@ -46,11 +42,10 @@ export default function EditorLayout(): React.ReactElement {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[flow?.id]
|
[flow?.id],
|
||||||
);
|
);
|
||||||
|
|
||||||
const onFlowStatusUpdate = React.useCallback(
|
const onFlowStatusUpdate = React.useCallback(
|
||||||
async (active: boolean) => {
|
async (active) => {
|
||||||
await updateFlowStatus({
|
await updateFlowStatus({
|
||||||
variables: {
|
variables: {
|
||||||
input: {
|
input: {
|
||||||
@@ -67,9 +62,8 @@ export default function EditorLayout(): React.ReactElement {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[flow?.id]
|
[flow?.id],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Stack direction="column" height="100%">
|
<Stack direction="column" height="100%">
|
@@ -4,31 +4,21 @@ import Stack from '@mui/material/Stack';
|
|||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import Tooltip from '@mui/material/Tooltip';
|
import Tooltip from '@mui/material/Tooltip';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import type { IExecution } from 'types';
|
|
||||||
|
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
|
function ExecutionName(props) {
|
||||||
type ExecutionHeaderProps = {
|
|
||||||
execution: IExecution;
|
|
||||||
};
|
|
||||||
|
|
||||||
function ExecutionName(props: Pick<IExecution['flow'], 'name'>) {
|
|
||||||
return (
|
return (
|
||||||
<Typography variant="h3" gutterBottom>
|
<Typography variant="h3" gutterBottom>
|
||||||
{props.name}
|
{props.name}
|
||||||
</Typography>
|
</Typography>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
function ExecutionId(props) {
|
||||||
function ExecutionId(props: Pick<IExecution, 'id'>) {
|
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
|
|
||||||
const id = (
|
const id = (
|
||||||
<Typography variant="body1" component="span">
|
<Typography variant="body1" component="span">
|
||||||
{props.id}
|
{props.id}
|
||||||
</Typography>
|
</Typography>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: 'flex' }}>
|
<Box sx={{ display: 'flex' }}>
|
||||||
<Typography variant="body2">
|
<Typography variant="body2">
|
||||||
@@ -37,13 +27,9 @@ function ExecutionId(props: Pick<IExecution, 'id'>) {
|
|||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
function ExecutionDate(props) {
|
||||||
function ExecutionDate(props: Pick<IExecution, 'createdAt'>) {
|
const createdAt = DateTime.fromMillis(parseInt(props.createdAt, 10));
|
||||||
const createdAt = DateTime.fromMillis(
|
|
||||||
parseInt(props.createdAt as string, 10)
|
|
||||||
);
|
|
||||||
const relativeCreatedAt = createdAt.toRelative();
|
const relativeCreatedAt = createdAt.toRelative();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title={createdAt.toLocaleString(DateTime.DATETIME_FULL_WITH_SECONDS)}
|
title={createdAt.toLocaleString(DateTime.DATETIME_FULL_WITH_SECONDS)}
|
||||||
@@ -54,14 +40,9 @@ function ExecutionDate(props: Pick<IExecution, 'createdAt'>) {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
export default function ExecutionHeader(props) {
|
||||||
export default function ExecutionHeader(
|
|
||||||
props: ExecutionHeaderProps
|
|
||||||
): React.ReactElement {
|
|
||||||
const { execution } = props;
|
const { execution } = props;
|
||||||
|
|
||||||
if (!execution) return <React.Fragment />;
|
if (!execution) return <React.Fragment />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack direction="column">
|
<Stack direction="column">
|
||||||
<Stack
|
<Stack
|
@@ -5,29 +5,16 @@ import CardActionArea from '@mui/material/CardActionArea';
|
|||||||
import Chip from '@mui/material/Chip';
|
import Chip from '@mui/material/Chip';
|
||||||
import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos';
|
import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import type { IExecution } from 'types';
|
|
||||||
|
|
||||||
import * as URLS from 'config/urls';
|
import * as URLS from 'config/urls';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
import FlowAppIcons from 'components/FlowAppIcons';
|
import FlowAppIcons from 'components/FlowAppIcons';
|
||||||
import { Apps, CardContent, ArrowContainer, Title, Typography } from './style';
|
import { Apps, CardContent, ArrowContainer, Title, Typography } from './style';
|
||||||
|
export default function ExecutionRow(props) {
|
||||||
type ExecutionRowProps = {
|
|
||||||
execution: IExecution;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function ExecutionRow(
|
|
||||||
props: ExecutionRowProps
|
|
||||||
): React.ReactElement {
|
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
const { execution } = props;
|
const { execution } = props;
|
||||||
const { flow } = execution;
|
const { flow } = execution;
|
||||||
|
const createdAt = DateTime.fromMillis(parseInt(execution.createdAt, 10));
|
||||||
const createdAt = DateTime.fromMillis(
|
|
||||||
parseInt(execution.createdAt as string, 10)
|
|
||||||
);
|
|
||||||
const relativeCreatedAt = createdAt.toRelative();
|
const relativeCreatedAt = createdAt.toRelative();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link to={URLS.EXECUTION(execution.id)} data-test="execution-row">
|
<Link to={URLS.EXECUTION(execution.id)} data-test="execution-row">
|
||||||
<Card sx={{ mb: 1 }}>
|
<Card sx={{ mb: 1 }}>
|
||||||
@@ -65,7 +52,7 @@ export default function ExecutionRow(
|
|||||||
label={formatMessage(
|
label={formatMessage(
|
||||||
execution.status === 'success'
|
execution.status === 'success'
|
||||||
? 'execution.statusSuccess'
|
? 'execution.statusSuccess'
|
||||||
: 'execution.statusFailure'
|
: 'execution.statusFailure',
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
@@ -3,7 +3,6 @@ import MuiCardContent from '@mui/material/CardContent';
|
|||||||
import MuiBox from '@mui/material/Box';
|
import MuiBox from '@mui/material/Box';
|
||||||
import MuiStack from '@mui/material/Stack';
|
import MuiStack from '@mui/material/Stack';
|
||||||
import MuiTypography from '@mui/material/Typography';
|
import MuiTypography from '@mui/material/Typography';
|
||||||
|
|
||||||
export const CardContent = styled(MuiCardContent)(({ theme }) => ({
|
export const CardContent = styled(MuiCardContent)(({ theme }) => ({
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
gridTemplateRows: 'auto',
|
gridTemplateRows: 'auto',
|
||||||
@@ -22,14 +21,12 @@ export const CardContent = styled(MuiCardContent)(({ theme }) => ({
|
|||||||
gridTemplateRows: 'auto auto',
|
gridTemplateRows: 'auto auto',
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const Apps = styled(MuiStack)(() => ({
|
export const Apps = styled(MuiStack)(() => ({
|
||||||
gridArea: 'apps',
|
gridArea: 'apps',
|
||||||
}));
|
}));
|
||||||
export const Title = styled(MuiStack)(() => ({
|
export const Title = styled(MuiStack)(() => ({
|
||||||
gridArea: 'title',
|
gridArea: 'title',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const ArrowContainer = styled(MuiBox)(() => ({
|
export const ArrowContainer = styled(MuiBox)(() => ({
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -42,7 +39,6 @@ export const Typography = styled(MuiTypography)(() => ({
|
|||||||
width: '100%',
|
width: '100%',
|
||||||
maxWidth: '85%',
|
maxWidth: '85%',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const DesktopOnlyBreakline = styled('br')(({ theme }) => ({
|
export const DesktopOnlyBreakline = styled('br')(({ theme }) => ({
|
||||||
[theme.breakpoints.down('sm')]: {
|
[theme.breakpoints.down('sm')]: {
|
||||||
display: 'none',
|
display: 'none',
|
@@ -8,14 +8,11 @@ import Tab from '@mui/material/Tab';
|
|||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import Tooltip from '@mui/material/Tooltip';
|
import Tooltip from '@mui/material/Tooltip';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import type { IApp, IExecutionStep, IStep } from 'types';
|
|
||||||
|
|
||||||
import TabPanel from 'components/TabPanel';
|
import TabPanel from 'components/TabPanel';
|
||||||
import SearchableJSONViewer from 'components/SearchableJSONViewer';
|
import SearchableJSONViewer from 'components/SearchableJSONViewer';
|
||||||
import AppIcon from 'components/AppIcon';
|
import AppIcon from 'components/AppIcon';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
import useApps from 'hooks/useApps';
|
import useApps from 'hooks/useApps';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AppIconWrapper,
|
AppIconWrapper,
|
||||||
AppIconStatusIconWrapper,
|
AppIconStatusIconWrapper,
|
||||||
@@ -24,23 +21,13 @@ import {
|
|||||||
Metadata,
|
Metadata,
|
||||||
Wrapper,
|
Wrapper,
|
||||||
} from './style';
|
} from './style';
|
||||||
|
function ExecutionStepId(props) {
|
||||||
type ExecutionStepProps = {
|
|
||||||
collapsed?: boolean;
|
|
||||||
step: IStep;
|
|
||||||
index?: number;
|
|
||||||
executionStep: IExecutionStep;
|
|
||||||
};
|
|
||||||
|
|
||||||
function ExecutionStepId(props: Pick<IExecutionStep, 'id'>) {
|
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
|
|
||||||
const id = (
|
const id = (
|
||||||
<Typography variant="caption" component="span">
|
<Typography variant="caption" component="span">
|
||||||
{props.id}
|
{props.id}
|
||||||
</Typography>
|
</Typography>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: 'flex' }} gridArea="id">
|
<Box sx={{ display: 'flex' }} gridArea="id">
|
||||||
<Typography variant="caption" fontWeight="bold">
|
<Typography variant="caption" fontWeight="bold">
|
||||||
@@ -49,12 +36,10 @@ function ExecutionStepId(props: Pick<IExecutionStep, 'id'>) {
|
|||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
function ExecutionStepDate(props) {
|
||||||
function ExecutionStepDate(props: Pick<IExecutionStep, 'createdAt'>) {
|
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
const createdAt = DateTime.fromMillis(parseInt(props.createdAt, 10));
|
const createdAt = DateTime.fromMillis(parseInt(props.createdAt, 10));
|
||||||
const relativeCreatedAt = createdAt.toRelative();
|
const relativeCreatedAt = createdAt.toRelative();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title={createdAt.toLocaleString(DateTime.DATETIME_FULL_WITH_SECONDS)}
|
title={createdAt.toLocaleString(DateTime.DATETIME_FULL_WITH_SECONDS)}
|
||||||
@@ -67,16 +52,12 @@ function ExecutionStepDate(props: Pick<IExecutionStep, 'createdAt'>) {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const validIcon = <CheckCircleIcon color="success" />;
|
const validIcon = <CheckCircleIcon color="success" />;
|
||||||
const errorIcon = <ErrorIcon color="error" />;
|
const errorIcon = <ErrorIcon color="error" />;
|
||||||
|
export default function ExecutionStep(props) {
|
||||||
export default function ExecutionStep(
|
|
||||||
props: ExecutionStepProps
|
|
||||||
): React.ReactElement | null {
|
|
||||||
const { executionStep } = props;
|
const { executionStep } = props;
|
||||||
const [activeTabIndex, setActiveTabIndex] = React.useState(0);
|
const [activeTabIndex, setActiveTabIndex] = React.useState(0);
|
||||||
const step: IStep = executionStep.step;
|
const step = executionStep.step;
|
||||||
const isTrigger = step.type === 'trigger';
|
const isTrigger = step.type === 'trigger';
|
||||||
const isAction = step.type === 'action';
|
const isAction = step.type === 'action';
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
@@ -84,14 +65,11 @@ export default function ExecutionStep(
|
|||||||
onlyWithTriggers: isTrigger,
|
onlyWithTriggers: isTrigger,
|
||||||
onlyWithActions: isAction,
|
onlyWithActions: isAction,
|
||||||
});
|
});
|
||||||
const app = apps?.find((currentApp: IApp) => currentApp.key === step.appKey);
|
const app = apps?.find((currentApp) => currentApp.key === step.appKey);
|
||||||
|
|
||||||
if (!apps) return null;
|
if (!apps) return null;
|
||||||
|
|
||||||
const validationStatusIcon =
|
const validationStatusIcon =
|
||||||
executionStep.status === 'success' ? validIcon : errorIcon;
|
executionStep.status === 'success' ? validIcon : errorIcon;
|
||||||
const hasError = !!executionStep.errorDetails;
|
const hasError = !!executionStep.errorDetails;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper elevation={1} data-test="execution-step">
|
<Wrapper elevation={1} data-test="execution-step">
|
||||||
<Header>
|
<Header>
|
@@ -1,13 +1,10 @@
|
|||||||
import { styled, alpha } from '@mui/material/styles';
|
import { styled, alpha } from '@mui/material/styles';
|
||||||
import Card from '@mui/material/Card';
|
import Card from '@mui/material/Card';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
|
|
||||||
export const AppIconWrapper = styled('div')`
|
export const AppIconWrapper = styled('div')`
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
||||||
export const AppIconStatusIconWrapper = styled('span')`
|
export const AppIconStatusIconWrapper = styled('span')`
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -23,43 +20,35 @@ export const AppIconStatusIconWrapper = styled('span')`
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Wrapper = styled(Card)`
|
export const Wrapper = styled(Card)`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow: unset;
|
overflow: unset;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
type HeaderProps = {
|
|
||||||
collapsed?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Header = styled('div', {
|
export const Header = styled('div', {
|
||||||
shouldForwardProp: (prop) => prop !== 'collapsed',
|
shouldForwardProp: (prop) => prop !== 'collapsed',
|
||||||
}) <HeaderProps>`
|
})`
|
||||||
padding: ${({ theme }) => theme.spacing(2)};
|
padding: ${({ theme }) => theme.spacing(2)};
|
||||||
cursor: ${({ collapsed }) => (collapsed ? 'pointer' : 'unset')};
|
cursor: ${({ collapsed }) => (collapsed ? 'pointer' : 'unset')};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Content = styled('div')`
|
export const Content = styled('div')`
|
||||||
border: 1px solid ${({ theme }) => alpha(theme.palette.divider, 0.8)};
|
border: 1px solid ${({ theme }) => alpha(theme.palette.divider, 0.8)};
|
||||||
border-left: none;
|
border-left: none;
|
||||||
border-right: none;
|
border-right: none;
|
||||||
padding: ${({ theme }) => theme.spacing(2, 0)};
|
padding: ${({ theme }) => theme.spacing(2, 0)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Metadata = styled(Box)`
|
export const Metadata = styled(Box)`
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr auto;
|
grid-template-columns: 1fr auto;
|
||||||
grid-template-rows: auto auto;
|
grid-template-rows: auto auto;
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
"step id"
|
'step id'
|
||||||
"step date";
|
'step date';
|
||||||
|
|
||||||
${({ theme }) => theme.breakpoints.down('sm')} {
|
${({ theme }) => theme.breakpoints.down('sm')} {
|
||||||
grid-template-rows: auto auto auto;
|
grid-template-rows: auto auto auto;
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
"id"
|
'id'
|
||||||
"step"
|
'step'
|
||||||
"date";
|
'date';
|
||||||
}
|
}
|
||||||
` as typeof Box;
|
`;
|
@@ -1,20 +1,12 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import type { IStep } from 'types';
|
|
||||||
|
|
||||||
import AppIcon from 'components/AppIcon';
|
import AppIcon from 'components/AppIcon';
|
||||||
import IntermediateStepCount from 'components/IntermediateStepCount';
|
import IntermediateStepCount from 'components/IntermediateStepCount';
|
||||||
|
export default function FlowAppIcons(props) {
|
||||||
type FlowAppIconsProps = {
|
|
||||||
steps: Partial<IStep>[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function FlowAppIcons(props: FlowAppIconsProps) {
|
|
||||||
const { steps } = props;
|
const { steps } = props;
|
||||||
const stepsCount = steps.length;
|
const stepsCount = steps.length;
|
||||||
const firstStep = steps[0];
|
const firstStep = steps[0];
|
||||||
const lastStep = steps.length > 1 && steps[stepsCount - 1];
|
const lastStep = steps.length > 1 && steps[stepsCount - 1];
|
||||||
const intermeaditeStepCount = stepsCount - 2;
|
const intermeaditeStepCount = stepsCount - 2;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AppIcon
|
<AppIcon
|
@@ -1,26 +1,15 @@
|
|||||||
import { useMutation } from '@apollo/client';
|
import { useMutation } from '@apollo/client';
|
||||||
import Menu from '@mui/material/Menu';
|
import Menu from '@mui/material/Menu';
|
||||||
import MenuItem from '@mui/material/MenuItem';
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
import type { PopoverProps } from '@mui/material/Popover';
|
|
||||||
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
|
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import Can from 'components/Can';
|
import Can from 'components/Can';
|
||||||
import * as URLS from 'config/urls';
|
import * as URLS from 'config/urls';
|
||||||
import { DELETE_FLOW } from 'graphql/mutations/delete-flow';
|
import { DELETE_FLOW } from 'graphql/mutations/delete-flow';
|
||||||
import { DUPLICATE_FLOW } from 'graphql/mutations/duplicate-flow';
|
import { DUPLICATE_FLOW } from 'graphql/mutations/duplicate-flow';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
|
export default function ContextMenu(props) {
|
||||||
type ContextMenuProps = {
|
|
||||||
flowId: string;
|
|
||||||
onClose: () => void;
|
|
||||||
anchorEl: PopoverProps['anchorEl'];
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function ContextMenu(
|
|
||||||
props: ContextMenuProps
|
|
||||||
): React.ReactElement {
|
|
||||||
const { flowId, onClose, anchorEl } = props;
|
const { flowId, onClose, anchorEl } = props;
|
||||||
const enqueueSnackbar = useEnqueueSnackbar();
|
const enqueueSnackbar = useEnqueueSnackbar();
|
||||||
const [deleteFlow] = useMutation(DELETE_FLOW);
|
const [deleteFlow] = useMutation(DELETE_FLOW);
|
||||||
@@ -28,22 +17,18 @@ export default function ContextMenu(
|
|||||||
refetchQueries: ['GetFlows'],
|
refetchQueries: ['GetFlows'],
|
||||||
});
|
});
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
|
|
||||||
const onFlowDuplicate = React.useCallback(async () => {
|
const onFlowDuplicate = React.useCallback(async () => {
|
||||||
await duplicateFlow({
|
await duplicateFlow({
|
||||||
variables: { input: { id: flowId } },
|
variables: { input: { id: flowId } },
|
||||||
});
|
});
|
||||||
|
|
||||||
enqueueSnackbar(formatMessage('flow.successfullyDuplicated'), {
|
enqueueSnackbar(formatMessage('flow.successfullyDuplicated'), {
|
||||||
variant: 'success',
|
variant: 'success',
|
||||||
SnackbarProps: {
|
SnackbarProps: {
|
||||||
'data-test': 'snackbar-duplicate-flow-success'
|
'data-test': 'snackbar-duplicate-flow-success',
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
onClose();
|
onClose();
|
||||||
}, [flowId, onClose, duplicateFlow]);
|
}, [flowId, onClose, duplicateFlow]);
|
||||||
|
|
||||||
const onFlowDelete = React.useCallback(async () => {
|
const onFlowDelete = React.useCallback(async () => {
|
||||||
await deleteFlow({
|
await deleteFlow({
|
||||||
variables: { input: { id: flowId } },
|
variables: { input: { id: flowId } },
|
||||||
@@ -52,20 +37,16 @@ export default function ContextMenu(
|
|||||||
__typename: 'Flow',
|
__typename: 'Flow',
|
||||||
id: flowId,
|
id: flowId,
|
||||||
});
|
});
|
||||||
|
|
||||||
cache.evict({
|
cache.evict({
|
||||||
id: flowCacheId,
|
id: flowCacheId,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
enqueueSnackbar(formatMessage('flow.successfullyDeleted'), {
|
enqueueSnackbar(formatMessage('flow.successfullyDeleted'), {
|
||||||
variant: 'success',
|
variant: 'success',
|
||||||
});
|
});
|
||||||
|
|
||||||
onClose();
|
onClose();
|
||||||
}, [flowId, onClose, deleteFlow]);
|
}, [flowId, onClose, deleteFlow]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu
|
<Menu
|
||||||
open={true}
|
open={true}
|
@@ -6,71 +6,46 @@ import CardActionArea from '@mui/material/CardActionArea';
|
|||||||
import Chip from '@mui/material/Chip';
|
import Chip from '@mui/material/Chip';
|
||||||
import MoreHorizIcon from '@mui/icons-material/MoreHoriz';
|
import MoreHorizIcon from '@mui/icons-material/MoreHoriz';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
|
|
||||||
import type { IFlow } from 'types';
|
|
||||||
import FlowAppIcons from 'components/FlowAppIcons';
|
import FlowAppIcons from 'components/FlowAppIcons';
|
||||||
import FlowContextMenu from 'components/FlowContextMenu';
|
import FlowContextMenu from 'components/FlowContextMenu';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
import * as URLS from 'config/urls';
|
import * as URLS from 'config/urls';
|
||||||
import { Apps, CardContent, ContextMenu, Title, Typography } from './style';
|
import { Apps, CardContent, ContextMenu, Title, Typography } from './style';
|
||||||
|
function getFlowStatusTranslationKey(status) {
|
||||||
type FlowRowProps = {
|
|
||||||
flow: IFlow;
|
|
||||||
};
|
|
||||||
|
|
||||||
function getFlowStatusTranslationKey(status: IFlow['status']): string {
|
|
||||||
if (status === 'published') {
|
if (status === 'published') {
|
||||||
return 'flow.published';
|
return 'flow.published';
|
||||||
} else if (status === 'paused') {
|
} else if (status === 'paused') {
|
||||||
return 'flow.paused';
|
return 'flow.paused';
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'flow.draft';
|
return 'flow.draft';
|
||||||
}
|
}
|
||||||
|
function getFlowStatusColor(status) {
|
||||||
function getFlowStatusColor(
|
|
||||||
status: IFlow['status']
|
|
||||||
):
|
|
||||||
| 'default'
|
|
||||||
| 'primary'
|
|
||||||
| 'secondary'
|
|
||||||
| 'error'
|
|
||||||
| 'info'
|
|
||||||
| 'success'
|
|
||||||
| 'warning' {
|
|
||||||
if (status === 'published') {
|
if (status === 'published') {
|
||||||
return 'success';
|
return 'success';
|
||||||
} else if (status === 'paused') {
|
} else if (status === 'paused') {
|
||||||
return 'error';
|
return 'error';
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'info';
|
return 'info';
|
||||||
}
|
}
|
||||||
|
export default function FlowRow(props) {
|
||||||
export default function FlowRow(props: FlowRowProps): React.ReactElement {
|
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
const contextButtonRef = React.useRef<HTMLButtonElement | null>(null);
|
const contextButtonRef = React.useRef(null);
|
||||||
const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(
|
const [anchorEl, setAnchorEl] = React.useState(null);
|
||||||
null
|
|
||||||
);
|
|
||||||
const { flow } = props;
|
const { flow } = props;
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
setAnchorEl(null);
|
setAnchorEl(null);
|
||||||
};
|
};
|
||||||
const onContextMenuClick = (event: React.MouseEvent) => {
|
const onContextMenuClick = (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
event.nativeEvent.stopImmediatePropagation();
|
event.nativeEvent.stopImmediatePropagation();
|
||||||
setAnchorEl(contextButtonRef.current);
|
setAnchorEl(contextButtonRef.current);
|
||||||
};
|
};
|
||||||
|
const createdAt = DateTime.fromMillis(parseInt(flow.createdAt, 10));
|
||||||
const createdAt = DateTime.fromMillis(parseInt(flow.createdAt as string, 10));
|
const updatedAt = DateTime.fromMillis(parseInt(flow.updatedAt, 10));
|
||||||
const updatedAt = DateTime.fromMillis(parseInt(flow.updatedAt as string, 10));
|
|
||||||
const isUpdated = updatedAt > createdAt;
|
const isUpdated = updatedAt > createdAt;
|
||||||
const relativeCreatedAt = createdAt.toRelative();
|
const relativeCreatedAt = createdAt.toRelative();
|
||||||
const relativeUpdatedAt = updatedAt.toRelative();
|
const relativeUpdatedAt = updatedAt.toRelative();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Card sx={{ mb: 1 }} data-test="flow-row">
|
<Card sx={{ mb: 1 }} data-test="flow-row">
|
@@ -3,7 +3,6 @@ import MuiStack from '@mui/material/Stack';
|
|||||||
import MuiBox from '@mui/material/Box';
|
import MuiBox from '@mui/material/Box';
|
||||||
import MuiCardContent from '@mui/material/CardContent';
|
import MuiCardContent from '@mui/material/CardContent';
|
||||||
import MuiTypography from '@mui/material/Typography';
|
import MuiTypography from '@mui/material/Typography';
|
||||||
|
|
||||||
export const CardContent = styled(MuiCardContent)(({ theme }) => ({
|
export const CardContent = styled(MuiCardContent)(({ theme }) => ({
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
gridTemplateRows: 'auto',
|
gridTemplateRows: 'auto',
|
||||||
@@ -22,14 +21,12 @@ export const CardContent = styled(MuiCardContent)(({ theme }) => ({
|
|||||||
gridTemplateRows: 'auto auto',
|
gridTemplateRows: 'auto auto',
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const Apps = styled(MuiStack)(() => ({
|
export const Apps = styled(MuiStack)(() => ({
|
||||||
gridArea: 'apps',
|
gridArea: 'apps',
|
||||||
}));
|
}));
|
||||||
export const Title = styled(MuiStack)(() => ({
|
export const Title = styled(MuiStack)(() => ({
|
||||||
gridArea: 'title',
|
gridArea: 'title',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const ContextMenu = styled(MuiBox)(({ theme }) => ({
|
export const ContextMenu = styled(MuiBox)(({ theme }) => ({
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -42,7 +39,6 @@ export const Typography = styled(MuiTypography)(() => ({
|
|||||||
width: '100%',
|
width: '100%',
|
||||||
maxWidth: '85%',
|
maxWidth: '85%',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const DesktopOnlyBreakline = styled('br')(({ theme }) => ({
|
export const DesktopOnlyBreakline = styled('br')(({ theme }) => ({
|
||||||
[theme.breakpoints.down('sm')]: {
|
[theme.breakpoints.down('sm')]: {
|
||||||
display: 'none',
|
display: 'none',
|
@@ -13,9 +13,6 @@ import CircularProgress from '@mui/material/CircularProgress';
|
|||||||
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
||||||
import { yupResolver } from '@hookform/resolvers/yup';
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
import * as yup from 'yup';
|
import * as yup from 'yup';
|
||||||
import type { BaseSchema } from 'yup';
|
|
||||||
import type { IApp, ITrigger, IAction, IStep, ISubstep } from 'types';
|
|
||||||
|
|
||||||
import { EditorContext } from 'contexts/Editor';
|
import { EditorContext } from 'contexts/Editor';
|
||||||
import { StepExecutionsProvider } from 'contexts/StepExecutions';
|
import { StepExecutionsProvider } from 'contexts/StepExecutions';
|
||||||
import TestSubstep from 'components/TestSubstep';
|
import TestSubstep from 'components/TestSubstep';
|
||||||
@@ -36,35 +33,19 @@ import {
|
|||||||
Wrapper,
|
Wrapper,
|
||||||
} from './style';
|
} from './style';
|
||||||
import isEmpty from 'helpers/isEmpty';
|
import isEmpty from 'helpers/isEmpty';
|
||||||
|
|
||||||
type FlowStepProps = {
|
|
||||||
collapsed?: boolean;
|
|
||||||
step: IStep;
|
|
||||||
index?: number;
|
|
||||||
onOpen?: () => void;
|
|
||||||
onClose?: () => void;
|
|
||||||
onChange: (step: IStep) => void;
|
|
||||||
onContinue?: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const validIcon = <CheckCircleIcon color="success" />;
|
const validIcon = <CheckCircleIcon color="success" />;
|
||||||
const errorIcon = <ErrorIcon color="error" />;
|
const errorIcon = <ErrorIcon color="error" />;
|
||||||
|
function generateValidationSchema(substeps) {
|
||||||
function generateValidationSchema(substeps: ISubstep[]) {
|
|
||||||
const fieldValidations = substeps?.reduce(
|
const fieldValidations = substeps?.reduce(
|
||||||
(allValidations, { arguments: args }) => {
|
(allValidations, { arguments: args }) => {
|
||||||
if (!args || !Array.isArray(args)) return allValidations;
|
if (!args || !Array.isArray(args)) return allValidations;
|
||||||
|
const substepArgumentValidations = {};
|
||||||
const substepArgumentValidations: Record<string, BaseSchema> = {};
|
|
||||||
|
|
||||||
for (const arg of args) {
|
for (const arg of args) {
|
||||||
const { key, required } = arg;
|
const { key, required } = arg;
|
||||||
|
|
||||||
// base validation for the field if not exists
|
// base validation for the field if not exists
|
||||||
if (!substepArgumentValidations[key]) {
|
if (!substepArgumentValidations[key]) {
|
||||||
substepArgumentValidations[key] = yup.mixed();
|
substepArgumentValidations[key] = yup.mixed();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
typeof substepArgumentValidations[key] === 'object' &&
|
typeof substepArgumentValidations[key] === 'object' &&
|
||||||
(arg.type === 'string' || arg.type === 'dropdown')
|
(arg.type === 'string' || arg.type === 'dropdown')
|
||||||
@@ -76,21 +57,19 @@ function generateValidationSchema(substeps: ISubstep[]) {
|
|||||||
.test(
|
.test(
|
||||||
'empty-check',
|
'empty-check',
|
||||||
`${key} must be not empty`,
|
`${key} must be not empty`,
|
||||||
(value: any) => !isEmpty(value)
|
(value) => !isEmpty(value),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the field depends on another field, add the dependsOn required validation
|
// if the field depends on another field, add the dependsOn required validation
|
||||||
if (Array.isArray(arg.dependsOn) && arg.dependsOn.length > 0) {
|
if (Array.isArray(arg.dependsOn) && arg.dependsOn.length > 0) {
|
||||||
for (const dependsOnKey of arg.dependsOn) {
|
for (const dependsOnKey of arg.dependsOn) {
|
||||||
const missingDependencyValueMessage = `We're having trouble loading '${key}' data as required field '${dependsOnKey}' is missing.`;
|
const missingDependencyValueMessage = `We're having trouble loading '${key}' data as required field '${dependsOnKey}' is missing.`;
|
||||||
|
|
||||||
// TODO: make `dependsOnKey` agnostic to the field. However, nested validation schema is not supported.
|
// TODO: make `dependsOnKey` agnostic to the field. However, nested validation schema is not supported.
|
||||||
// So the fields under the `parameters` key are subject to their siblings only and thus, `parameters.` is removed.
|
// So the fields under the `parameters` key are subject to their siblings only and thus, `parameters.` is removed.
|
||||||
substepArgumentValidations[key] = substepArgumentValidations[
|
substepArgumentValidations[key] = substepArgumentValidations[
|
||||||
key
|
key
|
||||||
].when(`${dependsOnKey.replace('parameters.', '')}`, {
|
].when(`${dependsOnKey.replace('parameters.', '')}`, {
|
||||||
is: (value: string) => Boolean(value) === false,
|
is: (value) => Boolean(value) === false,
|
||||||
then: (schema) =>
|
then: (schema) =>
|
||||||
schema
|
schema
|
||||||
.notOneOf([''], missingDependencyValueMessage)
|
.notOneOf([''], missingDependencyValueMessage)
|
||||||
@@ -100,37 +79,28 @@ function generateValidationSchema(substeps: ISubstep[]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...allValidations,
|
...allValidations,
|
||||||
...substepArgumentValidations,
|
...substepArgumentValidations,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
{}
|
{},
|
||||||
);
|
);
|
||||||
|
|
||||||
const validationSchema = yup.object({
|
const validationSchema = yup.object({
|
||||||
parameters: yup.object(fieldValidations),
|
parameters: yup.object(fieldValidations),
|
||||||
});
|
});
|
||||||
|
|
||||||
return yupResolver(validationSchema);
|
return yupResolver(validationSchema);
|
||||||
}
|
}
|
||||||
|
export default function FlowStep(props) {
|
||||||
export default function FlowStep(
|
|
||||||
props: FlowStepProps
|
|
||||||
): React.ReactElement | null {
|
|
||||||
const { collapsed, onChange, onContinue } = props;
|
const { collapsed, onChange, onContinue } = props;
|
||||||
const editorContext = React.useContext(EditorContext);
|
const editorContext = React.useContext(EditorContext);
|
||||||
const contextButtonRef = React.useRef<HTMLButtonElement | null>(null);
|
const contextButtonRef = React.useRef(null);
|
||||||
const step: IStep = props.step;
|
const step = props.step;
|
||||||
const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(
|
const [anchorEl, setAnchorEl] = React.useState(null);
|
||||||
null
|
|
||||||
);
|
|
||||||
const isTrigger = step.type === 'trigger';
|
const isTrigger = step.type === 'trigger';
|
||||||
const isAction = step.type === 'action';
|
const isAction = step.type === 'action';
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
const [currentSubstep, setCurrentSubstep] = React.useState<number | null>(0);
|
const [currentSubstep, setCurrentSubstep] = React.useState(0);
|
||||||
|
|
||||||
const { apps } = useApps({
|
const { apps } = useApps({
|
||||||
onlyWithTriggers: isTrigger,
|
onlyWithTriggers: isTrigger,
|
||||||
onlyWithActions: isAction,
|
onlyWithActions: isAction,
|
||||||
@@ -141,7 +111,6 @@ export default function FlowStep(
|
|||||||
] = useLazyQuery(GET_STEP_WITH_TEST_EXECUTIONS, {
|
] = useLazyQuery(GET_STEP_WITH_TEST_EXECUTIONS, {
|
||||||
fetchPolicy: 'network-only',
|
fetchPolicy: 'network-only',
|
||||||
});
|
});
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!stepWithTestExecutionsCalled && !collapsed && !isTrigger) {
|
if (!stepWithTestExecutionsCalled && !collapsed && !isTrigger) {
|
||||||
getStepWithTestExecutions({
|
getStepWithTestExecutions({
|
||||||
@@ -157,33 +126,25 @@ export default function FlowStep(
|
|||||||
step.id,
|
step.id,
|
||||||
isTrigger,
|
isTrigger,
|
||||||
]);
|
]);
|
||||||
|
const app = apps?.find((currentApp) => currentApp.key === step.appKey);
|
||||||
const app = apps?.find((currentApp: IApp) => currentApp.key === step.appKey);
|
const actionsOrTriggers = (isTrigger ? app?.triggers : app?.actions) || [];
|
||||||
|
|
||||||
const actionsOrTriggers: Array<ITrigger | IAction> =
|
|
||||||
(isTrigger ? app?.triggers : app?.actions) || [];
|
|
||||||
const actionOrTrigger = actionsOrTriggers?.find(
|
const actionOrTrigger = actionsOrTriggers?.find(
|
||||||
({ key }) => key === step.key
|
({ key }) => key === step.key,
|
||||||
);
|
);
|
||||||
const substeps = actionOrTrigger?.substeps || [];
|
const substeps = actionOrTrigger?.substeps || [];
|
||||||
|
const handleChange = React.useCallback(({ step }) => {
|
||||||
const handleChange = React.useCallback(({ step }: { step: IStep }) => {
|
|
||||||
onChange(step);
|
onChange(step);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const expandNextStep = React.useCallback(() => {
|
const expandNextStep = React.useCallback(() => {
|
||||||
setCurrentSubstep((currentSubstep) => (currentSubstep ?? 0) + 1);
|
setCurrentSubstep((currentSubstep) => (currentSubstep ?? 0) + 1);
|
||||||
}, []);
|
}, []);
|
||||||
|
const handleSubmit = (val) => {
|
||||||
const handleSubmit = (val: any) => {
|
handleChange({ step: val });
|
||||||
handleChange({ step: val as IStep });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const stepValidationSchema = React.useMemo(
|
const stepValidationSchema = React.useMemo(
|
||||||
() => generateValidationSchema(substeps),
|
() => generateValidationSchema(substeps),
|
||||||
[substeps]
|
[substeps],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!apps) {
|
if (!apps) {
|
||||||
return (
|
return (
|
||||||
<CircularProgress
|
<CircularProgress
|
||||||
@@ -192,26 +153,22 @@ export default function FlowStep(
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
const onContextMenuClose = (event) => {
|
||||||
const onContextMenuClose = (event: React.SyntheticEvent) => {
|
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
setAnchorEl(null);
|
setAnchorEl(null);
|
||||||
};
|
};
|
||||||
const onContextMenuClick = (event: React.SyntheticEvent) => {
|
const onContextMenuClick = (event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
setAnchorEl(contextButtonRef.current);
|
setAnchorEl(contextButtonRef.current);
|
||||||
};
|
};
|
||||||
const onOpen = () => collapsed && props.onOpen?.();
|
const onOpen = () => collapsed && props.onOpen?.();
|
||||||
const onClose = () => props.onClose?.();
|
const onClose = () => props.onClose?.();
|
||||||
|
const toggleSubstep = (substepIndex) =>
|
||||||
const toggleSubstep = (substepIndex: number) =>
|
|
||||||
setCurrentSubstep((value) =>
|
setCurrentSubstep((value) =>
|
||||||
value !== substepIndex ? substepIndex : null
|
value !== substepIndex ? substepIndex : null,
|
||||||
);
|
);
|
||||||
|
|
||||||
const validationStatusIcon =
|
const validationStatusIcon =
|
||||||
step.status === 'completed' ? validIcon : errorIcon;
|
step.status === 'completed' ? validIcon : errorIcon;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper
|
<Wrapper
|
||||||
elevation={collapsed ? 1 : 4}
|
elevation={collapsed ? 1 : 4}
|
||||||
@@ -259,9 +216,7 @@ export default function FlowStep(
|
|||||||
<Content>
|
<Content>
|
||||||
<List>
|
<List>
|
||||||
<StepExecutionsProvider
|
<StepExecutionsProvider
|
||||||
value={
|
value={stepWithTestExecutionsData?.getStepWithTestExecutions}
|
||||||
stepWithTestExecutionsData?.getStepWithTestExecutions as IStep[]
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<Form
|
<Form
|
||||||
defaultValues={step}
|
defaultValues={step}
|
||||||
@@ -284,7 +239,7 @@ export default function FlowStep(
|
|||||||
|
|
||||||
{actionOrTrigger &&
|
{actionOrTrigger &&
|
||||||
substeps?.length > 0 &&
|
substeps?.length > 0 &&
|
||||||
substeps.map((substep: ISubstep, index: number) => (
|
substeps.map((substep, index) => (
|
||||||
<React.Fragment key={`${substep?.name}-${index}`}>
|
<React.Fragment key={`${substep?.name}-${index}`}>
|
||||||
{substep.key === 'chooseConnection' && app && (
|
{substep.key === 'chooseConnection' && app && (
|
||||||
<ChooseConnectionSubstep
|
<ChooseConnectionSubstep
|
||||||
@@ -319,7 +274,7 @@ export default function FlowStep(
|
|||||||
|
|
||||||
{substep.key &&
|
{substep.key &&
|
||||||
['chooseConnection', 'testStep'].includes(
|
['chooseConnection', 'testStep'].includes(
|
||||||
substep.key
|
substep.key,
|
||||||
) === false && (
|
) === false && (
|
||||||
<FlowSubstep
|
<FlowSubstep
|
||||||
expanded={currentSubstep === index + 1}
|
expanded={currentSubstep === index + 1}
|
@@ -1,10 +1,8 @@
|
|||||||
import { styled, alpha } from '@mui/material/styles';
|
import { styled, alpha } from '@mui/material/styles';
|
||||||
import Card from '@mui/material/Card';
|
import Card from '@mui/material/Card';
|
||||||
|
|
||||||
export const AppIconWrapper = styled('div')`
|
export const AppIconWrapper = styled('div')`
|
||||||
position: relative;
|
position: relative;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const AppIconStatusIconWrapper = styled('span')`
|
export const AppIconStatusIconWrapper = styled('span')`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0;
|
right: 0;
|
||||||
@@ -19,23 +17,16 @@ export const AppIconStatusIconWrapper = styled('span')`
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Wrapper = styled(Card)`
|
export const Wrapper = styled(Card)`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow: unset;
|
overflow: unset;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
type HeaderProps = {
|
|
||||||
collapsed?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Header = styled('div', {
|
export const Header = styled('div', {
|
||||||
shouldForwardProp: (prop) => prop !== 'collapsed',
|
shouldForwardProp: (prop) => prop !== 'collapsed',
|
||||||
})<HeaderProps>`
|
})`
|
||||||
padding: ${({ theme }) => theme.spacing(2)};
|
padding: ${({ theme }) => theme.spacing(2)};
|
||||||
cursor: ${({ collapsed }) => (collapsed ? 'pointer' : 'unset')};
|
cursor: ${({ collapsed }) => (collapsed ? 'pointer' : 'unset')};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Content = styled('div')`
|
export const Content = styled('div')`
|
||||||
border: 1px solid ${({ theme }) => alpha(theme.palette.divider, 0.8)};
|
border: 1px solid ${({ theme }) => alpha(theme.palette.divider, 0.8)};
|
||||||
border-left: none;
|
border-left: none;
|
@@ -2,35 +2,21 @@ import * as React from 'react';
|
|||||||
import { useMutation } from '@apollo/client';
|
import { useMutation } from '@apollo/client';
|
||||||
import Menu from '@mui/material/Menu';
|
import Menu from '@mui/material/Menu';
|
||||||
import MenuItem from '@mui/material/MenuItem';
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
import type { PopoverProps } from '@mui/material/Popover';
|
|
||||||
|
|
||||||
import { DELETE_STEP } from 'graphql/mutations/delete-step';
|
import { DELETE_STEP } from 'graphql/mutations/delete-step';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
|
function FlowStepContextMenu(props) {
|
||||||
type FlowStepContextMenuProps = {
|
|
||||||
stepId: string;
|
|
||||||
onClose: PopoverProps['onClose'];
|
|
||||||
anchorEl: HTMLButtonElement;
|
|
||||||
deletable: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
function FlowStepContextMenu(
|
|
||||||
props: FlowStepContextMenuProps
|
|
||||||
): React.ReactElement {
|
|
||||||
const { stepId, onClose, anchorEl, deletable } = props;
|
const { stepId, onClose, anchorEl, deletable } = props;
|
||||||
const [deleteStep] = useMutation(DELETE_STEP, {
|
const [deleteStep] = useMutation(DELETE_STEP, {
|
||||||
refetchQueries: ['GetFlow', 'GetStepWithTestExecutions'],
|
refetchQueries: ['GetFlow', 'GetStepWithTestExecutions'],
|
||||||
});
|
});
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
|
|
||||||
const deleteActionHandler = React.useCallback(
|
const deleteActionHandler = React.useCallback(
|
||||||
async (event: React.SyntheticEvent) => {
|
async (event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
await deleteStep({ variables: { input: { id: stepId } } });
|
await deleteStep({ variables: { input: { id: stepId } } });
|
||||||
},
|
},
|
||||||
[stepId]
|
[stepId],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu
|
<Menu
|
||||||
open={true}
|
open={true}
|
||||||
@@ -46,5 +32,4 @@ function FlowStepContextMenu(
|
|||||||
</Menu>
|
</Menu>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default FlowStepContextMenu;
|
export default FlowStepContextMenu;
|
@@ -8,32 +8,18 @@ import Divider from '@mui/material/Divider';
|
|||||||
import IconButton from '@mui/material/IconButton';
|
import IconButton from '@mui/material/IconButton';
|
||||||
import RemoveIcon from '@mui/icons-material/Remove';
|
import RemoveIcon from '@mui/icons-material/Remove';
|
||||||
import AddIcon from '@mui/icons-material/Add';
|
import AddIcon from '@mui/icons-material/Add';
|
||||||
import type { IField, IFieldText, IFieldDropdown } from 'types';
|
|
||||||
|
|
||||||
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';
|
||||||
|
const createGroupItem = () => ({
|
||||||
type TGroupItem = {
|
|
||||||
key: string;
|
|
||||||
operator: string;
|
|
||||||
value: string;
|
|
||||||
id: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type TGroup = Record<'and', TGroupItem[]>;
|
|
||||||
|
|
||||||
const createGroupItem = (): TGroupItem => ({
|
|
||||||
key: '',
|
key: '',
|
||||||
operator: operators[0].value,
|
operator: operators[0].value,
|
||||||
value: '',
|
value: '',
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
});
|
});
|
||||||
|
const createGroup = () => ({
|
||||||
const createGroup = (): TGroup => ({
|
|
||||||
and: [createGroupItem()],
|
and: [createGroupItem()],
|
||||||
});
|
});
|
||||||
|
|
||||||
const operators = [
|
const operators = [
|
||||||
{
|
{
|
||||||
label: 'Equal',
|
label: 'Equal',
|
||||||
@@ -68,10 +54,7 @@ const operators = [
|
|||||||
value: 'not_contains',
|
value: 'not_contains',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
const createStringArgument = (argumentOptions) => {
|
||||||
const createStringArgument = (
|
|
||||||
argumentOptions: Omit<IFieldText, 'type' | 'required' | 'variables'>
|
|
||||||
): IField => {
|
|
||||||
return {
|
return {
|
||||||
...argumentOptions,
|
...argumentOptions,
|
||||||
type: 'string',
|
type: 'string',
|
||||||
@@ -79,69 +62,52 @@ const createStringArgument = (
|
|||||||
variables: true,
|
variables: true,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
const createDropdownArgument = (argumentOptions) => {
|
||||||
const createDropdownArgument = (
|
|
||||||
argumentOptions: Omit<IFieldDropdown, 'type' | 'required'>
|
|
||||||
): IField => {
|
|
||||||
return {
|
return {
|
||||||
...argumentOptions,
|
...argumentOptions,
|
||||||
required: true,
|
required: true,
|
||||||
type: 'dropdown',
|
type: 'dropdown',
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
function FilterConditions(props) {
|
||||||
type FilterConditionsProps = {
|
|
||||||
stepId: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
function FilterConditions(props: FilterConditionsProps): React.ReactElement {
|
|
||||||
const { stepId } = props;
|
const { stepId } = props;
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
const { control, setValue, getValues } = useFormContext();
|
const { control, setValue, getValues } = useFormContext();
|
||||||
const groups = useWatch({ control, name: 'parameters.or' });
|
const groups = useWatch({ control, name: 'parameters.or' });
|
||||||
const editorContext = React.useContext(EditorContext);
|
const editorContext = React.useContext(EditorContext);
|
||||||
|
|
||||||
React.useEffect(function addInitialGroupWhenEmpty() {
|
React.useEffect(function addInitialGroupWhenEmpty() {
|
||||||
const groups = getValues('parameters.or');
|
const groups = getValues('parameters.or');
|
||||||
|
|
||||||
if (!groups) {
|
if (!groups) {
|
||||||
setValue('parameters.or', [createGroup()]);
|
setValue('parameters.or', [createGroup()]);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const appendGroup = React.useCallback(() => {
|
const appendGroup = React.useCallback(() => {
|
||||||
const values = getValues('parameters.or');
|
const values = getValues('parameters.or');
|
||||||
|
|
||||||
setValue('parameters.or', values.concat(createGroup()));
|
setValue('parameters.or', values.concat(createGroup()));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const appendGroupItem = React.useCallback((index) => {
|
const appendGroupItem = React.useCallback((index) => {
|
||||||
const group = getValues(`parameters.or.${index}.and`);
|
const group = getValues(`parameters.or.${index}.and`);
|
||||||
setValue(`parameters.or.${index}.and`, group.concat(createGroupItem()));
|
setValue(`parameters.or.${index}.and`, group.concat(createGroupItem()));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const removeGroupItem = React.useCallback((groupIndex, groupItemIndex) => {
|
const removeGroupItem = React.useCallback((groupIndex, groupItemIndex) => {
|
||||||
const group: TGroupItem[] = getValues(`parameters.or.${groupIndex}.and`);
|
const group = getValues(`parameters.or.${groupIndex}.and`);
|
||||||
|
|
||||||
if (group.length === 1) {
|
if (group.length === 1) {
|
||||||
const groups: TGroup[] = getValues('parameters.or');
|
const groups = getValues('parameters.or');
|
||||||
|
|
||||||
setValue(
|
setValue(
|
||||||
'parameters.or',
|
'parameters.or',
|
||||||
groups.filter((group, index) => index !== groupIndex)
|
groups.filter((group, index) => index !== groupIndex),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
setValue(
|
setValue(
|
||||||
`parameters.or.${groupIndex}.and`,
|
`parameters.or.${groupIndex}.and`,
|
||||||
group.filter((groupItem, index) => index !== groupItemIndex)
|
group.filter((groupItem, index) => index !== groupItemIndex),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<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: TGroup, groupIndex: number) => (
|
{groups?.map((group, groupIndex) => (
|
||||||
<>
|
<>
|
||||||
{groupIndex !== 0 && <Divider />}
|
{groupIndex !== 0 && <Divider />}
|
||||||
|
|
||||||
@@ -152,81 +118,79 @@ function FilterConditions(props: FilterConditionsProps): React.ReactElement {
|
|||||||
formatMessage('filterConditions.orContinueIf')}
|
formatMessage('filterConditions.orContinueIf')}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
{group?.and?.map(
|
{group?.and?.map((groupItem, groupItemIndex) => (
|
||||||
(groupItem: TGroupItem, groupItemIndex: number) => (
|
<Stack direction="row" spacing={2} key={`item-${groupItem.id}`}>
|
||||||
<Stack direction="row" spacing={2} key={`item-${groupItem.id}`}>
|
<Stack
|
||||||
<Stack
|
direction={{ xs: 'column', sm: 'row' }}
|
||||||
direction={{ xs: 'column', sm: 'row' }}
|
spacing={{ xs: 2 }}
|
||||||
spacing={{ xs: 2 }}
|
sx={{ display: 'flex', flex: 1 }}
|
||||||
sx={{ display: 'flex', flex: 1 }}
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flex: '1 0 0px',
|
||||||
|
maxWidth: ['100%', '33%'],
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Box
|
<InputCreator
|
||||||
sx={{
|
schema={createStringArgument({
|
||||||
display: 'flex',
|
key: `or.${groupIndex}.and.${groupItemIndex}.key`,
|
||||||
flex: '1 0 0px',
|
label: 'Choose field',
|
||||||
maxWidth: ['100%', '33%'],
|
})}
|
||||||
}}
|
namePrefix="parameters"
|
||||||
>
|
stepId={stepId}
|
||||||
<InputCreator
|
disabled={editorContext.readOnly}
|
||||||
schema={createStringArgument({
|
/>
|
||||||
key: `or.${groupIndex}.and.${groupItemIndex}.key`,
|
</Box>
|
||||||
label: 'Choose field',
|
|
||||||
})}
|
|
||||||
namePrefix="parameters"
|
|
||||||
stepId={stepId}
|
|
||||||
disabled={editorContext.readOnly}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flex: '1 0 0px',
|
flex: '1 0 0px',
|
||||||
maxWidth: ['100%', '33%'],
|
maxWidth: ['100%', '33%'],
|
||||||
}}
|
}}
|
||||||
>
|
|
||||||
<InputCreator
|
|
||||||
schema={createDropdownArgument({
|
|
||||||
key: `or.${groupIndex}.and.${groupItemIndex}.operator`,
|
|
||||||
options: operators,
|
|
||||||
label: 'Choose condition',
|
|
||||||
})}
|
|
||||||
namePrefix="parameters"
|
|
||||||
stepId={stepId}
|
|
||||||
disabled={editorContext.readOnly}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: 'flex',
|
|
||||||
flex: '1 0 0px',
|
|
||||||
maxWidth: ['100%', '33%'],
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<InputCreator
|
|
||||||
schema={createStringArgument({
|
|
||||||
key: `or.${groupIndex}.and.${groupItemIndex}.value`,
|
|
||||||
label: 'Enter text',
|
|
||||||
})}
|
|
||||||
namePrefix="parameters"
|
|
||||||
stepId={stepId}
|
|
||||||
disabled={editorContext.readOnly}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
<IconButton
|
|
||||||
size="small"
|
|
||||||
edge="start"
|
|
||||||
onClick={() => removeGroupItem(groupIndex, groupItemIndex)}
|
|
||||||
sx={{ width: 61, height: 61 }}
|
|
||||||
>
|
>
|
||||||
<RemoveIcon />
|
<InputCreator
|
||||||
</IconButton>
|
schema={createDropdownArgument({
|
||||||
|
key: `or.${groupIndex}.and.${groupItemIndex}.operator`,
|
||||||
|
options: operators,
|
||||||
|
label: 'Choose condition',
|
||||||
|
})}
|
||||||
|
namePrefix="parameters"
|
||||||
|
stepId={stepId}
|
||||||
|
disabled={editorContext.readOnly}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flex: '1 0 0px',
|
||||||
|
maxWidth: ['100%', '33%'],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<InputCreator
|
||||||
|
schema={createStringArgument({
|
||||||
|
key: `or.${groupIndex}.and.${groupItemIndex}.value`,
|
||||||
|
label: 'Enter text',
|
||||||
|
})}
|
||||||
|
namePrefix="parameters"
|
||||||
|
stepId={stepId}
|
||||||
|
disabled={editorContext.readOnly}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
|
||||||
)}
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
edge="start"
|
||||||
|
onClick={() => removeGroupItem(groupIndex, groupItemIndex)}
|
||||||
|
sx={{ width: 61, height: 61 }}
|
||||||
|
>
|
||||||
|
<RemoveIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Stack>
|
||||||
|
))}
|
||||||
|
|
||||||
<Stack spacing={1} direction="row">
|
<Stack spacing={1} direction="row">
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -255,5 +219,4 @@ function FilterConditions(props: FilterConditionsProps): React.ReactElement {
|
|||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default FilterConditions;
|
export default FilterConditions;
|
@@ -4,24 +4,11 @@ import Collapse from '@mui/material/Collapse';
|
|||||||
import ListItem from '@mui/material/ListItem';
|
import ListItem from '@mui/material/ListItem';
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
import Stack from '@mui/material/Stack';
|
import Stack from '@mui/material/Stack';
|
||||||
import type { IStep, ISubstep } from 'types';
|
|
||||||
|
|
||||||
import { EditorContext } from 'contexts/Editor';
|
import { EditorContext } from 'contexts/Editor';
|
||||||
import FlowSubstepTitle from 'components/FlowSubstepTitle';
|
import FlowSubstepTitle from 'components/FlowSubstepTitle';
|
||||||
import InputCreator from 'components/InputCreator';
|
import InputCreator from 'components/InputCreator';
|
||||||
import FilterConditions from './FilterConditions';
|
import FilterConditions from './FilterConditions';
|
||||||
|
function FlowSubstep(props) {
|
||||||
type FlowSubstepProps = {
|
|
||||||
substep: ISubstep;
|
|
||||||
expanded?: boolean;
|
|
||||||
onExpand: () => void;
|
|
||||||
onCollapse: () => void;
|
|
||||||
onChange: ({ step }: { step: IStep }) => void;
|
|
||||||
onSubmit: () => void;
|
|
||||||
step: IStep;
|
|
||||||
};
|
|
||||||
|
|
||||||
function FlowSubstep(props: FlowSubstepProps): React.ReactElement {
|
|
||||||
const {
|
const {
|
||||||
substep,
|
substep,
|
||||||
expanded = false,
|
expanded = false,
|
||||||
@@ -30,15 +17,11 @@ function FlowSubstep(props: FlowSubstepProps): React.ReactElement {
|
|||||||
onSubmit,
|
onSubmit,
|
||||||
step,
|
step,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const { name, arguments: args } = substep;
|
const { name, arguments: args } = substep;
|
||||||
|
|
||||||
const editorContext = React.useContext(EditorContext);
|
const editorContext = React.useContext(EditorContext);
|
||||||
const formContext = useFormContext();
|
const formContext = useFormContext();
|
||||||
const validationStatus = formContext.formState.isValid;
|
const validationStatus = formContext.formState.isValid;
|
||||||
|
|
||||||
const onToggle = expanded ? onCollapse : onExpand;
|
const onToggle = expanded ? onCollapse : onExpand;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<FlowSubstepTitle
|
<FlowSubstepTitle
|
||||||
@@ -90,5 +73,4 @@ function FlowSubstep(props: FlowSubstepProps): React.ReactElement {
|
|||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default FlowSubstep;
|
export default FlowSubstep;
|
@@ -3,25 +3,13 @@ import ExpandLessIcon from '@mui/icons-material/ExpandLess';
|
|||||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||||
import ErrorIcon from '@mui/icons-material/Error';
|
import ErrorIcon from '@mui/icons-material/Error';
|
||||||
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
||||||
|
|
||||||
import { ListItemButton, Typography } from './style';
|
import { ListItemButton, Typography } from './style';
|
||||||
|
|
||||||
type FlowSubstepTitleProps = {
|
|
||||||
expanded?: boolean;
|
|
||||||
onClick: () => void;
|
|
||||||
title: string;
|
|
||||||
valid?: boolean | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const validIcon = <CheckCircleIcon color="success" />;
|
const validIcon = <CheckCircleIcon color="success" />;
|
||||||
const errorIcon = <ErrorIcon color="error" />;
|
const errorIcon = <ErrorIcon color="error" />;
|
||||||
|
function FlowSubstepTitle(props) {
|
||||||
function FlowSubstepTitle(props: FlowSubstepTitleProps): React.ReactElement {
|
|
||||||
const { expanded = false, onClick = () => null, valid = null, title } = props;
|
const { expanded = false, onClick = () => null, valid = null, title } = props;
|
||||||
|
|
||||||
const hasValidation = valid !== null;
|
const hasValidation = valid !== null;
|
||||||
const validationStatusIcon = valid ? validIcon : errorIcon;
|
const validationStatusIcon = valid ? validIcon : errorIcon;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListItemButton onClick={onClick} selected={expanded} divider>
|
<ListItemButton onClick={onClick} selected={expanded} divider>
|
||||||
<Typography variant="body2">
|
<Typography variant="body2">
|
||||||
@@ -33,5 +21,4 @@ function FlowSubstepTitle(props: FlowSubstepTitleProps): React.ReactElement {
|
|||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default FlowSubstepTitle;
|
export default FlowSubstepTitle;
|
@@ -1,11 +1,9 @@
|
|||||||
import { styled } from '@mui/material/styles';
|
import { styled } from '@mui/material/styles';
|
||||||
import MuiListItemButton from '@mui/material/ListItemButton';
|
import MuiListItemButton from '@mui/material/ListItemButton';
|
||||||
import MuiTypography from '@mui/material/Typography';
|
import MuiTypography from '@mui/material/Typography';
|
||||||
|
|
||||||
export const ListItemButton = styled(MuiListItemButton)`
|
export const ListItemButton = styled(MuiListItemButton)`
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Typography = styled(MuiTypography)`
|
export const Typography = styled(MuiTypography)`
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
@@ -3,24 +3,20 @@ import { useMutation } from '@apollo/client';
|
|||||||
import Paper from '@mui/material/Paper';
|
import Paper from '@mui/material/Paper';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import LoadingButton from '@mui/lab/LoadingButton';
|
import LoadingButton from '@mui/lab/LoadingButton';
|
||||||
|
|
||||||
import { FORGOT_PASSWORD } from 'graphql/mutations/forgot-password.ee';
|
import { FORGOT_PASSWORD } from 'graphql/mutations/forgot-password.ee';
|
||||||
import Form from 'components/Form';
|
import Form from 'components/Form';
|
||||||
import TextField from 'components/TextField';
|
import TextField from 'components/TextField';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
|
|
||||||
export default function ForgotPasswordForm() {
|
export default function ForgotPasswordForm() {
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
const [forgotPassword, { data, loading }] = useMutation(FORGOT_PASSWORD);
|
const [forgotPassword, { data, loading }] = useMutation(FORGOT_PASSWORD);
|
||||||
|
const handleSubmit = async (values) => {
|
||||||
const handleSubmit = async (values: any) => {
|
|
||||||
await forgotPassword({
|
await forgotPassword({
|
||||||
variables: {
|
variables: {
|
||||||
input: values,
|
input: values,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper sx={{ px: 2, py: 4 }}>
|
<Paper sx={{ px: 2, py: 4 }}>
|
||||||
<Typography
|
<Typography
|
||||||
@@ -59,9 +55,14 @@ export default function ForgotPasswordForm() {
|
|||||||
{formatMessage('forgotPasswordForm.submit')}
|
{formatMessage('forgotPasswordForm.submit')}
|
||||||
</LoadingButton>
|
</LoadingButton>
|
||||||
|
|
||||||
{data && <Typography variant="body1" sx={{ color: (theme) => theme.palette.success.main }}>
|
{data && (
|
||||||
{formatMessage('forgotPasswordForm.instructionsSent')}
|
<Typography
|
||||||
</Typography>}
|
variant="body1"
|
||||||
|
sx={{ color: (theme) => theme.palette.success.main }}
|
||||||
|
>
|
||||||
|
{formatMessage('forgotPasswordForm.instructionsSent')}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
</Form>
|
</Form>
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
@@ -1,26 +1,7 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {
|
import { FormProvider, useForm, useWatch } from 'react-hook-form';
|
||||||
FormProvider,
|
|
||||||
useForm,
|
|
||||||
useWatch,
|
|
||||||
FieldValues,
|
|
||||||
SubmitHandler,
|
|
||||||
UseFormReturn,
|
|
||||||
} from 'react-hook-form';
|
|
||||||
import type { UseFormProps } from 'react-hook-form';
|
|
||||||
|
|
||||||
type FormProps = {
|
|
||||||
children?: React.ReactNode;
|
|
||||||
defaultValues?: UseFormProps['defaultValues'];
|
|
||||||
onSubmit?: SubmitHandler<FieldValues>;
|
|
||||||
render?: (props: UseFormReturn) => React.ReactNode;
|
|
||||||
resolver?: UseFormProps['resolver'];
|
|
||||||
mode?: UseFormProps['mode'];
|
|
||||||
};
|
|
||||||
|
|
||||||
const noop = () => null;
|
const noop = () => null;
|
||||||
|
export default function Form(props) {
|
||||||
export default function Form(props: FormProps): React.ReactElement {
|
|
||||||
const {
|
const {
|
||||||
children,
|
children,
|
||||||
onSubmit = noop,
|
onSubmit = noop,
|
||||||
@@ -30,27 +11,22 @@ export default function Form(props: FormProps): React.ReactElement {
|
|||||||
mode = 'all',
|
mode = 'all',
|
||||||
...formProps
|
...formProps
|
||||||
} = props;
|
} = props;
|
||||||
|
const methods = useForm({
|
||||||
const methods: UseFormReturn = useForm({
|
|
||||||
defaultValues,
|
defaultValues,
|
||||||
reValidateMode: 'onBlur',
|
reValidateMode: 'onBlur',
|
||||||
resolver,
|
resolver,
|
||||||
mode,
|
mode,
|
||||||
});
|
});
|
||||||
|
|
||||||
const form = useWatch({ control: methods.control });
|
const form = useWatch({ control: methods.control });
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* For fields having `dependsOn` fields, we need to re-validate the form.
|
* For fields having `dependsOn` fields, we need to re-validate the form.
|
||||||
*/
|
*/
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
methods.trigger();
|
methods.trigger();
|
||||||
}, [methods.trigger, form]);
|
}, [methods.trigger, form]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
methods.reset(defaultValues);
|
methods.reset(defaultValues);
|
||||||
}, [defaultValues]);
|
}, [defaultValues]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormProvider {...methods}>
|
<FormProvider {...methods}>
|
||||||
<form onSubmit={methods.handleSubmit(onSubmit)} {...formProps}>
|
<form onSubmit={methods.handleSubmit(onSubmit)} {...formProps}>
|
@@ -1,9 +1,7 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import Slide, { SlideProps } from '@mui/material/Slide';
|
import Slide from '@mui/material/Slide';
|
||||||
import useScrollTrigger from '@mui/material/useScrollTrigger';
|
import useScrollTrigger from '@mui/material/useScrollTrigger';
|
||||||
|
export default function HideOnScroll(props) {
|
||||||
export default function HideOnScroll(props: SlideProps): React.ReactElement {
|
|
||||||
const trigger = useScrollTrigger();
|
const trigger = useScrollTrigger();
|
||||||
|
|
||||||
return <Slide appear={false} direction="down" in={!trigger} {...props} />;
|
return <Slide appear={false} direction="down" in={!trigger} {...props} />;
|
||||||
}
|
}
|
@@ -1,8 +1,6 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import MuiTextField from '@mui/material/TextField';
|
import MuiTextField from '@mui/material/TextField';
|
||||||
import CircularProgress from '@mui/material/CircularProgress';
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
import type { IField, IFieldDropdownOption } from 'types';
|
|
||||||
|
|
||||||
import useDynamicFields from 'hooks/useDynamicFields';
|
import useDynamicFields from 'hooks/useDynamicFields';
|
||||||
import useDynamicData from 'hooks/useDynamicData';
|
import useDynamicData from 'hooks/useDynamicData';
|
||||||
import PowerInput from 'components/PowerInput';
|
import PowerInput from 'components/PowerInput';
|
||||||
@@ -10,29 +8,9 @@ import TextField from 'components/TextField';
|
|||||||
import ControlledAutocomplete from 'components/ControlledAutocomplete';
|
import ControlledAutocomplete from 'components/ControlledAutocomplete';
|
||||||
import ControlledCustomAutocomplete from 'components/ControlledCustomAutocomplete';
|
import ControlledCustomAutocomplete from 'components/ControlledCustomAutocomplete';
|
||||||
import DynamicField from 'components/DynamicField';
|
import DynamicField from 'components/DynamicField';
|
||||||
|
const optionGenerator = (options) =>
|
||||||
type InputCreatorProps = {
|
options?.map(({ name, value }) => ({ label: name, value: value }));
|
||||||
onChange?: React.ChangeEventHandler;
|
export default function InputCreator(props) {
|
||||||
onBlur?: React.FocusEventHandler;
|
|
||||||
schema: IField;
|
|
||||||
namePrefix?: string;
|
|
||||||
stepId?: string;
|
|
||||||
disabled?: boolean;
|
|
||||||
showOptionValue?: boolean;
|
|
||||||
shouldUnregister?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
type RawOption = {
|
|
||||||
name: string;
|
|
||||||
value: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const optionGenerator = (options: RawOption[]): IFieldDropdownOption[] =>
|
|
||||||
options?.map(({ name, value }) => ({ label: name as string, value: value }));
|
|
||||||
|
|
||||||
export default function InputCreator(
|
|
||||||
props: InputCreatorProps
|
|
||||||
): React.ReactElement {
|
|
||||||
const {
|
const {
|
||||||
onChange,
|
onChange,
|
||||||
onBlur,
|
onBlur,
|
||||||
@@ -43,7 +21,6 @@ export default function InputCreator(
|
|||||||
showOptionValue,
|
showOptionValue,
|
||||||
shouldUnregister,
|
shouldUnregister,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
key: name,
|
key: name,
|
||||||
label,
|
label,
|
||||||
@@ -53,13 +30,10 @@ export default function InputCreator(
|
|||||||
description,
|
description,
|
||||||
type,
|
type,
|
||||||
} = schema;
|
} = schema;
|
||||||
|
|
||||||
const { data, loading } = useDynamicData(stepId, schema);
|
const { data, loading } = useDynamicData(stepId, schema);
|
||||||
|
|
||||||
const { data: additionalFields, loading: additionalFieldsLoading } =
|
const { data: additionalFields, loading: additionalFieldsLoading } =
|
||||||
useDynamicFields(stepId, schema);
|
useDynamicFields(stepId, schema);
|
||||||
const computedName = namePrefix ? `${namePrefix}.${name}` : name;
|
const computedName = namePrefix ? `${namePrefix}.${name}` : name;
|
||||||
|
|
||||||
if (type === 'dynamic') {
|
if (type === 'dynamic') {
|
||||||
return (
|
return (
|
||||||
<DynamicField
|
<DynamicField
|
||||||
@@ -76,10 +50,8 @@ export default function InputCreator(
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'dropdown') {
|
if (type === 'dropdown') {
|
||||||
const preparedOptions = schema.options || optionGenerator(data);
|
const preparedOptions = schema.options || optionGenerator(data);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{!schema.variables && (
|
{!schema.variables && (
|
||||||
@@ -92,7 +64,7 @@ export default function InputCreator(
|
|||||||
disableClearable={required}
|
disableClearable={required}
|
||||||
options={preparedOptions}
|
options={preparedOptions}
|
||||||
renderInput={(params) => <MuiTextField {...params} label={label} />}
|
renderInput={(params) => <MuiTextField {...params} label={label} />}
|
||||||
defaultValue={value as string}
|
defaultValue={value}
|
||||||
description={description}
|
description={description}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
@@ -112,7 +84,7 @@ export default function InputCreator(
|
|||||||
disableClearable={required}
|
disableClearable={required}
|
||||||
options={preparedOptions}
|
options={preparedOptions}
|
||||||
renderInput={(params) => <MuiTextField {...params} label={label} />}
|
renderInput={(params) => <MuiTextField {...params} label={label} />}
|
||||||
defaultValue={value as string}
|
defaultValue={value}
|
||||||
description={description}
|
description={description}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
@@ -141,7 +113,6 @@ export default function InputCreator(
|
|||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'string') {
|
if (type === 'string') {
|
||||||
if (schema.variables) {
|
if (schema.variables) {
|
||||||
return (
|
return (
|
||||||
@@ -178,7 +149,6 @@ export default function InputCreator(
|
|||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<TextField
|
<TextField
|
||||||
@@ -218,6 +188,5 @@ export default function InputCreator(
|
|||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <React.Fragment />;
|
return <React.Fragment />;
|
||||||
}
|
}
|
@@ -1,17 +1,8 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
|
|
||||||
import { Container } from './style';
|
import { Container } from './style';
|
||||||
|
export default function IntermediateStepCount(props) {
|
||||||
type IntermediateStepCountProps = {
|
|
||||||
count: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function IntermediateStepCount(
|
|
||||||
props: IntermediateStepCountProps
|
|
||||||
) {
|
|
||||||
const { count } = props;
|
const { count } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<Typography variant="subtitle1" sx={{}}>
|
<Typography variant="subtitle1" sx={{}}>
|
@@ -1,5 +1,4 @@
|
|||||||
import { styled } from '@mui/material/styles';
|
import { styled } from '@mui/material/styles';
|
||||||
|
|
||||||
export const Container = styled('div')(({ theme }) => ({
|
export const Container = styled('div')(({ theme }) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
@@ -1,11 +1,6 @@
|
|||||||
import { IntlProvider as BaseIntlProvider } from 'react-intl';
|
import { IntlProvider as BaseIntlProvider } from 'react-intl';
|
||||||
import englishMessages from 'locales/en.json';
|
import englishMessages from 'locales/en.json';
|
||||||
|
const IntlProvider = ({ children }) => {
|
||||||
type IntlProviderProps = {
|
|
||||||
children: React.ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
const IntlProvider = ({ children }: IntlProviderProps): React.ReactElement => {
|
|
||||||
return (
|
return (
|
||||||
<BaseIntlProvider
|
<BaseIntlProvider
|
||||||
locale={navigator.language}
|
locale={navigator.language}
|
||||||
@@ -16,5 +11,4 @@ const IntlProvider = ({ children }: IntlProviderProps): React.ReactElement => {
|
|||||||
</BaseIntlProvider>
|
</BaseIntlProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default IntlProvider;
|
export default IntlProvider;
|
@@ -7,16 +7,12 @@ import CardContent from '@mui/material/CardContent';
|
|||||||
import Divider from '@mui/material/Divider';
|
import Divider from '@mui/material/Divider';
|
||||||
import Grid from '@mui/material/Grid';
|
import Grid from '@mui/material/Grid';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
|
|
||||||
import useInvoices from 'hooks/useInvoices.ee';
|
import useInvoices from 'hooks/useInvoices.ee';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
|
|
||||||
export default function Invoices() {
|
export default function Invoices() {
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
const { invoices, loading } = useInvoices();
|
const { invoices, loading } = useInvoices();
|
||||||
|
|
||||||
if (loading || invoices.length === 0) return <React.Fragment />;
|
if (loading || invoices.length === 0) return <React.Fragment />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<Card sx={{ mb: 3, p: 2 }}>
|
<Card sx={{ mb: 3, p: 2 }}>
|
||||||
@@ -69,7 +65,9 @@ export default function Invoices() {
|
|||||||
fontWeight="500"
|
fontWeight="500"
|
||||||
sx={{ color: 'text.secondary' }}
|
sx={{ color: 'text.secondary' }}
|
||||||
>
|
>
|
||||||
{DateTime.fromISO(invoice.payout_date).toFormat('LLL dd, yyyy')}
|
{DateTime.fromISO(invoice.payout_date).toFormat(
|
||||||
|
'LLL dd, yyyy',
|
||||||
|
)}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
@@ -1,11 +1,5 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { JSONTree } from 'react-json-tree';
|
import { JSONTree } from 'react-json-tree';
|
||||||
import type { IJSONObject } from 'types';
|
|
||||||
|
|
||||||
type JSONViewerProps = {
|
|
||||||
data: IJSONObject;
|
|
||||||
};
|
|
||||||
|
|
||||||
const theme = {
|
const theme = {
|
||||||
scheme: 'inspector',
|
scheme: 'inspector',
|
||||||
author: 'Alexander Kuznetsov (alexkuz@gmail.com)',
|
author: 'Alexander Kuznetsov (alexkuz@gmail.com)',
|
||||||
@@ -36,16 +30,14 @@ const theme = {
|
|||||||
// base0C - Support, Regular Expressions, Escape Characters, Markup Quotes
|
// base0C - Support, Regular Expressions, Escape Characters, Markup Quotes
|
||||||
base0C: '#86c1b9',
|
base0C: '#86c1b9',
|
||||||
// base0D - Functions, Methods, Attribute IDs, Headings
|
// base0D - Functions, Methods, Attribute IDs, Headings
|
||||||
base0D: '#d73a49', // key
|
base0D: '#d73a49',
|
||||||
// base0E - Keywords, Storage, Selector, Markup Italic, Diff Changed
|
// base0E - Keywords, Storage, Selector, Markup Italic, Diff Changed
|
||||||
base0E: '#EC31C0',
|
base0E: '#EC31C0',
|
||||||
// base0F - Deprecated, Opening/Closing Embedded Language Tags, e.g. <?php ?>
|
// base0F - Deprecated, Opening/Closing Embedded Language Tags, e.g. <?php ?>
|
||||||
base0F: '#a16946',
|
base0F: '#a16946',
|
||||||
};
|
};
|
||||||
|
function JSONViewer(props) {
|
||||||
function JSONViewer(props: JSONViewerProps) {
|
|
||||||
const { data } = props;
|
const { data } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<JSONTree
|
<JSONTree
|
||||||
hideRoot
|
hideRoot
|
||||||
@@ -56,5 +48,4 @@ function JSONViewer(props: JSONViewerProps) {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default JSONViewer;
|
export default JSONViewer;
|
@@ -1,5 +1,4 @@
|
|||||||
import GlobalStyles from '@mui/material/GlobalStyles';
|
import GlobalStyles from '@mui/material/GlobalStyles';
|
||||||
|
|
||||||
export const jsonViewerStyles = (
|
export const jsonViewerStyles = (
|
||||||
<GlobalStyles
|
<GlobalStyles
|
||||||
styles={(theme) => ({
|
styles={(theme) => ({
|
||||||
@@ -16,17 +15,14 @@ export const jsonViewerStyles = (
|
|||||||
'var(--indentguide-size) var(--indentguide-style) var(--indentguide-color)',
|
'var(--indentguide-size) var(--indentguide-style) var(--indentguide-color)',
|
||||||
'--indentguide-active':
|
'--indentguide-active':
|
||||||
'var(--indentguide-size) var(--indentguide-style) var(--indentguide-color-active)',
|
'var(--indentguide-size) var(--indentguide-style) var(--indentguide-color-active)',
|
||||||
|
|
||||||
/* Types colors */
|
/* Types colors */
|
||||||
'--string-color': theme.palette.text.secondary,
|
'--string-color': theme.palette.text.secondary,
|
||||||
'--number-color': theme.palette.text.primary,
|
'--number-color': theme.palette.text.primary,
|
||||||
'--boolean-color': theme.palette.text.primary,
|
'--boolean-color': theme.palette.text.primary,
|
||||||
'--null-color': theme.palette.text.primary,
|
'--null-color': theme.palette.text.primary,
|
||||||
'--property-color': theme.palette.text.primary,
|
'--property-color': theme.palette.text.primary,
|
||||||
|
|
||||||
/* Collapsed node preview */
|
/* Collapsed node preview */
|
||||||
'--preview-color': theme.palette.text.primary,
|
'--preview-color': theme.palette.text.primary,
|
||||||
|
|
||||||
/* Search highlight color */
|
/* Search highlight color */
|
||||||
'--highlight-color': '#6fb3d2',
|
'--highlight-color': '#6fb3d2',
|
||||||
},
|
},
|
@@ -9,18 +9,12 @@ import SwapCallsIcon from '@mui/icons-material/SwapCalls';
|
|||||||
import HistoryIcon from '@mui/icons-material/History';
|
import HistoryIcon from '@mui/icons-material/History';
|
||||||
import NotificationsIcon from '@mui/icons-material/Notifications';
|
import NotificationsIcon from '@mui/icons-material/Notifications';
|
||||||
import ArrowBackIosNew from '@mui/icons-material/ArrowBackIosNew';
|
import ArrowBackIosNew from '@mui/icons-material/ArrowBackIosNew';
|
||||||
|
|
||||||
import * as URLS from 'config/urls';
|
import * as URLS from 'config/urls';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
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 useConfig from 'hooks/useConfig';
|
||||||
|
|
||||||
type PublicLayoutProps = {
|
|
||||||
children: React.ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
const drawerLinks = [
|
const drawerLinks = [
|
||||||
{
|
{
|
||||||
Icon: SwapCallsIcon,
|
Icon: SwapCallsIcon,
|
||||||
@@ -41,64 +35,37 @@ const drawerLinks = [
|
|||||||
dataTest: 'executions-page-drawer-link',
|
dataTest: 'executions-page-drawer-link',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
type GenerateDrawerBottomLinksOptions = {
|
|
||||||
disableNotificationsPage: boolean;
|
|
||||||
notificationBadgeContent: number;
|
|
||||||
additionalDrawerLink?: string;
|
|
||||||
additionalDrawerLinkText?: string;
|
|
||||||
additionalDrawerLinkIcon?: string;
|
|
||||||
formatMessage: ReturnType<typeof useFormatMessage>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const generateDrawerBottomLinks = async ({
|
const generateDrawerBottomLinks = async ({
|
||||||
disableNotificationsPage,
|
disableNotificationsPage,
|
||||||
notificationBadgeContent = 0,
|
notificationBadgeContent = 0,
|
||||||
additionalDrawerLink,
|
additionalDrawerLink,
|
||||||
additionalDrawerLinkText,
|
additionalDrawerLinkText,
|
||||||
formatMessage,
|
formatMessage,
|
||||||
}: GenerateDrawerBottomLinksOptions) => {
|
}) => {
|
||||||
const notificationsPageLinkObject = {
|
const notificationsPageLinkObject = {
|
||||||
Icon: NotificationsIcon,
|
Icon: NotificationsIcon,
|
||||||
primary: formatMessage('settingsDrawer.notifications'),
|
primary: formatMessage('settingsDrawer.notifications'),
|
||||||
to: URLS.UPDATES,
|
to: URLS.UPDATES,
|
||||||
badgeContent: notificationBadgeContent,
|
badgeContent: notificationBadgeContent,
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasAdditionalDrawerLink =
|
const hasAdditionalDrawerLink =
|
||||||
additionalDrawerLink && additionalDrawerLinkText;
|
additionalDrawerLink && additionalDrawerLinkText;
|
||||||
|
|
||||||
const additionalDrawerLinkObject = {
|
const additionalDrawerLinkObject = {
|
||||||
Icon: ArrowBackIosNew,
|
Icon: ArrowBackIosNew,
|
||||||
primary: additionalDrawerLinkText || '',
|
primary: additionalDrawerLinkText || '',
|
||||||
to: additionalDrawerLink || '',
|
to: additionalDrawerLink || '',
|
||||||
target: '_blank' as const,
|
target: '_blank',
|
||||||
};
|
};
|
||||||
|
|
||||||
const links = [];
|
const links = [];
|
||||||
|
|
||||||
if (!disableNotificationsPage) {
|
if (!disableNotificationsPage) {
|
||||||
links.push(notificationsPageLinkObject);
|
links.push(notificationsPageLinkObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasAdditionalDrawerLink) {
|
if (hasAdditionalDrawerLink) {
|
||||||
links.push(additionalDrawerLinkObject);
|
links.push(additionalDrawerLinkObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
return links;
|
return links;
|
||||||
};
|
};
|
||||||
|
export default function PublicLayout({ children }) {
|
||||||
type Link = {
|
|
||||||
Icon: React.ElementType;
|
|
||||||
primary: string;
|
|
||||||
target?: '_blank';
|
|
||||||
to: string;
|
|
||||||
badgeContent?: React.ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function PublicLayout({
|
|
||||||
children,
|
|
||||||
}: PublicLayoutProps): React.ReactElement {
|
|
||||||
const version = useVersion();
|
const version = useVersion();
|
||||||
const { config, loading } = useConfig([
|
const { config, loading } = useConfig([
|
||||||
'disableNotificationsPage',
|
'disableNotificationsPage',
|
||||||
@@ -107,31 +74,25 @@ export default function PublicLayout({
|
|||||||
]);
|
]);
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
const [bottomLinks, setBottomLinks] = React.useState<Link[]>([]);
|
const [bottomLinks, setBottomLinks] = React.useState([]);
|
||||||
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('lg'));
|
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('lg'));
|
||||||
const [isDrawerOpen, setDrawerOpen] = React.useState(!matchSmallScreens);
|
const [isDrawerOpen, setDrawerOpen] = React.useState(!matchSmallScreens);
|
||||||
|
|
||||||
const openDrawer = () => setDrawerOpen(true);
|
const openDrawer = () => setDrawerOpen(true);
|
||||||
const closeDrawer = () => setDrawerOpen(false);
|
const closeDrawer = () => setDrawerOpen(false);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
async function perform() {
|
async function perform() {
|
||||||
const newBottomLinks = await generateDrawerBottomLinks({
|
const newBottomLinks = await generateDrawerBottomLinks({
|
||||||
notificationBadgeContent: version.newVersionCount,
|
notificationBadgeContent: version.newVersionCount,
|
||||||
disableNotificationsPage: config?.disableNotificationsPage as boolean,
|
disableNotificationsPage: config?.disableNotificationsPage,
|
||||||
additionalDrawerLink: config?.additionalDrawerLink as string,
|
additionalDrawerLink: config?.additionalDrawerLink,
|
||||||
additionalDrawerLinkText: config?.additionalDrawerLinkText as string,
|
additionalDrawerLinkText: config?.additionalDrawerLinkText,
|
||||||
formatMessage,
|
formatMessage,
|
||||||
});
|
});
|
||||||
|
|
||||||
setBottomLinks(newBottomLinks);
|
setBottomLinks(newBottomLinks);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loading) return;
|
if (loading) return;
|
||||||
|
|
||||||
perform();
|
perform();
|
||||||
}, [config, loading, version.newVersionCount]);
|
}, [config, loading, version.newVersionCount]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AppBar
|
<AppBar
|
49
packages/web/src/components/ListItemLink/index.jsx
Normal file
49
packages/web/src/components/ListItemLink/index.jsx
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { useMatch } from 'react-router-dom';
|
||||||
|
import ListItem from '@mui/material/ListItemButton';
|
||||||
|
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||||
|
import ListItemText from '@mui/material/ListItemText';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
export default function ListItemLink(props) {
|
||||||
|
const { icon, primary, to, onClick, 'data-test': dataTest, target } = props;
|
||||||
|
const selected = useMatch({ path: to, end: true });
|
||||||
|
const CustomLink = React.useMemo(
|
||||||
|
() =>
|
||||||
|
React.forwardRef(function InLineLink(linkProps, ref) {
|
||||||
|
try {
|
||||||
|
// challenge the link to check if it's absolute URL
|
||||||
|
new URL(to); // should throw an error if it's not an absolute URL
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
{...linkProps}
|
||||||
|
ref={ref}
|
||||||
|
href={to}
|
||||||
|
target={target}
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
return <Link ref={ref} {...linkProps} to={to} />;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
[to],
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<li>
|
||||||
|
<ListItem
|
||||||
|
component={CustomLink}
|
||||||
|
sx={{ pl: { xs: 2, sm: 3 } }}
|
||||||
|
selected={!!selected}
|
||||||
|
onClick={onClick}
|
||||||
|
data-test={dataTest}
|
||||||
|
target={target}
|
||||||
|
>
|
||||||
|
<ListItemIcon sx={{ minWidth: 52 }}>{icon}</ListItemIcon>
|
||||||
|
<ListItemText
|
||||||
|
primary={primary}
|
||||||
|
primaryTypographyProps={{ variant: 'body1' }}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
@@ -1,66 +0,0 @@
|
|||||||
import * as React from 'react';
|
|
||||||
import { useMatch } from 'react-router-dom';
|
|
||||||
import ListItem from '@mui/material/ListItemButton';
|
|
||||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
|
||||||
import ListItemText from '@mui/material/ListItemText';
|
|
||||||
import { Link, LinkProps } from 'react-router-dom';
|
|
||||||
|
|
||||||
type ListItemLinkProps = {
|
|
||||||
icon: React.ReactNode;
|
|
||||||
primary: string;
|
|
||||||
to: string;
|
|
||||||
target?: '_blank';
|
|
||||||
onClick?: (event: React.SyntheticEvent) => void;
|
|
||||||
'data-test'?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function ListItemLink(
|
|
||||||
props: ListItemLinkProps
|
|
||||||
): React.ReactElement {
|
|
||||||
const { icon, primary, to, onClick, 'data-test': dataTest, target } = props;
|
|
||||||
const selected = useMatch({ path: to, end: true });
|
|
||||||
|
|
||||||
const CustomLink = React.useMemo(
|
|
||||||
() =>
|
|
||||||
React.forwardRef<HTMLAnchorElement, Omit<LinkProps, 'to'>>(
|
|
||||||
function InLineLink(linkProps, ref) {
|
|
||||||
try {
|
|
||||||
// challenge the link to check if it's absolute URL
|
|
||||||
new URL(to); // should throw an error if it's not an absolute URL
|
|
||||||
|
|
||||||
return (
|
|
||||||
<a
|
|
||||||
{...linkProps}
|
|
||||||
ref={ref}
|
|
||||||
href={to}
|
|
||||||
target={target}
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} catch {
|
|
||||||
return <Link ref={ref} {...linkProps} to={to} />;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
),
|
|
||||||
[to]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<li>
|
|
||||||
<ListItem
|
|
||||||
component={CustomLink}
|
|
||||||
sx={{ pl: { xs: 2, sm: 3 } }}
|
|
||||||
selected={!!selected}
|
|
||||||
onClick={onClick}
|
|
||||||
data-test={dataTest}
|
|
||||||
target={target}
|
|
||||||
>
|
|
||||||
<ListItemIcon sx={{ minWidth: 52 }}>{icon}</ListItemIcon>
|
|
||||||
<ListItemText
|
|
||||||
primary={primary}
|
|
||||||
primaryTypographyProps={{ variant: 'body1' }}
|
|
||||||
/>
|
|
||||||
</ListItem>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
}
|
|
@@ -7,18 +7,7 @@ import {
|
|||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import EditIcon from '@mui/icons-material/Edit';
|
import EditIcon from '@mui/icons-material/Edit';
|
||||||
import DeleteIcon from '@mui/icons-material/Delete';
|
import DeleteIcon from '@mui/icons-material/Delete';
|
||||||
|
const ListLoader = ({ rowsNumber, columnsNumber, 'data-test': dataTest }) => {
|
||||||
type ListLoaderProps = {
|
|
||||||
rowsNumber: number;
|
|
||||||
columnsNumber: number;
|
|
||||||
'data-test'?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ListLoader = ({
|
|
||||||
rowsNumber,
|
|
||||||
columnsNumber,
|
|
||||||
'data-test': dataTest,
|
|
||||||
}: ListLoaderProps) => {
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{[...Array(rowsNumber)].map((row, index) => (
|
{[...Array(rowsNumber)].map((row, index) => (
|
||||||
@@ -49,5 +38,4 @@ const ListLoader = ({
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ListLoader;
|
export default ListLoader;
|
45
packages/web/src/components/LiveChat/Chatwoot.ee.jsx
Normal file
45
packages/web/src/components/LiveChat/Chatwoot.ee.jsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { useTheme } from '@mui/material/styles';
|
||||||
|
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||||
|
import appConfig from 'config/app';
|
||||||
|
import useCurrentUser from 'hooks/useCurrentUser';
|
||||||
|
const Chatwoot = ({ ready }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const currentUser = useCurrentUser();
|
||||||
|
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
|
React.useEffect(function initiateChatwoot() {
|
||||||
|
window.chatwootSDK.run({
|
||||||
|
websiteToken: 'EFyq5MTsvS7XwUrwSH36VskT',
|
||||||
|
baseUrl: appConfig.chatwootBaseUrl,
|
||||||
|
});
|
||||||
|
return function removeChatwoot() {
|
||||||
|
window.$chatwoot.reset();
|
||||||
|
window.$chatwoot.toggleBubbleVisibility('hide');
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
React.useEffect(
|
||||||
|
function initiateUser() {
|
||||||
|
if (!currentUser?.id || !ready) return;
|
||||||
|
window.$chatwoot.setUser(currentUser.id, {
|
||||||
|
email: currentUser.email,
|
||||||
|
name: currentUser.fullName,
|
||||||
|
});
|
||||||
|
if (!matchSmallScreens) {
|
||||||
|
window.$chatwoot.toggleBubbleVisibility('show');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[currentUser, ready, matchSmallScreens],
|
||||||
|
);
|
||||||
|
React.useLayoutEffect(
|
||||||
|
function hideChatwoot() {
|
||||||
|
if (matchSmallScreens) {
|
||||||
|
window.$chatwoot?.toggleBubbleVisibility('hide');
|
||||||
|
} else {
|
||||||
|
window.$chatwoot?.toggleBubbleVisibility('show');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[matchSmallScreens],
|
||||||
|
);
|
||||||
|
return <React.Fragment />;
|
||||||
|
};
|
||||||
|
export default Chatwoot;
|
@@ -1,55 +0,0 @@
|
|||||||
import * as React from 'react';
|
|
||||||
import { useTheme } from '@mui/material/styles';
|
|
||||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
|
||||||
|
|
||||||
import appConfig from 'config/app';
|
|
||||||
import useCurrentUser from 'hooks/useCurrentUser';
|
|
||||||
|
|
||||||
type ChatwootProps = {
|
|
||||||
ready: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Chatwoot = ({ ready }: ChatwootProps) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
const currentUser = useCurrentUser();
|
|
||||||
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('md'));
|
|
||||||
|
|
||||||
React.useEffect(function initiateChatwoot() {
|
|
||||||
window.chatwootSDK.run({
|
|
||||||
websiteToken: 'EFyq5MTsvS7XwUrwSH36VskT',
|
|
||||||
baseUrl: appConfig.chatwootBaseUrl,
|
|
||||||
});
|
|
||||||
|
|
||||||
return function removeChatwoot() {
|
|
||||||
window.$chatwoot.reset();
|
|
||||||
window.$chatwoot.toggleBubbleVisibility('hide');
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
React.useEffect(function initiateUser() {
|
|
||||||
if (!currentUser?.id || !ready) return;
|
|
||||||
|
|
||||||
window.$chatwoot.setUser(currentUser.id, {
|
|
||||||
email: currentUser.email,
|
|
||||||
name: currentUser.fullName,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!matchSmallScreens) {
|
|
||||||
window.$chatwoot.toggleBubbleVisibility("show");
|
|
||||||
}
|
|
||||||
}, [currentUser, ready, matchSmallScreens]);
|
|
||||||
|
|
||||||
React.useLayoutEffect(function hideChatwoot() {
|
|
||||||
if (matchSmallScreens) {
|
|
||||||
window.$chatwoot?.toggleBubbleVisibility('hide');
|
|
||||||
} else {
|
|
||||||
window.$chatwoot?.toggleBubbleVisibility('show');
|
|
||||||
}
|
|
||||||
}, [matchSmallScreens])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<React.Fragment />
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Chatwoot;
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user