Compare commits
16 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
579638f932 | ||
![]() |
48871c82a6 | ||
![]() |
14056c42ef | ||
![]() |
90fe1576de | ||
![]() |
d61cf13985 | ||
![]() |
dfe6dfd0c6 | ||
![]() |
c138c7d0e9 | ||
![]() |
d542be947e | ||
![]() |
c76366e72e | ||
![]() |
75abfda783 | ||
![]() |
f3d8d7d4ad | ||
![]() |
7255eccb22 | ||
![]() |
a0decb70cc | ||
![]() |
532f562495 | ||
![]() |
27e58ae925 | ||
![]() |
4867ffcb4b |
@@ -34,7 +34,6 @@ cd $CURRENT_DIR
|
||||
|
||||
echo "Installing and linking dependencies..."
|
||||
yarn
|
||||
yarn lerna bootstrap
|
||||
|
||||
echo "Migrating database..."
|
||||
cd packages/backend
|
||||
|
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@@ -38,7 +38,7 @@ jobs:
|
||||
cache-dependency-path: yarn.lock
|
||||
- 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: yarn --frozen-lockfile && yarn lerna bootstrap
|
||||
- run: yarn --frozen-lockfile
|
||||
- run: cd packages/backend && yarn start
|
||||
env:
|
||||
ENCRYPTION_KEY: sample_encryption_key
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
cache-dependency-path: yarn.lock
|
||||
- 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: yarn --frozen-lockfile && yarn lerna bootstrap
|
||||
- run: yarn --frozen-lockfile
|
||||
- run: cd packages/backend && yarn start:worker
|
||||
env:
|
||||
ENCRYPTION_KEY: sample_encryption_key
|
||||
@@ -78,7 +78,7 @@ jobs:
|
||||
cache-dependency-path: yarn.lock
|
||||
- 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: yarn --frozen-lockfile && yarn lerna bootstrap
|
||||
- run: yarn --frozen-lockfile
|
||||
- run: cd packages/web && yarn build
|
||||
env:
|
||||
CI: false
|
||||
|
2
.github/workflows/playwright.yml
vendored
2
.github/workflows/playwright.yml
vendored
@@ -59,7 +59,7 @@ jobs:
|
||||
with:
|
||||
node-version: 18
|
||||
- name: Install dependencies
|
||||
run: yarn && yarn lerna bootstrap
|
||||
run: yarn
|
||||
- name: Install Playwright Browsers
|
||||
run: yarn playwright install --with-deps
|
||||
- name: Build Automatisch web
|
||||
|
10
lerna.json
10
lerna.json
@@ -1,13 +1,11 @@
|
||||
{
|
||||
"packages": [
|
||||
"packages/*"
|
||||
],
|
||||
"version": "0.10.0",
|
||||
"packages": ["packages/*"],
|
||||
"version": "0.14.0",
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"command": {
|
||||
"add": {
|
||||
"exact": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
|
||||
}
|
||||
|
@@ -23,7 +23,7 @@
|
||||
"eslint": "^8.13.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"lerna": "^4.0.0",
|
||||
"lerna": "^8.0.0",
|
||||
"prettier": "^2.5.1"
|
||||
},
|
||||
"publishConfig": {
|
||||
|
@@ -8,7 +8,7 @@ export default {
|
||||
key: 'instanceUrl',
|
||||
label: 'WordPress instance URL',
|
||||
type: 'string',
|
||||
required: false,
|
||||
required: true,
|
||||
readOnly: false,
|
||||
value: null,
|
||||
placeholder: null,
|
||||
|
@@ -52,7 +52,7 @@ const appConfig = {
|
||||
isDev: appEnv === 'development',
|
||||
isTest: appEnv === 'test',
|
||||
isProd: appEnv === 'production',
|
||||
version: '0.13.1',
|
||||
version: '0.14.0',
|
||||
postgresDatabase: process.env.POSTGRES_DATABASE || 'automatisch_development',
|
||||
postgresSchema: process.env.POSTGRES_SCHEMA || 'public',
|
||||
postgresPort: parseInt(process.env.POSTGRES_PORT || '5432'),
|
||||
|
@@ -10,7 +10,7 @@ describe('GET /api/v1/automatisch/version', () => {
|
||||
|
||||
const expectedPayload = {
|
||||
data: {
|
||||
version: '0.13.1',
|
||||
version: '0.14.0',
|
||||
},
|
||||
meta: {
|
||||
count: 1,
|
||||
|
@@ -1,8 +1,9 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { vi, describe, it, expect } from 'vitest';
|
||||
import SamlAuthProvider from '../models/saml-auth-provider.ee';
|
||||
import SamlAuthProvidersRoleMapping from '../models/saml-auth-providers-role-mapping.ee';
|
||||
import Identity from './identity.ee';
|
||||
import Base from './base';
|
||||
import appConfig from '../config/app';
|
||||
|
||||
describe('SamlAuthProvider model', () => {
|
||||
it('tableName should return correct name', () => {
|
||||
@@ -45,4 +46,39 @@ describe('SamlAuthProvider model', () => {
|
||||
|
||||
expect(virtualAttributes).toStrictEqual(expectedAttributes);
|
||||
});
|
||||
|
||||
it('loginUrl should return the URL of login', () => {
|
||||
const samlAuthProvider = new SamlAuthProvider();
|
||||
samlAuthProvider.issuer = 'sample-issuer';
|
||||
|
||||
vi.spyOn(appConfig, 'baseUrl', 'get').mockReturnValue(
|
||||
'https://automatisch.io'
|
||||
);
|
||||
|
||||
expect(samlAuthProvider.loginUrl).toStrictEqual(
|
||||
'https://automatisch.io/login/saml/sample-issuer'
|
||||
);
|
||||
});
|
||||
|
||||
it('loginCallbackUrl should return the URL of login callback', () => {
|
||||
const samlAuthProvider = new SamlAuthProvider();
|
||||
samlAuthProvider.issuer = 'sample-issuer';
|
||||
|
||||
vi.spyOn(appConfig, 'baseUrl', 'get').mockReturnValue(
|
||||
'https://automatisch.io'
|
||||
);
|
||||
|
||||
expect(samlAuthProvider.loginCallBackUrl).toStrictEqual(
|
||||
'https://automatisch.io/login/saml/sample-issuer/callback'
|
||||
);
|
||||
});
|
||||
|
||||
it('remoteLogoutUrl should return the URL from entrypoint', () => {
|
||||
const samlAuthProvider = new SamlAuthProvider();
|
||||
samlAuthProvider.entryPoint = 'https://example.com/saml/logout';
|
||||
|
||||
expect(samlAuthProvider.remoteLogoutUrl).toStrictEqual(
|
||||
'https://example.com/saml/logout'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@@ -580,4 +580,28 @@ describe('User model', () => {
|
||||
expect(refetchedUser.invitationTokenSentAt).toBe(null);
|
||||
expect(refetchedUser.status).toBe('active');
|
||||
});
|
||||
|
||||
describe('updatePassword', () => {
|
||||
it('should update password when the given current password matches with the user password', async () => {
|
||||
const user = await createUser({ password: 'sample-password' });
|
||||
|
||||
const updatedUser = await user.updatePassword({
|
||||
currentPassword: 'sample-password',
|
||||
password: 'new-password',
|
||||
});
|
||||
|
||||
expect(await updatedUser.login('new-password')).toBe(true);
|
||||
});
|
||||
|
||||
it('should throw validation error when the given current password does not match with the user password', async () => {
|
||||
const user = await createUser({ password: 'sample-password' });
|
||||
|
||||
await expect(
|
||||
user.updatePassword({
|
||||
currentPassword: 'wrong-password',
|
||||
password: 'new-password',
|
||||
})
|
||||
).rejects.toThrowError('currentPassword: is incorrect.');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -112,7 +112,7 @@ export default function ResetPasswordForm() {
|
||||
<Alert
|
||||
data-test="accept-invitation-form-error"
|
||||
severity="error"
|
||||
sx={{ mt: 1, fontWeight: 500 }}
|
||||
sx={{ mt: 1 }}
|
||||
>
|
||||
{formatMessage('acceptInvitationForm.invalidToken')}
|
||||
</Alert>
|
||||
|
@@ -126,7 +126,7 @@ function AddAppConnection(props) {
|
||||
</DialogTitle>
|
||||
|
||||
{authDocUrl && (
|
||||
<Alert severity="info" sx={{ fontWeight: 300 }}>
|
||||
<Alert severity="info">
|
||||
{formatMessage('addAppConnection.callToDocs', {
|
||||
appName: name,
|
||||
docsLink: generateExternalLink(authDocUrl),
|
||||
@@ -138,7 +138,7 @@ function AddAppConnection(props) {
|
||||
<Alert
|
||||
data-test="add-connection-error"
|
||||
severity="error"
|
||||
sx={{ mt: 1, fontWeight: 500, wordBreak: 'break-all' }}
|
||||
sx={{ mt: 1, wordBreak: 'break-all' }}
|
||||
>
|
||||
{!errorDetails && errorMessage}
|
||||
{errorDetails && (
|
||||
|
@@ -32,10 +32,7 @@ function AdminApplicationAuthClientDialog(props) {
|
||||
<Dialog open={true} onClose={onClose}>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
{error && (
|
||||
<Alert
|
||||
severity="error"
|
||||
sx={{ mt: 1, fontWeight: 500, wordBreak: 'break-all' }}
|
||||
>
|
||||
<Alert severity="error" sx={{ mt: 1, wordBreak: 'break-all' }}>
|
||||
{error.message}
|
||||
</Alert>
|
||||
)}
|
||||
|
@@ -188,7 +188,7 @@ function InstallationForm() {
|
||||
)}
|
||||
/>
|
||||
{install.isSuccess && (
|
||||
<Alert data-test="success-alert" severity="success" sx={{ mt: 3, fontWeight: 500 }}>
|
||||
<Alert data-test="success-alert" severity="success" sx={{ mt: 3 }}>
|
||||
{formatMessage('installationForm.success', {
|
||||
link: (str) => (
|
||||
<Link
|
||||
|
@@ -11,16 +11,19 @@ import Form from 'components/Form';
|
||||
import TextField from 'components/TextField';
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
import useCreateAccessToken from 'hooks/useCreateAccessToken';
|
||||
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
|
||||
import { Alert } from '@mui/material';
|
||||
|
||||
function LoginForm() {
|
||||
const isCloud = useCloud();
|
||||
const navigate = useNavigate();
|
||||
const formatMessage = useFormatMessage();
|
||||
const enqueueSnackbar = useEnqueueSnackbar();
|
||||
const authentication = useAuthentication();
|
||||
const { mutateAsync: createAccessToken, isPending: loading } =
|
||||
useCreateAccessToken();
|
||||
const {
|
||||
mutateAsync: createAccessToken,
|
||||
isPending: loading,
|
||||
error,
|
||||
isError,
|
||||
} = useCreateAccessToken();
|
||||
|
||||
React.useEffect(() => {
|
||||
if (authentication.isAuthenticated) {
|
||||
@@ -37,23 +40,19 @@ function LoginForm() {
|
||||
});
|
||||
const { token } = data;
|
||||
authentication.updateToken(token);
|
||||
} catch (error) {
|
||||
const errors = error?.response?.data?.errors
|
||||
? Object.values(error.response.data.errors)
|
||||
: [];
|
||||
} catch {}
|
||||
};
|
||||
|
||||
if (errors.length) {
|
||||
for (const [error] of errors) {
|
||||
enqueueSnackbar(error, {
|
||||
variant: 'error',
|
||||
});
|
||||
}
|
||||
} else {
|
||||
enqueueSnackbar(error?.message || formatMessage('loginForm.error'), {
|
||||
variant: 'error',
|
||||
});
|
||||
}
|
||||
}
|
||||
const renderError = () => {
|
||||
const errors = error?.response?.data?.errors?.general || [
|
||||
formatMessage('loginForm.error'),
|
||||
];
|
||||
|
||||
return errors.map((error) => (
|
||||
<Alert severity="error" sx={{ mt: 2 }}>
|
||||
{error}
|
||||
</Alert>
|
||||
));
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -106,6 +105,8 @@ function LoginForm() {
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{isError && renderError()}
|
||||
|
||||
<LoadingButton
|
||||
type="submit"
|
||||
variant="contained"
|
||||
|
@@ -7,7 +7,7 @@ import FormControl from '@mui/material/FormControl';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
|
||||
export default function SearchInput({ onChange }) {
|
||||
export default function SearchInput({ onChange, defaultValue = '' }) {
|
||||
const formatMessage = useFormatMessage();
|
||||
return (
|
||||
<FormControl variant="outlined" fullWidth>
|
||||
@@ -16,6 +16,7 @@ export default function SearchInput({ onChange }) {
|
||||
</InputLabel>
|
||||
|
||||
<OutlinedInput
|
||||
defaultValue={defaultValue}
|
||||
id="search-input"
|
||||
type="text"
|
||||
size="medium"
|
||||
@@ -34,4 +35,5 @@ export default function SearchInput({ onChange }) {
|
||||
|
||||
SearchInput.propTypes = {
|
||||
onChange: PropTypes.func,
|
||||
defaultValue: PropTypes.string,
|
||||
};
|
||||
|
@@ -84,10 +84,7 @@ function TestSubstep(props) {
|
||||
}}
|
||||
>
|
||||
{hasError && (
|
||||
<Alert
|
||||
severity="error"
|
||||
sx={{ mb: 2, fontWeight: 500, width: '100%' }}
|
||||
>
|
||||
<Alert severity="error" sx={{ mb: 2, width: '100%' }}>
|
||||
<pre style={{ margin: 0, whiteSpace: 'pre-wrap' }}>
|
||||
{JSON.stringify(errorDetails, null, 2)}
|
||||
</pre>
|
||||
@@ -104,13 +101,11 @@ function TestSubstep(props) {
|
||||
severity="warning"
|
||||
sx={{ mb: 1, width: '100%' }}
|
||||
>
|
||||
<AlertTitle sx={{ fontWeight: 700 }}>
|
||||
<AlertTitle>
|
||||
{formatMessage('flowEditor.noTestDataTitle')}
|
||||
</AlertTitle>
|
||||
|
||||
<Box sx={{ fontWeight: 400 }}>
|
||||
{formatMessage('flowEditor.noTestDataMessage')}
|
||||
</Box>
|
||||
<Box>{formatMessage('flowEditor.noTestDataMessage')}</Box>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
|
@@ -124,7 +124,6 @@ export default function CreateUser() {
|
||||
<Alert
|
||||
severity="info"
|
||||
color="primary"
|
||||
sx={{ fontWeight: '500' }}
|
||||
data-test="invitation-email-info-alert"
|
||||
>
|
||||
{formatMessage('createUser.invitationEmailInfo', {
|
||||
|
@@ -42,13 +42,9 @@ export default function Execution() {
|
||||
<Grid container item sx={{ mt: 2, mb: [2, 5] }} rowGap={3}>
|
||||
{!isExecutionStepsLoading && !data?.pages?.[0].data.length && (
|
||||
<Alert severity="warning" sx={{ flex: 1 }}>
|
||||
<AlertTitle sx={{ fontWeight: 700 }}>
|
||||
{formatMessage('execution.noDataTitle')}
|
||||
</AlertTitle>
|
||||
<AlertTitle>{formatMessage('execution.noDataTitle')}</AlertTitle>
|
||||
|
||||
<Box sx={{ fontWeight: 400 }}>
|
||||
{formatMessage('execution.noDataMessage')}
|
||||
</Box>
|
||||
<Box>{formatMessage('execution.noDataMessage')}</Box>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { Link, useSearchParams } from 'react-router-dom';
|
||||
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import debounce from 'lodash/debounce';
|
||||
import Box from '@mui/material/Box';
|
||||
import Grid from '@mui/material/Grid';
|
||||
@@ -23,13 +23,18 @@ import useLazyFlows from 'hooks/useLazyFlows';
|
||||
|
||||
export default function Flows() {
|
||||
const formatMessage = useFormatMessage();
|
||||
const navigate = useNavigate();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const page = parseInt(searchParams.get('page') || '', 10) || 1;
|
||||
const [flowName, setFlowName] = React.useState('');
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
const flowName = searchParams.get('flowName') || '';
|
||||
const [isLoading, setIsLoading] = React.useState(true);
|
||||
const currentUserAbility = useCurrentUserAbility();
|
||||
|
||||
const { data, mutate: fetchFlows } = useLazyFlows(
|
||||
const {
|
||||
data,
|
||||
mutate: fetchFlows,
|
||||
isSuccess,
|
||||
} = useLazyFlows(
|
||||
{ flowName, page },
|
||||
{
|
||||
onSettled: () => {
|
||||
@@ -38,6 +43,36 @@ export default function Flows() {
|
||||
},
|
||||
);
|
||||
|
||||
const flows = data?.data || [];
|
||||
const pageInfo = data?.meta;
|
||||
const hasFlows = flows?.length;
|
||||
const navigateToLastPage = isSuccess && !hasFlows && page > 1;
|
||||
|
||||
const onSearchChange = React.useCallback((event) => {
|
||||
setSearchParams({ flowName: event.target.value });
|
||||
}, []);
|
||||
|
||||
const getPathWithSearchParams = (page, flowName) => {
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (page > 1) {
|
||||
searchParams.set('page', page);
|
||||
}
|
||||
if (flowName) {
|
||||
searchParams.set('flowName', flowName);
|
||||
}
|
||||
|
||||
return { search: searchParams.toString() };
|
||||
};
|
||||
|
||||
const onDuplicateFlow = () => {
|
||||
if (pageInfo?.currentPage > 1) {
|
||||
navigate(getPathWithSearchParams(1, flowName));
|
||||
} else {
|
||||
fetchFlows();
|
||||
}
|
||||
};
|
||||
|
||||
const fetchData = React.useMemo(
|
||||
() => debounce(fetchFlows, 300),
|
||||
[fetchFlows],
|
||||
@@ -54,21 +89,14 @@ export default function Flows() {
|
||||
}, [fetchData, flowName, page]);
|
||||
|
||||
React.useEffect(
|
||||
function resetPageOnSearch() {
|
||||
// reset search params which only consists of `page`
|
||||
setSearchParams({});
|
||||
function redirectToLastPage() {
|
||||
if (navigateToLastPage) {
|
||||
navigate(getPathWithSearchParams(pageInfo.totalPages, flowName));
|
||||
}
|
||||
},
|
||||
[flowName],
|
||||
[navigateToLastPage],
|
||||
);
|
||||
|
||||
const flows = data?.data || [];
|
||||
const pageInfo = data?.meta;
|
||||
const hasFlows = flows?.length;
|
||||
|
||||
const onSearchChange = React.useCallback((event) => {
|
||||
setFlowName(event.target.value);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box sx={{ py: 3 }}>
|
||||
<Container>
|
||||
@@ -78,7 +106,7 @@ export default function Flows() {
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} sm="auto" order={{ xs: 2, sm: 1 }}>
|
||||
<SearchInput onChange={onSearchChange} />
|
||||
<SearchInput onChange={onSearchChange} defaultValue={flowName} />
|
||||
</Grid>
|
||||
|
||||
<Grid
|
||||
@@ -111,7 +139,7 @@ export default function Flows() {
|
||||
</Grid>
|
||||
|
||||
<Divider sx={{ mt: [2, 0], mb: 2 }} />
|
||||
{isLoading && (
|
||||
{(isLoading || navigateToLastPage) && (
|
||||
<CircularProgress sx={{ display: 'block', margin: '20px auto' }} />
|
||||
)}
|
||||
{!isLoading &&
|
||||
@@ -119,11 +147,11 @@ export default function Flows() {
|
||||
<FlowRow
|
||||
key={flow.id}
|
||||
flow={flow}
|
||||
onDuplicateFlow={fetchFlows}
|
||||
onDuplicateFlow={onDuplicateFlow}
|
||||
onDeleteFlow={fetchFlows}
|
||||
/>
|
||||
))}
|
||||
{!isLoading && !hasFlows && (
|
||||
{!isLoading && !navigateToLastPage && !hasFlows && (
|
||||
<NoResultFound
|
||||
text={formatMessage('flows.noFlows')}
|
||||
{...(currentUserAbility.can('create', 'Flow') && {
|
||||
@@ -131,23 +159,23 @@ export default function Flows() {
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
{!isLoading && pageInfo && pageInfo.totalPages > 1 && (
|
||||
<Pagination
|
||||
sx={{ display: 'flex', justifyContent: 'center', mt: 3 }}
|
||||
page={pageInfo?.currentPage}
|
||||
count={pageInfo?.totalPages}
|
||||
onChange={(event, page) =>
|
||||
setSearchParams({ page: page.toString() })
|
||||
}
|
||||
renderItem={(item) => (
|
||||
<PaginationItem
|
||||
component={Link}
|
||||
to={`${item.page === 1 ? '' : `?page=${item.page}`}`}
|
||||
{...item}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{!isLoading &&
|
||||
!navigateToLastPage &&
|
||||
pageInfo &&
|
||||
pageInfo.totalPages > 1 && (
|
||||
<Pagination
|
||||
sx={{ display: 'flex', justifyContent: 'center', mt: 3 }}
|
||||
page={pageInfo?.currentPage}
|
||||
count={pageInfo?.totalPages}
|
||||
renderItem={(item) => (
|
||||
<PaginationItem
|
||||
component={Link}
|
||||
to={getPathWithSearchParams(item.page, flowName)}
|
||||
{...item}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
|
@@ -266,8 +266,8 @@ function ProfileSettings() {
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} justifyContent="flex-end" sx={{ pt: 5 }}>
|
||||
<Alert variant="outlined" severity="error" sx={{ fontWeight: 500 }}>
|
||||
<AlertTitle sx={{ fontWeight: 700 }}>
|
||||
<Alert variant="outlined" severity="error">
|
||||
<AlertTitle>
|
||||
{formatMessage('profileSettings.deleteMyAccount')}
|
||||
</AlertTitle>
|
||||
|
||||
|
@@ -278,6 +278,20 @@ export const defaultTheme = createTheme({
|
||||
}),
|
||||
},
|
||||
},
|
||||
MuiAlert: {
|
||||
styleOverrides: {
|
||||
root: ({ theme }) => ({
|
||||
fontWeight: theme.typography.fontWeightRegular,
|
||||
}),
|
||||
},
|
||||
},
|
||||
MuiAlertTitle: {
|
||||
styleOverrides: {
|
||||
root: ({ theme }) => ({
|
||||
fontWeight: theme.typography.fontWeightBold,
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
export const mationTheme = createTheme(
|
||||
|
Reference in New Issue
Block a user