Merge pull request #1964 from automatisch/AUT-1095
feat: create onboarding UX flow
This commit is contained in:
208
packages/web/src/components/InstallationForm/index.jsx
Normal file
208
packages/web/src/components/InstallationForm/index.jsx
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { Link as RouterLink } from 'react-router-dom';
|
||||||
|
import Paper from '@mui/material/Paper';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import { Alert } from '@mui/material';
|
||||||
|
import LoadingButton from '@mui/lab/LoadingButton';
|
||||||
|
import * as yup from 'yup';
|
||||||
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
|
import { enqueueSnackbar } from 'notistack';
|
||||||
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
import Link from '@mui/material/Link';
|
||||||
|
|
||||||
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
|
import useInstallation from 'hooks/useInstallation';
|
||||||
|
import * as URLS from 'config/urls';
|
||||||
|
import Form from 'components/Form';
|
||||||
|
import TextField from 'components/TextField';
|
||||||
|
|
||||||
|
const validationSchema = yup.object().shape({
|
||||||
|
fullName: yup.string().trim().required('installationForm.mandatoryInput'),
|
||||||
|
email: yup
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.email('installationForm.validateEmail')
|
||||||
|
.required('installationForm.mandatoryInput'),
|
||||||
|
password: yup.string().required('installationForm.mandatoryInput'),
|
||||||
|
confirmPassword: yup
|
||||||
|
.string()
|
||||||
|
.required('installationForm.mandatoryInput')
|
||||||
|
.oneOf([yup.ref('password')], 'installationForm.passwordsMustMatch'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const initialValues = {
|
||||||
|
fullName: '',
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
confirmPassword: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
function InstallationForm() {
|
||||||
|
const formatMessage = useFormatMessage();
|
||||||
|
const install = useInstallation();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const handleOnRedirect = () => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ['automatisch', 'config'],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (values) => {
|
||||||
|
const { fullName, email, password } = values;
|
||||||
|
try {
|
||||||
|
await install.mutateAsync({
|
||||||
|
fullName,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
enqueueSnackbar(
|
||||||
|
error?.message || formatMessage('installationForm.error'),
|
||||||
|
{
|
||||||
|
variant: 'error',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper sx={{ px: 2, py: 4 }}>
|
||||||
|
<Typography
|
||||||
|
variant="h3"
|
||||||
|
align="center"
|
||||||
|
sx={{
|
||||||
|
borderBottom: '1px solid',
|
||||||
|
borderColor: (theme) => theme.palette.text.disabled,
|
||||||
|
pb: 2,
|
||||||
|
mb: 2,
|
||||||
|
}}
|
||||||
|
gutterBottom
|
||||||
|
>
|
||||||
|
{formatMessage('installationForm.title')}
|
||||||
|
</Typography>
|
||||||
|
<Form
|
||||||
|
defaultValues={initialValues}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
resolver={yupResolver(validationSchema)}
|
||||||
|
mode="onChange"
|
||||||
|
render={({ formState: { errors, touchedFields } }) => (
|
||||||
|
<>
|
||||||
|
<TextField
|
||||||
|
label={formatMessage('installationForm.fullNameFieldLabel')}
|
||||||
|
name="fullName"
|
||||||
|
fullWidth
|
||||||
|
margin="dense"
|
||||||
|
autoComplete="fullName"
|
||||||
|
data-test="fullName-text-field"
|
||||||
|
error={touchedFields.fullName && !!errors?.fullName}
|
||||||
|
helperText={
|
||||||
|
touchedFields.fullName && errors?.fullName?.message
|
||||||
|
? formatMessage(errors?.fullName?.message, {
|
||||||
|
inputName: formatMessage(
|
||||||
|
'installationForm.fullNameFieldLabel',
|
||||||
|
),
|
||||||
|
})
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
required
|
||||||
|
readOnly={install.isSuccess}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label={formatMessage('installationForm.emailFieldLabel')}
|
||||||
|
name="email"
|
||||||
|
fullWidth
|
||||||
|
margin="dense"
|
||||||
|
autoComplete="email"
|
||||||
|
data-test="email-text-field"
|
||||||
|
error={touchedFields.email && !!errors?.email}
|
||||||
|
helperText={
|
||||||
|
touchedFields.email && errors?.email?.message
|
||||||
|
? formatMessage(errors?.email?.message, {
|
||||||
|
inputName: formatMessage(
|
||||||
|
'installationForm.emailFieldLabel',
|
||||||
|
),
|
||||||
|
})
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
required
|
||||||
|
readOnly={install.isSuccess}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label={formatMessage('installationForm.passwordFieldLabel')}
|
||||||
|
name="password"
|
||||||
|
fullWidth
|
||||||
|
margin="dense"
|
||||||
|
type="password"
|
||||||
|
error={touchedFields.password && !!errors?.password}
|
||||||
|
helperText={
|
||||||
|
touchedFields.password && errors?.password?.message
|
||||||
|
? formatMessage(errors?.password?.message, {
|
||||||
|
inputName: formatMessage(
|
||||||
|
'installationForm.passwordFieldLabel',
|
||||||
|
),
|
||||||
|
})
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
required
|
||||||
|
readOnly={install.isSuccess}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label={formatMessage(
|
||||||
|
'installationForm.confirmPasswordFieldLabel',
|
||||||
|
)}
|
||||||
|
name="confirmPassword"
|
||||||
|
fullWidth
|
||||||
|
margin="dense"
|
||||||
|
type="password"
|
||||||
|
error={touchedFields.confirmPassword && !!errors?.confirmPassword}
|
||||||
|
helperText={
|
||||||
|
touchedFields.confirmPassword &&
|
||||||
|
errors?.confirmPassword?.message
|
||||||
|
? formatMessage(errors?.confirmPassword?.message, {
|
||||||
|
inputName: formatMessage(
|
||||||
|
'installationForm.confirmPasswordFieldLabel',
|
||||||
|
),
|
||||||
|
})
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
required
|
||||||
|
readOnly={install.isSuccess}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LoadingButton
|
||||||
|
type="submit"
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
sx={{ boxShadow: 2, mt: 3 }}
|
||||||
|
loading={install.isPending}
|
||||||
|
disabled={install.isSuccess}
|
||||||
|
fullWidth
|
||||||
|
data-test="signUp-button"
|
||||||
|
>
|
||||||
|
{formatMessage('installationForm.submit')}
|
||||||
|
</LoadingButton>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{install.isSuccess && (
|
||||||
|
<Alert severity="success" sx={{ mt: 3, fontWeight: 500 }}>
|
||||||
|
{formatMessage('installationForm.success', {
|
||||||
|
link: (str) => (
|
||||||
|
<Link
|
||||||
|
component={RouterLink}
|
||||||
|
to={URLS.LOGIN}
|
||||||
|
onClick={handleOnRedirect}
|
||||||
|
replace
|
||||||
|
>
|
||||||
|
{str}
|
||||||
|
</Link>
|
||||||
|
),
|
||||||
|
})}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default InstallationForm;
|
@@ -8,6 +8,7 @@ export const SIGNUP = '/sign-up';
|
|||||||
export const ACCEPT_INVITATON = '/accept-invitation';
|
export const ACCEPT_INVITATON = '/accept-invitation';
|
||||||
export const FORGOT_PASSWORD = '/forgot-password';
|
export const FORGOT_PASSWORD = '/forgot-password';
|
||||||
export const RESET_PASSWORD = '/reset-password';
|
export const RESET_PASSWORD = '/reset-password';
|
||||||
|
export const INSTALLATION = '/installation';
|
||||||
export const APPS = '/apps';
|
export const APPS = '/apps';
|
||||||
export const NEW_APP_CONNECTION = '/apps/new';
|
export const NEW_APP_CONNECTION = '/apps/new';
|
||||||
export const APP = (appKey) => `/app/${appKey}`;
|
export const APP = (appKey) => `/app/${appKey}`;
|
||||||
|
@@ -1,10 +1,12 @@
|
|||||||
import { Link as RouterLink } from 'react-router-dom';
|
import { Link as RouterLink } from 'react-router-dom';
|
||||||
import Link from '@mui/material/Link';
|
import Link from '@mui/material/Link';
|
||||||
|
|
||||||
export const generateInternalLink = (link) => (str) => (
|
export const generateInternalLink = (link) => (str) => (
|
||||||
<Link component={RouterLink} to={link}>
|
<Link component={RouterLink} to={link}>
|
||||||
{str}
|
{str}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const generateExternalLink = (link) => (str) => (
|
export const generateExternalLink = (link) => (str) => (
|
||||||
<Link href={link} target="_blank">
|
<Link href={link} target="_blank">
|
||||||
{str}
|
{str}
|
||||||
|
15
packages/web/src/hooks/useInstallation.js
Normal file
15
packages/web/src/hooks/useInstallation.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { useMutation } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import api from 'helpers/api';
|
||||||
|
|
||||||
|
export default function useInstallation() {
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: async (payload) => {
|
||||||
|
const { data } = await api.post('/v1/installation/users', payload);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return mutation;
|
||||||
|
}
|
@@ -124,6 +124,17 @@
|
|||||||
"webhookUrlInfo.description": "You'll need to configure your application with this webhook URL.",
|
"webhookUrlInfo.description": "You'll need to configure your application with this webhook URL.",
|
||||||
"webhookUrlInfo.helperText": "We've generated a custom webhook URL for you to send requests to. <link>Learn more about webhooks</link>.",
|
"webhookUrlInfo.helperText": "We've generated a custom webhook URL for you to send requests to. <link>Learn more about webhooks</link>.",
|
||||||
"webhookUrlInfo.copy": "Copy",
|
"webhookUrlInfo.copy": "Copy",
|
||||||
|
"installationForm.title": "Installation",
|
||||||
|
"installationForm.fullNameFieldLabel": "Full name",
|
||||||
|
"installationForm.emailFieldLabel": "Email",
|
||||||
|
"installationForm.passwordFieldLabel": "Password",
|
||||||
|
"installationForm.confirmPasswordFieldLabel": "Confirm password",
|
||||||
|
"installationForm.submit": "Create admin",
|
||||||
|
"installationForm.validateEmail": "Email must be valid.",
|
||||||
|
"installationForm.passwordsMustMatch": "Passwords must match.",
|
||||||
|
"installationForm.mandatoryInput": "{inputName} is required.",
|
||||||
|
"installationForm.success": "The admin account has been created, and thus, the installation has been completed. You can now log in <link>here</link>.",
|
||||||
|
"installationForm.error": "Something went wrong. Please try again.",
|
||||||
"signupForm.title": "Sign up",
|
"signupForm.title": "Sign up",
|
||||||
"signupForm.fullNameFieldLabel": "Full name",
|
"signupForm.fullNameFieldLabel": "Full name",
|
||||||
"signupForm.emailFieldLabel": "Email",
|
"signupForm.emailFieldLabel": "Email",
|
||||||
|
21
packages/web/src/pages/Installation/index.jsx
Normal file
21
packages/web/src/pages/Installation/index.jsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import Stack from '@mui/material/Stack';
|
||||||
|
|
||||||
|
import Container from 'components/Container';
|
||||||
|
import InstallationForm from 'components/InstallationForm';
|
||||||
|
|
||||||
|
export default function Installation() {
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
flexDirection="column"
|
||||||
|
py={3}
|
||||||
|
flex={1}
|
||||||
|
>
|
||||||
|
<Container maxWidth="sm">
|
||||||
|
<InstallationForm />
|
||||||
|
</Container>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
@@ -1,4 +1,10 @@
|
|||||||
import { Route, Routes as ReactRouterRoutes, Navigate } from 'react-router-dom';
|
import { useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
Route,
|
||||||
|
Routes as ReactRouterRoutes,
|
||||||
|
Navigate,
|
||||||
|
useNavigate,
|
||||||
|
} from 'react-router-dom';
|
||||||
|
|
||||||
import Layout from 'components/Layout';
|
import Layout from 'components/Layout';
|
||||||
import NoResultFound from 'components/NotFound';
|
import NoResultFound from 'components/NotFound';
|
||||||
@@ -23,12 +29,22 @@ import adminSettingsRoutes from './adminSettingsRoutes';
|
|||||||
import Notifications from 'pages/Notifications';
|
import Notifications from 'pages/Notifications';
|
||||||
import useAutomatischConfig from 'hooks/useAutomatischConfig';
|
import useAutomatischConfig from 'hooks/useAutomatischConfig';
|
||||||
import useAuthentication from 'hooks/useAuthentication';
|
import useAuthentication from 'hooks/useAuthentication';
|
||||||
|
import Installation from 'pages/Installation';
|
||||||
|
|
||||||
function Routes() {
|
function Routes() {
|
||||||
const { data: configData } = useAutomatischConfig();
|
const { data: configData } = useAutomatischConfig();
|
||||||
const { isAuthenticated } = useAuthentication();
|
const { isAuthenticated } = useAuthentication();
|
||||||
const config = configData?.data;
|
const config = configData?.data;
|
||||||
|
|
||||||
|
const installed = configData?.data?.['installation.completed'] === true;
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!installed) {
|
||||||
|
navigate(URLS.INSTALLATION, { replace: true });
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactRouterRoutes>
|
<ReactRouterRoutes>
|
||||||
<Route
|
<Route
|
||||||
@@ -134,6 +150,17 @@ function Routes() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{!installed && (
|
||||||
|
<Route
|
||||||
|
path={URLS.INSTALLATION}
|
||||||
|
element={
|
||||||
|
<PublicLayout>
|
||||||
|
<Installation />
|
||||||
|
</PublicLayout>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{!config?.disableNotificationsPage && (
|
{!config?.disableNotificationsPage && (
|
||||||
<Route
|
<Route
|
||||||
path={URLS.UPDATES}
|
path={URLS.UPDATES}
|
||||||
|
Reference in New Issue
Block a user