Merge pull request #1665 from automatisch/ts-removal-in-web

refactor(web): remove typescript
This commit is contained in:
Ali BARIN
2024-02-29 11:43:07 +01:00
committed by GitHub
337 changed files with 2067 additions and 4997 deletions

View File

@@ -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'],
},
},
],
};

View File

@@ -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

View File

@@ -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",

View File

@@ -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"

View File

@@ -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",

View File

@@ -0,0 +1,3 @@
node_modules
build
source

View File

@@ -0,0 +1,3 @@
module.exports = {
extends: ['react-app', 'prettier'],
};

View File

@@ -0,0 +1,6 @@
{
"compilerOptions": {
"baseUrl": "src"
},
"include": ["src"]
}

View File

@@ -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"
]
} }
} }

View File

@@ -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 (
<> <>

View File

@@ -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;

View File

@@ -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} />
))} ))}

View File

@@ -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',

View File

@@ -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}

View File

@@ -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

View File

@@ -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',

View File

@@ -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;

View File

@@ -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}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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}

View File

@@ -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

View File

@@ -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;

View File

@@ -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>

View File

@@ -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>

View File

@@ -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',

View File

@@ -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' })}
> >

View File

@@ -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;

View File

@@ -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',

View File

@@ -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} />
))} ))}
</> </>

View File

@@ -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} />
))} ))}

View File

@@ -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"

View File

@@ -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;

View File

@@ -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',

View 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} />;
}

View File

@@ -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} />);
};

View File

@@ -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"

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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; }));

View File

@@ -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 }}

View File

@@ -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)} />;
} }

View File

@@ -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; `;

View File

@@ -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>
)} )}

View 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',
};

View File

@@ -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',
};

View File

@@ -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;

View File

@@ -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 }}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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)};
`; `;

View File

@@ -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;

View File

@@ -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,

View File

@@ -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;

View File

@@ -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')}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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,

View File

@@ -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;

View File

@@ -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;

View File

@@ -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`]:

View File

@@ -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"

View File

@@ -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%">

View File

@@ -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

View File

@@ -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',
)} )}
/> />

View File

@@ -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',

View File

@@ -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>

View File

@@ -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; `;

View File

@@ -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

View File

@@ -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}

View File

@@ -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">

View File

@@ -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',

View File

@@ -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}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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>
); );

View File

@@ -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}>

View File

@@ -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} />;
} }

View File

@@ -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 />;
} }

View File

@@ -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={{}}>

View File

@@ -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',

View File

@@ -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;

View File

@@ -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>

View File

@@ -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;

View File

@@ -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',
}, },

View File

@@ -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

View 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>
);
}

View File

@@ -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>
);
}

View File

@@ -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;

View 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;

View File

@@ -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