mirror of
https://github.com/strapi/strapi.git
synced 2025-08-11 02:07:51 +00:00
Merge fix
This commit is contained in:
commit
65e29e5f61
@ -0,0 +1,94 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { Loader } from '@strapi/design-system/Loader';
|
||||
import { Grid } from '@strapi/design-system/Grid';
|
||||
import { Box } from '@strapi/design-system/Box';
|
||||
import { Flex } from '@strapi/design-system/Flex';
|
||||
import { Typography } from '@strapi/design-system/Typography';
|
||||
import { pxToRem } from '@strapi/helper-plugin';
|
||||
import getDefaultMessage from '../utils/getActionTypesDefaultMessages';
|
||||
import ActionItem from './ActionItem';
|
||||
|
||||
const ActionBody = ({ status, data, formattedDate }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
if (status === 'loading') {
|
||||
return (
|
||||
<Flex padding={7} justifyContent="center" alignItems="center">
|
||||
<Loader />
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
const { action, user, payload } = data;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box marginBottom={pxToRem(12)}>
|
||||
<Typography variant="delta" id="title">
|
||||
{formatMessage({
|
||||
id: 'Settings.permissions.auditLogs.details',
|
||||
defaultMessage: 'Log Details',
|
||||
})}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Grid
|
||||
gap={4}
|
||||
gridCols={2}
|
||||
paddingTop={4}
|
||||
paddingBottom={4}
|
||||
paddingLeft={6}
|
||||
paddingRight={6}
|
||||
background="neutral100"
|
||||
hasRadius
|
||||
>
|
||||
<ActionItem
|
||||
actionLabel={formatMessage({
|
||||
id: 'Settings.permissions.auditLogs.action',
|
||||
defaultMessage: 'Action',
|
||||
})}
|
||||
actionName={formatMessage({
|
||||
id: `Settings.permissions.auditLogs.${action}`,
|
||||
defaultMessage: getDefaultMessage(action),
|
||||
})}
|
||||
/>
|
||||
<ActionItem
|
||||
actionLabel={formatMessage({
|
||||
id: 'Settings.permissions.auditLogs.date',
|
||||
defaultMessage: 'Date',
|
||||
})}
|
||||
actionName={formattedDate}
|
||||
/>
|
||||
<ActionItem
|
||||
actionLabel={formatMessage({
|
||||
id: 'Settings.permissions.auditLogs.user',
|
||||
defaultMessage: 'User',
|
||||
})}
|
||||
actionName={user ? user.fullname : '-'}
|
||||
/>
|
||||
</Grid>
|
||||
{/* TODO remove when adding JSON component */}
|
||||
<Box as="pre" marginTop={4}>
|
||||
<Typography>{JSON.stringify(payload, null, 2)}</Typography>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
ActionBody.defaultProps = {
|
||||
data: {},
|
||||
};
|
||||
|
||||
ActionBody.propTypes = {
|
||||
status: PropTypes.oneOf(['idle', 'loading', 'error', 'success']).isRequired,
|
||||
data: PropTypes.shape({
|
||||
action: PropTypes.string,
|
||||
date: PropTypes.string,
|
||||
payload: PropTypes.object,
|
||||
user: PropTypes.object,
|
||||
}),
|
||||
formattedDate: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default ActionBody;
|
@ -0,0 +1,53 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useQuery } from 'react-query';
|
||||
import { ModalLayout, ModalHeader, ModalBody } from '@strapi/design-system/ModalLayout';
|
||||
import { Breadcrumbs, Crumb } from '@strapi/design-system/Breadcrumbs';
|
||||
import { useNotification } from '@strapi/helper-plugin';
|
||||
import useFormatTimeStamp from '../hooks/useFormatTimeStamp';
|
||||
import { useFetchClient } from '../../../../../../hooks';
|
||||
import ActionBody from './ActionBody';
|
||||
|
||||
const Modal = ({ handleClose, logId }) => {
|
||||
const { get } = useFetchClient();
|
||||
const toggleNotification = useNotification();
|
||||
|
||||
const fetchAuditLog = async (id) => {
|
||||
const { data } = await get(`/admin/audit-logs/${id}`);
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
const { data, status } = useQuery(['audit-log', logId], () => fetchAuditLog(logId), {
|
||||
onError() {
|
||||
toggleNotification({
|
||||
type: 'warning',
|
||||
message: { id: 'notification.error', defaultMessage: 'An error occured' },
|
||||
});
|
||||
handleClose();
|
||||
},
|
||||
});
|
||||
|
||||
const formatTimeStamp = useFormatTimeStamp();
|
||||
const formattedDate = data ? formatTimeStamp(data.date) : '';
|
||||
|
||||
return (
|
||||
<ModalLayout onClose={handleClose} labelledBy="title">
|
||||
<ModalHeader>
|
||||
<Breadcrumbs label={formattedDate} id="title">
|
||||
<Crumb>{formattedDate}</Crumb>
|
||||
</Breadcrumbs>
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<ActionBody status={status} data={data} formattedDate={formattedDate} />
|
||||
</ModalBody>
|
||||
</ModalLayout>
|
||||
);
|
||||
};
|
||||
|
||||
Modal.propTypes = {
|
||||
handleClose: PropTypes.func.isRequired,
|
||||
logId: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
export default Modal;
|
@ -1,80 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { ModalLayout, ModalHeader, ModalBody } from '@strapi/design-system/ModalLayout';
|
||||
import { Breadcrumbs, Crumb } from '@strapi/design-system/Breadcrumbs';
|
||||
import { Grid } from '@strapi/design-system/Grid';
|
||||
import { Box } from '@strapi/design-system/Box';
|
||||
import { Typography } from '@strapi/design-system/Typography';
|
||||
import { pxToRem } from '@strapi/helper-plugin';
|
||||
import getDefaultMessage from '../utils/getActionTypesDefaultMessages';
|
||||
import useFormatTimeStamp from '../hooks/useFormatTimeStamp';
|
||||
import ActionItem from './ActionItem';
|
||||
|
||||
const ModalDialog = ({ onToggle, data: { date, user, action } }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const formatTimeStamp = useFormatTimeStamp();
|
||||
const formattedDate = formatTimeStamp(date);
|
||||
|
||||
return (
|
||||
<ModalLayout onClose={onToggle} labelledBy="title">
|
||||
<ModalHeader>
|
||||
<Breadcrumbs label={formattedDate}>
|
||||
<Crumb>{formattedDate}</Crumb>
|
||||
</Breadcrumbs>
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<Box marginBottom={pxToRem(12)}>
|
||||
<Typography variant="delta" id="title">
|
||||
{formatMessage({
|
||||
id: 'Settings.permissions.auditLogs.details',
|
||||
defaultMessage: 'Log Details',
|
||||
})}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Grid
|
||||
gap={4}
|
||||
gridCols={2}
|
||||
paddingTop={4}
|
||||
paddingBottom={4}
|
||||
paddingLeft={6}
|
||||
paddingRight={6}
|
||||
background="neutral100"
|
||||
hasRadius
|
||||
>
|
||||
<ActionItem
|
||||
actionLabel={formatMessage({
|
||||
id: 'Settings.permissions.auditLogs.action',
|
||||
defaultMessage: 'Action',
|
||||
})}
|
||||
actionName={formatMessage({
|
||||
id: `Settings.permissions.auditLogs.${action}`,
|
||||
defaultMessage: getDefaultMessage(action),
|
||||
})}
|
||||
/>
|
||||
<ActionItem
|
||||
actionLabel={formatMessage({
|
||||
id: 'Settings.permissions.auditLogs.date',
|
||||
defaultMessage: 'Date',
|
||||
})}
|
||||
actionName={formattedDate}
|
||||
/>
|
||||
<ActionItem
|
||||
actionLabel={formatMessage({
|
||||
id: 'Settings.permissions.auditLogs.user',
|
||||
defaultMessage: 'User',
|
||||
})}
|
||||
actionName={user ? user.fullname : '-'}
|
||||
/>
|
||||
</Grid>
|
||||
</ModalBody>
|
||||
</ModalLayout>
|
||||
);
|
||||
};
|
||||
|
||||
ModalDialog.propTypes = {
|
||||
onToggle: PropTypes.func.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default ModalDialog;
|
@ -10,7 +10,7 @@ import { onRowClick, stopPropagation } from '@strapi/helper-plugin';
|
||||
import useFormatTimeStamp from '../hooks/useFormatTimeStamp';
|
||||
import getDefaultMessage from '../utils/getActionTypesDefaultMessages';
|
||||
|
||||
const TableRows = ({ headers, rows, onModalToggle }) => {
|
||||
const TableRows = ({ headers, rows, onOpenModal }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const formatTimeStamp = useFormatTimeStamp();
|
||||
|
||||
@ -36,7 +36,7 @@ const TableRows = ({ headers, rows, onModalToggle }) => {
|
||||
<Tr
|
||||
key={data.id}
|
||||
{...onRowClick({
|
||||
fn: () => onModalToggle(data.id),
|
||||
fn: () => onOpenModal(data.id),
|
||||
})}
|
||||
>
|
||||
{headers.map(({ key, name, cellFormatter }) => {
|
||||
@ -55,7 +55,7 @@ const TableRows = ({ headers, rows, onModalToggle }) => {
|
||||
<Td {...stopPropagation}>
|
||||
<Flex justifyContent="end">
|
||||
<IconButton
|
||||
onClick={() => onModalToggle(data.id)}
|
||||
onClick={() => onOpenModal(data.id)}
|
||||
aria-label={formatMessage(
|
||||
{ id: 'app.component.table.view', defaultMessage: '{target} details' },
|
||||
{ target: `${data.action} action` }
|
||||
@ -79,7 +79,7 @@ TableRows.defaultProps = {
|
||||
TableRows.propTypes = {
|
||||
headers: PropTypes.array.isRequired,
|
||||
rows: PropTypes.array,
|
||||
onModalToggle: PropTypes.func.isRequired,
|
||||
onOpenModal: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default TableRows;
|
||||
|
@ -60,14 +60,14 @@ const rows = [
|
||||
},
|
||||
];
|
||||
|
||||
const onModalToggle = jest.fn();
|
||||
const onModalOpen = jest.fn();
|
||||
|
||||
// eslint-disable-next-line react/prop-types
|
||||
const App = (
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<IntlProvider locale="en" messages={{}} defaultLocale="en" textComponent="span">
|
||||
<Router history={history}>
|
||||
<TableRows headers={headers} rows={rows} onModalToggle={onModalToggle} />
|
||||
<TableRows headers={headers} rows={rows} onOpenModal={onModalOpen} />
|
||||
</Router>
|
||||
</IntlProvider>
|
||||
</ThemeProvider>
|
||||
@ -94,13 +94,13 @@ describe('ADMIN | Pages | AUDIT LOGS | ListView | Dynamic Table | Table Rows', (
|
||||
const label = screen.getByText(/update action details/i);
|
||||
const viewDetailsButton = label.closest('button');
|
||||
fireEvent.click(viewDetailsButton);
|
||||
expect(onModalToggle).toHaveBeenCalled();
|
||||
expect(onModalOpen).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should open a modal when clicked on a row', () => {
|
||||
render(App);
|
||||
const rows = document.querySelectorAll('tr');
|
||||
fireEvent.click(rows[0]);
|
||||
expect(onModalToggle).toHaveBeenCalled();
|
||||
expect(onModalOpen).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
@ -15,13 +15,11 @@ import adminPermissions from '../../../../../permissions';
|
||||
import { useFetchClient } from '../../../../../hooks';
|
||||
import TableRows from './TableRows';
|
||||
import tableHeaders from './utils/tableHeaders';
|
||||
import ModalDialog from './ModalDialog';
|
||||
import PaginationFooter from './PaginationFooter';
|
||||
import Modal from './Modal';
|
||||
|
||||
const ListView = () => {
|
||||
const { formatMessage } = useIntl();
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [detailsActionData, setDetailsActionData] = useState(null);
|
||||
const toggleNotification = useNotification();
|
||||
const {
|
||||
allowedActions: { canRead },
|
||||
@ -31,14 +29,14 @@ const ListView = () => {
|
||||
|
||||
useFocusWhenNavigate();
|
||||
|
||||
const fetchData = async ({ queryKey }) => {
|
||||
const fetchAuditLogsPage = async ({ queryKey }) => {
|
||||
const search = queryKey[1];
|
||||
const { data } = await get(`/admin/audit-logs${search}`);
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
const { data, isLoading } = useQuery(['auditLogs', search], fetchData, {
|
||||
const { data, isLoading } = useQuery(['auditLogs', search], fetchAuditLogsPage, {
|
||||
enabled: canRead,
|
||||
keepPreviousData: true,
|
||||
retry: false,
|
||||
@ -64,14 +62,7 @@ const ListView = () => {
|
||||
},
|
||||
}));
|
||||
|
||||
const handleToggle = (id) => {
|
||||
setIsModalOpen((prev) => !prev);
|
||||
|
||||
if (data.results && id) {
|
||||
const actionData = data.results.find((action) => action.id === id);
|
||||
setDetailsActionData(actionData);
|
||||
}
|
||||
};
|
||||
const [modalLogId, setModalLogId] = useState(null);
|
||||
|
||||
return (
|
||||
<Main aria-busy={isLoading}>
|
||||
@ -91,11 +82,15 @@ const ListView = () => {
|
||||
withBulkActions
|
||||
isLoading={isLoading}
|
||||
>
|
||||
<TableRows headers={headers} rows={data?.results || []} onModalToggle={handleToggle} />
|
||||
<TableRows
|
||||
headers={headers}
|
||||
rows={data?.results || []}
|
||||
onOpenModal={(id) => setModalLogId(id)}
|
||||
/>
|
||||
</DynamicTable>
|
||||
<PaginationFooter pagination={data?.pagination} />
|
||||
</ContentLayout>
|
||||
{isModalOpen && <ModalDialog onToggle={handleToggle} data={detailsActionData} />}
|
||||
{modalLogId && <Modal handleClose={() => setModalLogId(null)} logId={modalLogId} />}
|
||||
</Main>
|
||||
);
|
||||
};
|
||||
|
@ -2,13 +2,13 @@ import React from 'react';
|
||||
import { Router } from 'react-router-dom';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { render, screen, waitFor, within } from '@testing-library/react';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { ThemeProvider, lightTheme } from '@strapi/design-system';
|
||||
import { TrackingProvider } from '@strapi/helper-plugin';
|
||||
import ListView from '../index';
|
||||
import { TEST_DATA, getBigTestData } from './utils/data';
|
||||
import { TEST_PAGE_DATA, TEST_SINGLE_DATA, getBigTestPageData } from './utils/data';
|
||||
|
||||
const history = createMemoryHistory();
|
||||
const user = userEvent.setup();
|
||||
@ -87,7 +87,7 @@ describe('ADMIN | Pages | AUDIT LOGS | ListView', () => {
|
||||
it('should show a list of audit logs with all actions', async () => {
|
||||
mockUseQuery.mockReturnValue({
|
||||
data: {
|
||||
results: TEST_DATA,
|
||||
results: TEST_PAGE_DATA,
|
||||
},
|
||||
isLoading: false,
|
||||
});
|
||||
@ -104,20 +104,31 @@ describe('ADMIN | Pages | AUDIT LOGS | ListView', () => {
|
||||
it('should open a modal when clicked on a table row and close modal when clicked', async () => {
|
||||
mockUseQuery.mockReturnValue({
|
||||
data: {
|
||||
results: TEST_DATA,
|
||||
results: TEST_PAGE_DATA,
|
||||
},
|
||||
isLoading: false,
|
||||
});
|
||||
render(App);
|
||||
|
||||
const { container } = render(App);
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
||||
|
||||
const rows = container.querySelector('tbody').querySelectorAll('tr');
|
||||
await user.click(rows[0]);
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
||||
mockUseQuery.mockReturnValue({
|
||||
data: TEST_SINGLE_DATA,
|
||||
status: 'success',
|
||||
});
|
||||
|
||||
const label = screen.getByText(/close the modal/i);
|
||||
const closeButton = label.closest('button');
|
||||
const auditLogRow = screen.getByText('Create role').closest('tr');
|
||||
await user.click(auditLogRow);
|
||||
|
||||
const modal = screen.getByRole('dialog');
|
||||
expect(modal).toBeInTheDocument();
|
||||
|
||||
const modalContainer = within(modal);
|
||||
expect(modalContainer.getByText('Create role')).toBeInTheDocument();
|
||||
expect(modalContainer.getByText('test user')).toBeInTheDocument();
|
||||
expect(modalContainer.getAllByText('December 22, 2022, 16:11:03')).toHaveLength(3);
|
||||
|
||||
const closeButton = modalContainer.getByText(/close the modal/i).closest('button');
|
||||
await user.click(closeButton);
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
||||
});
|
||||
@ -125,7 +136,7 @@ describe('ADMIN | Pages | AUDIT LOGS | ListView', () => {
|
||||
it('should show pagination and be on page 1 on first render', async () => {
|
||||
mockUseQuery.mockReturnValue({
|
||||
data: {
|
||||
results: getBigTestData(15),
|
||||
results: getBigTestPageData(15),
|
||||
pagination: {
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
@ -147,7 +158,7 @@ describe('ADMIN | Pages | AUDIT LOGS | ListView', () => {
|
||||
it('paginates the results', async () => {
|
||||
mockUseQuery.mockReturnValue({
|
||||
data: {
|
||||
results: getBigTestData(35),
|
||||
results: getBigTestPageData(35),
|
||||
pagination: {
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
@ -191,7 +202,7 @@ describe('ADMIN | Pages | AUDIT LOGS | ListView', () => {
|
||||
|
||||
mockUseQuery.mockReturnValue({
|
||||
data: {
|
||||
results: getBigTestData(20),
|
||||
results: getBigTestPageData(20),
|
||||
pagination: {
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
|
@ -1,4 +1,4 @@
|
||||
export const TEST_DATA = [
|
||||
const TEST_PAGE_DATA = [
|
||||
{
|
||||
id: 1,
|
||||
action: 'role.create',
|
||||
@ -37,15 +37,31 @@ export const TEST_DATA = [
|
||||
},
|
||||
];
|
||||
|
||||
export const getBigTestData = (quantity) => {
|
||||
const TEST_SINGLE_DATA = {
|
||||
id: 1,
|
||||
action: 'role.create',
|
||||
date: '2022-12-22T16:11:03.126Z',
|
||||
payload: {
|
||||
meta: 'data',
|
||||
},
|
||||
user: {
|
||||
id: 1,
|
||||
fullname: 'test user',
|
||||
email: 'test@test.com',
|
||||
},
|
||||
};
|
||||
|
||||
const getBigTestPageData = (quantity) => {
|
||||
const data = [];
|
||||
|
||||
for (let i = 0; i < quantity; i++) {
|
||||
data.push({
|
||||
...TEST_DATA[i % TEST_DATA.length],
|
||||
...TEST_PAGE_DATA[i % TEST_PAGE_DATA.length],
|
||||
id: i + 1,
|
||||
});
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
export { TEST_PAGE_DATA, TEST_SINGLE_DATA, getBigTestPageData };
|
||||
|
Loading…
x
Reference in New Issue
Block a user