mirror of
https://github.com/strapi/strapi.git
synced 2025-11-01 10:23:34 +00:00
Merge pull request #12213 from strapi/cm/repeatable-components-list-view
CM: Allow displaying components in the list view
This commit is contained in:
commit
92e3f45e69
@ -14,6 +14,7 @@ const reducers = {
|
||||
'content-manager_listView': jest.fn(() => ({
|
||||
data: [],
|
||||
isLoading: true,
|
||||
components: [],
|
||||
contentType: {},
|
||||
initialDisplayedHeaders: [],
|
||||
displayedHeaders: [],
|
||||
|
||||
@ -1,87 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { Box } from '@strapi/design-system/Box';
|
||||
import { Typography } from '@strapi/design-system/Typography';
|
||||
import { Loader } from '@strapi/design-system/Loader';
|
||||
import { useNotifyAT } from '@strapi/design-system/LiveRegions';
|
||||
import { axiosInstance } from '../../../../../core/utils';
|
||||
import { getRequestUrl, getTrad } from '../../../../utils';
|
||||
import CellValue from '../CellValue';
|
||||
|
||||
const fetchRelation = async (endPoint, notifyStatus) => {
|
||||
const {
|
||||
data: { results, pagination },
|
||||
} = await axiosInstance.get(endPoint);
|
||||
|
||||
notifyStatus();
|
||||
|
||||
return { results, pagination };
|
||||
};
|
||||
|
||||
const PopoverContent = ({ fieldSchema, name, rowId, targetModel, queryInfos }) => {
|
||||
const requestURL = getRequestUrl(`${queryInfos.endPoint}/${rowId}/${name.split('.')[0]}`);
|
||||
const { notifyStatus } = useNotifyAT();
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
const notify = () => {
|
||||
const message = formatMessage({
|
||||
id: getTrad('DynamicTable.relation-loaded'),
|
||||
defaultMessage: 'The relations have been loaded',
|
||||
});
|
||||
notifyStatus(message);
|
||||
};
|
||||
|
||||
const { data, status } = useQuery([targetModel, rowId], () => fetchRelation(requestURL, notify), {
|
||||
staleTime: 0,
|
||||
});
|
||||
|
||||
if (status !== 'success') {
|
||||
return (
|
||||
<Box>
|
||||
<Loader>Loading content</Loader>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ul>
|
||||
{data?.results.map(entry => {
|
||||
const value = entry[fieldSchema.name];
|
||||
|
||||
return (
|
||||
<Box as="li" key={entry.id} padding={3}>
|
||||
<Typography>
|
||||
{value ? (
|
||||
<CellValue type={fieldSchema.schema.type} value={entry[fieldSchema.name]} />
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
{data?.pagination.total > 10 && (
|
||||
<Box as="li" padding={3}>
|
||||
<Typography>[...]</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
|
||||
PopoverContent.propTypes = {
|
||||
fieldSchema: PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
schema: PropTypes.shape({ type: PropTypes.string }).isRequired,
|
||||
}).isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
rowId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
targetModel: PropTypes.string.isRequired,
|
||||
queryInfos: PropTypes.shape({
|
||||
endPoint: PropTypes.string,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export default PopoverContent;
|
||||
@ -1,107 +0,0 @@
|
||||
import React, { useState, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { IconButton } from '@strapi/design-system/IconButton';
|
||||
import { Typography } from '@strapi/design-system/Typography';
|
||||
import { Box } from '@strapi/design-system/Box';
|
||||
import { Badge } from '@strapi/design-system/Badge';
|
||||
import { Flex } from '@strapi/design-system/Flex';
|
||||
import { Popover } from '@strapi/design-system/Popover';
|
||||
import { SortIcon, stopPropagation } from '@strapi/helper-plugin';
|
||||
import styled from 'styled-components';
|
||||
import PopoverContent from './PopoverContent';
|
||||
import CellValue from '../CellValue';
|
||||
|
||||
const SINGLE_RELATIONS = ['oneToOne', 'manyToOne'];
|
||||
|
||||
const ActionWrapper = styled.span`
|
||||
svg {
|
||||
height: ${4 / 16}rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const RelationCountBadge = styled(Badge)`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: ${20 / 16}rem;
|
||||
width: ${16 / 16}rem;
|
||||
`;
|
||||
|
||||
const Relation = ({ fieldSchema, metadatas, queryInfos, name, rowId, value }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const buttonRef = useRef();
|
||||
|
||||
if (SINGLE_RELATIONS.includes(fieldSchema.relation)) {
|
||||
return (
|
||||
<Typography textColor="neutral800">
|
||||
<CellValue type={metadatas.mainField.schema.type} value={value[metadatas.mainField.name]} />
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
|
||||
const handleTogglePopover = () => setVisible(prev => !prev);
|
||||
|
||||
return (
|
||||
<Flex {...stopPropagation}>
|
||||
<RelationCountBadge>{value.count}</RelationCountBadge>
|
||||
<Box paddingLeft={2}>
|
||||
<Typography textColor="neutral800">
|
||||
{formatMessage(
|
||||
{
|
||||
id: 'content-manager.containers.ListPage.items',
|
||||
defaultMessage: '{number, plural, =0 {items} one {item} other {items}}',
|
||||
},
|
||||
{ number: value.count }
|
||||
)}
|
||||
</Typography>
|
||||
</Box>
|
||||
{value.count > 0 && (
|
||||
<ActionWrapper>
|
||||
<IconButton
|
||||
onClick={handleTogglePopover}
|
||||
ref={buttonRef}
|
||||
noBorder
|
||||
label={formatMessage({
|
||||
id: 'content-manager.popover.display-relations.label',
|
||||
defaultMessage: 'Display relations',
|
||||
})}
|
||||
icon={<SortIcon isUp={visible} />}
|
||||
/>
|
||||
{visible && (
|
||||
<Popover source={buttonRef} spacing={16} centered>
|
||||
<PopoverContent
|
||||
queryInfos={queryInfos}
|
||||
name={name}
|
||||
fieldSchema={metadatas.mainField}
|
||||
targetModel={fieldSchema.targetModel}
|
||||
rowId={rowId}
|
||||
count={value.count}
|
||||
/>
|
||||
</Popover>
|
||||
)}
|
||||
</ActionWrapper>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
Relation.propTypes = {
|
||||
fieldSchema: PropTypes.shape({
|
||||
relation: PropTypes.string,
|
||||
targetModel: PropTypes.string,
|
||||
type: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
metadatas: PropTypes.shape({
|
||||
mainField: PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
schema: PropTypes.shape({ type: PropTypes.string.isRequired }).isRequired,
|
||||
}),
|
||||
}).isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
rowId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
queryInfos: PropTypes.shape({ endPoint: PropTypes.string.isRequired }).isRequired,
|
||||
value: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default Relation;
|
||||
@ -0,0 +1,135 @@
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useQuery } from 'react-query';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { Typography } from '@strapi/design-system/Typography';
|
||||
import { Box } from '@strapi/design-system/Box';
|
||||
import { Badge } from '@strapi/design-system/Badge';
|
||||
import { SimpleMenu, MenuItem } from '@strapi/design-system/SimpleMenu';
|
||||
import { Loader } from '@strapi/design-system/Loader';
|
||||
import styled from 'styled-components';
|
||||
import { useNotifyAT } from '@strapi/design-system/LiveRegions';
|
||||
import { stopPropagation } from '@strapi/helper-plugin';
|
||||
import CellValue from '../CellValue';
|
||||
import { axiosInstance } from '../../../../../core/utils';
|
||||
import { getRequestUrl, getTrad } from '../../../../utils';
|
||||
|
||||
const TypographyMaxWidth = styled(Typography)`
|
||||
max-width: 500px;
|
||||
`;
|
||||
|
||||
const fetchRelation = async (endPoint, notifyStatus) => {
|
||||
const {
|
||||
data: { results, pagination },
|
||||
} = await axiosInstance.get(endPoint);
|
||||
|
||||
notifyStatus();
|
||||
|
||||
return { results, pagination };
|
||||
};
|
||||
|
||||
const RelationMultiple = ({ fieldSchema, metadatas, queryInfos, name, rowId, value }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { notifyStatus } = useNotifyAT();
|
||||
const requestURL = getRequestUrl(`${queryInfos.endPoint}/${rowId}/${name.split('.')[0]}`);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const Label = (
|
||||
<>
|
||||
<Badge>{value.count}</Badge>{' '}
|
||||
{formatMessage(
|
||||
{
|
||||
id: 'content-manager.containers.ListPage.items',
|
||||
defaultMessage: '{number, plural, =0 {items} one {item} other {items}}',
|
||||
},
|
||||
{ number: value.count }
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
const notify = () => {
|
||||
const message = formatMessage({
|
||||
id: getTrad('DynamicTable.relation-loaded'),
|
||||
defaultMessage: 'Relations have been loaded',
|
||||
});
|
||||
notifyStatus(message);
|
||||
};
|
||||
|
||||
const { data, status } = useQuery(
|
||||
[fieldSchema.targetModel, rowId],
|
||||
() => fetchRelation(requestURL, notify),
|
||||
{
|
||||
enabled: isOpen,
|
||||
staleTime: 0,
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<Box {...stopPropagation}>
|
||||
<SimpleMenu
|
||||
label={Label}
|
||||
size="S"
|
||||
onOpen={() => setIsOpen(true)}
|
||||
onClose={() => setIsOpen(false)}
|
||||
>
|
||||
{status !== 'success' && (
|
||||
<MenuItem aria-disabled>
|
||||
<Loader small>
|
||||
{formatMessage({
|
||||
id: getTrad('DynamicTable.relation-loading'),
|
||||
defaultMessage: 'Relations are loading',
|
||||
})}
|
||||
</Loader>
|
||||
</MenuItem>
|
||||
)}
|
||||
|
||||
{status === 'success' && (
|
||||
<>
|
||||
{data?.results.map(entry => (
|
||||
<MenuItem key={entry.id} aria-disabled>
|
||||
<TypographyMaxWidth ellipsis>
|
||||
<CellValue
|
||||
type={metadatas.mainField.schema.type}
|
||||
value={entry[metadatas.mainField.name] || entry.id}
|
||||
/>
|
||||
</TypographyMaxWidth>
|
||||
</MenuItem>
|
||||
))}
|
||||
|
||||
{data?.pagination.total > 10 && (
|
||||
<MenuItem
|
||||
aria-disabled
|
||||
aria-label={formatMessage({
|
||||
id: getTrad('DynamicTable.relation-more'),
|
||||
defaultMessage: 'This relation contains more entities than displayed',
|
||||
})}
|
||||
>
|
||||
<Typography>...</Typography>
|
||||
</MenuItem>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</SimpleMenu>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
RelationMultiple.propTypes = {
|
||||
fieldSchema: PropTypes.shape({
|
||||
relation: PropTypes.string,
|
||||
targetModel: PropTypes.string,
|
||||
type: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
metadatas: PropTypes.shape({
|
||||
mainField: PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
schema: PropTypes.shape({ type: PropTypes.string.isRequired }).isRequired,
|
||||
}),
|
||||
}).isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
rowId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
queryInfos: PropTypes.shape({ endPoint: PropTypes.string.isRequired }).isRequired,
|
||||
value: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default RelationMultiple;
|
||||
@ -0,0 +1,291 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`DynamicTabe / Cellcontent / RelationMultiple renders and matches the snapshot 1`] = `
|
||||
.c11 {
|
||||
border: 0;
|
||||
-webkit-clip: rect(0 0 0 0);
|
||||
clip: rect(0 0 0 0);
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
.c5 {
|
||||
background: #f6f6f9;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
min-width: 20px;
|
||||
}
|
||||
|
||||
.c6 {
|
||||
display: -webkit-inline-box;
|
||||
display: -webkit-inline-flex;
|
||||
display: -ms-inline-flexbox;
|
||||
display: inline-flex;
|
||||
-webkit-flex-direction: row;
|
||||
-ms-flex-direction: row;
|
||||
flex-direction: row;
|
||||
-webkit-box-pack: center;
|
||||
-webkit-justify-content: center;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.c7 {
|
||||
color: #666687;
|
||||
font-weight: 600;
|
||||
font-size: 0.6875rem;
|
||||
line-height: 1.45;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.c4 {
|
||||
font-weight: 600;
|
||||
color: #32324d;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.33;
|
||||
}
|
||||
|
||||
.c9 {
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.c0 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
background: #ffffff;
|
||||
border: 1px solid #dcdce4;
|
||||
position: relative;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.c0 svg {
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
}
|
||||
|
||||
.c0 svg > g,
|
||||
.c0 svg path {
|
||||
fill: #ffffff;
|
||||
}
|
||||
|
||||
.c0[aria-disabled='true'] {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.c0:after {
|
||||
-webkit-transition-property: all;
|
||||
transition-property: all;
|
||||
-webkit-transition-duration: 0.2s;
|
||||
transition-duration: 0.2s;
|
||||
border-radius: 8px;
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
bottom: -4px;
|
||||
left: -4px;
|
||||
right: -4px;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
.c0:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.c0:focus-visible:after {
|
||||
border-radius: 8px;
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
bottom: -5px;
|
||||
left: -5px;
|
||||
right: -5px;
|
||||
border: 2px solid #4945ff;
|
||||
}
|
||||
|
||||
.c1 {
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
padding: 8px 16px;
|
||||
background: #4945ff;
|
||||
border: none;
|
||||
border: 1px solid transparent;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.c1 .c8 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.c1 .c3 {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.c1[aria-disabled='true'] {
|
||||
border: 1px solid #dcdce4;
|
||||
background: #eaeaef;
|
||||
}
|
||||
|
||||
.c1[aria-disabled='true'] .c3 {
|
||||
color: #666687;
|
||||
}
|
||||
|
||||
.c1[aria-disabled='true'] svg > g,
|
||||
.c1[aria-disabled='true'] svg path {
|
||||
fill: #666687;
|
||||
}
|
||||
|
||||
.c1[aria-disabled='true']:active {
|
||||
border: 1px solid #dcdce4;
|
||||
background: #eaeaef;
|
||||
}
|
||||
|
||||
.c1[aria-disabled='true']:active .c3 {
|
||||
color: #666687;
|
||||
}
|
||||
|
||||
.c1[aria-disabled='true']:active svg > g,
|
||||
.c1[aria-disabled='true']:active svg path {
|
||||
fill: #666687;
|
||||
}
|
||||
|
||||
.c1:hover {
|
||||
background-color: #f6f6f9;
|
||||
}
|
||||
|
||||
.c1:active {
|
||||
border: 1px solid undefined;
|
||||
background: undefined;
|
||||
}
|
||||
|
||||
.c1 .c3 {
|
||||
color: #32324d;
|
||||
}
|
||||
|
||||
.c1 svg > g,
|
||||
.c1 svg path {
|
||||
fill: #8e8ea9;
|
||||
}
|
||||
|
||||
.c10 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.c10 svg {
|
||||
height: 4px;
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.c2 {
|
||||
padding: 4px 12px;
|
||||
}
|
||||
|
||||
<div>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class=""
|
||||
role="button"
|
||||
>
|
||||
<div>
|
||||
<button
|
||||
aria-controls="simplemenu-1"
|
||||
aria-disabled="false"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
class="c0 c1 c2"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="c3 c4"
|
||||
>
|
||||
<div
|
||||
class="c5 c6"
|
||||
>
|
||||
<span
|
||||
class="c7"
|
||||
>
|
||||
1
|
||||
</span>
|
||||
</div>
|
||||
|
||||
item
|
||||
</span>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="c8 c9"
|
||||
>
|
||||
<span
|
||||
class="c10"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
height="1em"
|
||||
viewBox="0 0 14 8"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M14 .889a.86.86 0 01-.26.625L7.615 7.736A.834.834 0 017 8a.834.834 0 01-.615-.264L.26 1.514A.861.861 0 010 .889c0-.24.087-.45.26-.625A.834.834 0 01.875 0h12.25c.237 0 .442.088.615.264a.86.86 0 01.26.625z"
|
||||
fill="#32324D"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c11"
|
||||
>
|
||||
<p
|
||||
aria-live="polite"
|
||||
aria-relevant="all"
|
||||
id="live-region-log"
|
||||
role="log"
|
||||
/>
|
||||
<p
|
||||
aria-live="polite"
|
||||
aria-relevant="all"
|
||||
id="live-region-status"
|
||||
role="status"
|
||||
/>
|
||||
<p
|
||||
aria-live="assertive"
|
||||
aria-relevant="all"
|
||||
id="live-region-alert"
|
||||
role="alert"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@ -0,0 +1,80 @@
|
||||
import React from 'react';
|
||||
import { render, fireEvent, screen, waitFor } from '@testing-library/react';
|
||||
import { ThemeProvider, lightTheme } from '@strapi/design-system';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import { QueryClientProvider, QueryClient } from 'react-query';
|
||||
|
||||
import { axiosInstance } from '../../../../../../core/utils';
|
||||
import RelationMultiple from '../index';
|
||||
|
||||
jest.spyOn(axiosInstance, 'get').mockResolvedValue({
|
||||
data: {
|
||||
results: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Relation entity 1',
|
||||
},
|
||||
],
|
||||
|
||||
pagination: {
|
||||
total: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const DEFAULT_PROPS_FIXTURE = {
|
||||
fieldSchema: {
|
||||
type: 'relation',
|
||||
relation: 'manyToMany',
|
||||
target: 'api::category.category',
|
||||
},
|
||||
queryInfos: {
|
||||
endPoint: 'collection-types/api::address.address',
|
||||
},
|
||||
metadatas: {
|
||||
mainField: {
|
||||
name: 'name',
|
||||
schema: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
value: {
|
||||
count: 1,
|
||||
},
|
||||
name: 'categories.name',
|
||||
rowId: 1,
|
||||
};
|
||||
|
||||
const ComponentFixture = () => {
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<IntlProvider locale="en" messages={{}} defaultLocale="en">
|
||||
<RelationMultiple {...DEFAULT_PROPS_FIXTURE} />
|
||||
</IntlProvider>
|
||||
</ThemeProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
|
||||
describe('DynamicTabe / Cellcontent / RelationMultiple', () => {
|
||||
it('renders and matches the snapshot', async () => {
|
||||
const { container } = render(<ComponentFixture />);
|
||||
expect(container).toMatchSnapshot();
|
||||
expect(axiosInstance.get).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('fetches relation entities once the menu is opened', async () => {
|
||||
const { container } = render(<ComponentFixture />);
|
||||
const button = container.querySelector('[type=button]');
|
||||
|
||||
fireEvent(button, new MouseEvent('mousedown', { bubbles: true }));
|
||||
|
||||
expect(screen.getByText('Relations are loading')).toBeInTheDocument();
|
||||
expect(axiosInstance.get).toHaveBeenCalledTimes(1);
|
||||
await waitFor(() => expect(screen.getByText('Relation entity 1')).toBeInTheDocument());
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Typography } from '@strapi/design-system/Typography';
|
||||
import styled from 'styled-components';
|
||||
import CellValue from '../CellValue';
|
||||
|
||||
const TypographyMaxWidth = styled(Typography)`
|
||||
max-width: 500px;
|
||||
`;
|
||||
|
||||
const RelationSingle = ({ metadatas, value }) => {
|
||||
return (
|
||||
<TypographyMaxWidth textColor="neutral800" ellipsis>
|
||||
<CellValue
|
||||
type={metadatas.mainField.schema.type}
|
||||
value={value[metadatas.mainField.name] || value.id}
|
||||
/>
|
||||
</TypographyMaxWidth>
|
||||
);
|
||||
};
|
||||
|
||||
RelationSingle.propTypes = {
|
||||
metadatas: PropTypes.shape({
|
||||
mainField: PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
schema: PropTypes.shape({ type: PropTypes.string.isRequired }).isRequired,
|
||||
}),
|
||||
}).isRequired,
|
||||
value: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default RelationSingle;
|
||||
@ -0,0 +1,59 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`DynamicTabe / Cellcontent / RelationSingle renders and matches the snapshot 1`] = `
|
||||
.c2 {
|
||||
border: 0;
|
||||
-webkit-clip: rect(0 0 0 0);
|
||||
clip: rect(0 0 0 0);
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
.c0 {
|
||||
color: #32324d;
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.43;
|
||||
}
|
||||
|
||||
.c1 {
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
<div>
|
||||
<span
|
||||
class="c0 c1"
|
||||
>
|
||||
|
||||
</span>
|
||||
<div
|
||||
class="c2"
|
||||
>
|
||||
<p
|
||||
aria-live="polite"
|
||||
aria-relevant="all"
|
||||
id="live-region-log"
|
||||
role="log"
|
||||
/>
|
||||
<p
|
||||
aria-live="polite"
|
||||
aria-relevant="all"
|
||||
id="live-region-status"
|
||||
role="status"
|
||||
/>
|
||||
<p
|
||||
aria-live="assertive"
|
||||
aria-relevant="all"
|
||||
id="live-region-alert"
|
||||
role="alert"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@ -0,0 +1,37 @@
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { ThemeProvider, lightTheme } from '@strapi/design-system';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
|
||||
import RelationSingle from '../index';
|
||||
|
||||
const DEFAULT_PROPS_FIXTURE = {
|
||||
metadatas: {
|
||||
mainField: {
|
||||
name: 'name',
|
||||
schema: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
value: {
|
||||
count: 1,
|
||||
},
|
||||
};
|
||||
|
||||
const ComponentFixture = () => {
|
||||
return (
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<IntlProvider locale="en" messages={{}} defaultLocale="en">
|
||||
<RelationSingle {...DEFAULT_PROPS_FIXTURE} />
|
||||
</IntlProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
describe('DynamicTabe / Cellcontent / RelationSingle', () => {
|
||||
it('renders and matches the snapshot', async () => {
|
||||
const { container } = render(<ComponentFixture />);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,62 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styled from 'styled-components';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { Badge } from '@strapi/design-system/Badge';
|
||||
import { Box } from '@strapi/design-system/Box';
|
||||
import { Typography } from '@strapi/design-system/Typography';
|
||||
import { SimpleMenu, MenuItem } from '@strapi/design-system/SimpleMenu';
|
||||
import { stopPropagation } from '@strapi/helper-plugin';
|
||||
|
||||
import CellValue from '../CellValue';
|
||||
|
||||
const TypographyMaxWidth = styled(Typography)`
|
||||
max-width: 500px;
|
||||
`;
|
||||
|
||||
const RepeatableComponentCell = ({ value, metadatas }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const {
|
||||
mainField: { type: mainFieldType, name: mainFieldName },
|
||||
} = metadatas;
|
||||
|
||||
const Label = (
|
||||
<>
|
||||
<Badge>{value.length}</Badge>{' '}
|
||||
{formatMessage(
|
||||
{
|
||||
id: 'content-manager.containers.ListPage.items',
|
||||
defaultMessage: '{number, plural, =0 {items} one {item} other {items}}',
|
||||
},
|
||||
{ number: value.length }
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<Box {...stopPropagation}>
|
||||
<SimpleMenu label={Label} size="S">
|
||||
{value.map(item => (
|
||||
<MenuItem key={item.id} aria-disabled>
|
||||
<TypographyMaxWidth ellipsis>
|
||||
<CellValue type={mainFieldType} value={item[mainFieldName] || item.id} />
|
||||
</TypographyMaxWidth>
|
||||
</MenuItem>
|
||||
))}
|
||||
</SimpleMenu>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
RepeatableComponentCell.propTypes = {
|
||||
metadatas: PropTypes.shape({
|
||||
mainField: PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
type: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
}),
|
||||
}).isRequired,
|
||||
value: PropTypes.array.isRequired,
|
||||
};
|
||||
|
||||
export default RepeatableComponentCell;
|
||||
@ -0,0 +1,37 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styled from 'styled-components';
|
||||
import { Tooltip } from '@strapi/design-system/Tooltip';
|
||||
import { Typography } from '@strapi/design-system/Typography';
|
||||
|
||||
import CellValue from '../CellValue';
|
||||
|
||||
const TypographyMaxWidth = styled(Typography)`
|
||||
max-width: 250px;
|
||||
`;
|
||||
|
||||
const SingleComponentCell = ({ value, metadatas }) => {
|
||||
const { mainField } = metadatas;
|
||||
const content = value[mainField.name];
|
||||
|
||||
return (
|
||||
<Tooltip label={content}>
|
||||
<TypographyMaxWidth textColor="neutral800" ellipsis>
|
||||
<CellValue type={mainField.type} value={content} />
|
||||
</TypographyMaxWidth>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
SingleComponentCell.propTypes = {
|
||||
metadatas: PropTypes.shape({
|
||||
mainField: PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
type: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
}),
|
||||
}).isRequired,
|
||||
value: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default SingleComponentCell;
|
||||
@ -4,44 +4,64 @@ import styled from 'styled-components';
|
||||
import { Typography } from '@strapi/design-system/Typography';
|
||||
import Media from './Media';
|
||||
import MultipleMedias from './MultipleMedias';
|
||||
import Relation from './Relation';
|
||||
import RelationMultiple from './RelationMultiple';
|
||||
import RelationSingle from './RelationSingle';
|
||||
import RepeatableComponent from './RepeatableComponent';
|
||||
import SingleComponent from './SingleComponent';
|
||||
import CellValue from './CellValue';
|
||||
import hasContent from './utils/hasContent';
|
||||
import isSingleRelation from './utils/isSingleRelation';
|
||||
|
||||
const TypographyMaxWidth = styled(Typography)`
|
||||
max-width: 300px;
|
||||
`;
|
||||
|
||||
const CellContent = ({ content, fieldSchema, metadatas, name, queryInfos, rowId }) => {
|
||||
if (content === null || content === undefined) {
|
||||
const { type } = fieldSchema;
|
||||
|
||||
if (!hasContent(type, content, metadatas, fieldSchema)) {
|
||||
return <Typography textColor="neutral800">-</Typography>;
|
||||
}
|
||||
|
||||
if (fieldSchema.type === 'media' && !fieldSchema.multiple) {
|
||||
return <Media {...content} />;
|
||||
}
|
||||
switch (type) {
|
||||
case 'media':
|
||||
if (!fieldSchema.multiple) {
|
||||
return <Media {...content} />;
|
||||
}
|
||||
|
||||
if (fieldSchema.type === 'media' && fieldSchema.multiple) {
|
||||
return <MultipleMedias value={content} />;
|
||||
}
|
||||
return <MultipleMedias value={content} />;
|
||||
|
||||
if (fieldSchema.type === 'relation') {
|
||||
return (
|
||||
<Relation
|
||||
fieldSchema={fieldSchema}
|
||||
queryInfos={queryInfos}
|
||||
metadatas={metadatas}
|
||||
value={content}
|
||||
name={name}
|
||||
rowId={rowId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'relation': {
|
||||
if (isSingleRelation(fieldSchema.relation)) {
|
||||
return <RelationSingle metadatas={metadatas} value={content} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<TypographyMaxWidth ellipsis textColor="neutral800">
|
||||
<CellValue type={fieldSchema.type} value={content} />
|
||||
</TypographyMaxWidth>
|
||||
);
|
||||
return (
|
||||
<RelationMultiple
|
||||
fieldSchema={fieldSchema}
|
||||
queryInfos={queryInfos}
|
||||
metadatas={metadatas}
|
||||
value={content}
|
||||
name={name}
|
||||
rowId={rowId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
case 'component':
|
||||
if (fieldSchema.repeatable === true) {
|
||||
return <RepeatableComponent value={content} metadatas={metadatas} />;
|
||||
}
|
||||
|
||||
return <SingleComponent value={content} metadatas={metadatas} />;
|
||||
|
||||
default:
|
||||
return (
|
||||
<TypographyMaxWidth ellipsis textColor="neutral800">
|
||||
<CellValue type={type} value={content} />
|
||||
</TypographyMaxWidth>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
CellContent.defaultProps = {
|
||||
@ -51,8 +71,13 @@ CellContent.defaultProps = {
|
||||
|
||||
CellContent.propTypes = {
|
||||
content: PropTypes.any,
|
||||
fieldSchema: PropTypes.shape({ multiple: PropTypes.bool, type: PropTypes.string.isRequired })
|
||||
.isRequired,
|
||||
fieldSchema: PropTypes.shape({
|
||||
component: PropTypes.string,
|
||||
multiple: PropTypes.bool,
|
||||
type: PropTypes.string.isRequired,
|
||||
repeatable: PropTypes.bool,
|
||||
relation: PropTypes.string,
|
||||
}).isRequired,
|
||||
metadatas: PropTypes.object.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
rowId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
|
||||
@ -0,0 +1,29 @@
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
|
||||
import isSingleRelation from './isSingleRelation';
|
||||
|
||||
export default function hasContent(type, content, metadatas, fieldSchema) {
|
||||
if (type === 'component') {
|
||||
const {
|
||||
mainField: { name: mainFieldName },
|
||||
} = metadatas;
|
||||
|
||||
// Repeatable fields show the ID as fallback, in case the mainField
|
||||
// doesn't have any content
|
||||
if (fieldSchema?.repeatable) {
|
||||
return content.length > 0;
|
||||
}
|
||||
|
||||
return !isEmpty(content[mainFieldName]);
|
||||
}
|
||||
|
||||
if (type === 'relation') {
|
||||
if (isSingleRelation(fieldSchema.relation)) {
|
||||
return !isEmpty(content);
|
||||
}
|
||||
|
||||
return content.count > 0;
|
||||
}
|
||||
|
||||
return !isEmpty(content);
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
export default function isSingleRelation(type) {
|
||||
return ['oneToOne', 'manyToOne', 'oneToOneMorph'].includes(type);
|
||||
}
|
||||
@ -0,0 +1,125 @@
|
||||
import hasContent from '../hasContent';
|
||||
|
||||
describe('hasContent', () => {
|
||||
it('returns true for text content', () => {
|
||||
const normalizedContent = hasContent('text', 'content');
|
||||
expect(normalizedContent).toEqual(true);
|
||||
});
|
||||
|
||||
it('returns false for empty text content', () => {
|
||||
const normalizedContent = hasContent('text', '');
|
||||
expect(normalizedContent).toEqual(false);
|
||||
});
|
||||
|
||||
it('returns false for undefined text content', () => {
|
||||
const normalizedContent = hasContent('text', undefined);
|
||||
expect(normalizedContent).toEqual(false);
|
||||
});
|
||||
|
||||
it('extracts content from single components with content', () => {
|
||||
const normalizedContent = hasContent(
|
||||
'component',
|
||||
{ name: 'content', id: 1 },
|
||||
{ mainField: { name: 'name' } }
|
||||
);
|
||||
expect(normalizedContent).toEqual(true);
|
||||
});
|
||||
|
||||
it('extracts content from single components without content', () => {
|
||||
const normalizedContent = hasContent(
|
||||
'component',
|
||||
{ name: '', id: 1 },
|
||||
{ mainField: { name: 'name' } }
|
||||
);
|
||||
expect(normalizedContent).toEqual(false);
|
||||
});
|
||||
|
||||
it('extracts content from repeatable components with content', () => {
|
||||
const normalizedContent = hasContent(
|
||||
'component',
|
||||
[{ name: 'content_2', value: 'truthy', id: 1 }],
|
||||
{ mainField: { name: 'content_2' } },
|
||||
{ repeatable: true }
|
||||
);
|
||||
expect(normalizedContent).toEqual(true);
|
||||
});
|
||||
|
||||
it('extracts content from repeatable components without content', () => {
|
||||
const normalizedContent = hasContent(
|
||||
'component',
|
||||
[{ name: 'content_2', value: '', id: 1 }],
|
||||
{ mainField: { name: 'content_2' } },
|
||||
{ repeatable: true }
|
||||
);
|
||||
expect(normalizedContent).toEqual(true);
|
||||
});
|
||||
|
||||
it('extracts content from repeatable components without content', () => {
|
||||
const normalizedContent = hasContent(
|
||||
'component',
|
||||
[{ id: 1 }, { id: 2 }],
|
||||
{ mainField: { name: 'content_2' } },
|
||||
{ repeatable: true }
|
||||
);
|
||||
expect(normalizedContent).toEqual(true);
|
||||
});
|
||||
|
||||
it('extracts content from repeatable components without content', () => {
|
||||
const normalizedContent = hasContent(
|
||||
'component',
|
||||
[],
|
||||
{ mainField: { name: 'content_2' } },
|
||||
{ repeatable: true }
|
||||
);
|
||||
expect(normalizedContent).toEqual(false);
|
||||
});
|
||||
|
||||
it('extracts content from multiple relations with content', () => {
|
||||
const normalizedContent = hasContent('relation', { count: 1 }, undefined, {
|
||||
relation: 'manyToMany',
|
||||
});
|
||||
expect(normalizedContent).toEqual(true);
|
||||
});
|
||||
|
||||
it('extracts content from multiple relations without content', () => {
|
||||
const normalizedContent = hasContent('relation', { count: 0 }, undefined, {
|
||||
relation: 'manyToMany',
|
||||
});
|
||||
expect(normalizedContent).toEqual(false);
|
||||
});
|
||||
|
||||
it('extracts content from single relations with content', () => {
|
||||
const normalizedContent = hasContent('relation', { id: 1 }, undefined, {
|
||||
relation: 'oneToOne',
|
||||
});
|
||||
expect(normalizedContent).toEqual(true);
|
||||
});
|
||||
|
||||
it('extracts content from single relations without content', () => {
|
||||
const normalizedContent = hasContent('relation', null, undefined, {
|
||||
relation: 'oneToOne',
|
||||
});
|
||||
expect(normalizedContent).toEqual(false);
|
||||
});
|
||||
|
||||
it('returns oneToManyMorph relations as false with content', () => {
|
||||
const normalizedContent = hasContent('relation', { id: 1 }, undefined, {
|
||||
relation: 'oneToManyMorph',
|
||||
});
|
||||
expect(normalizedContent).toEqual(false);
|
||||
});
|
||||
|
||||
it('extracts content from oneToManyMorph relations with content', () => {
|
||||
const normalizedContent = hasContent('relation', { id: 1 }, undefined, {
|
||||
relation: 'oneToOneMorph',
|
||||
});
|
||||
expect(normalizedContent).toEqual(true);
|
||||
});
|
||||
|
||||
it('extracts content from oneToManyMorph relations with content', () => {
|
||||
const normalizedContent = hasContent('relation', null, undefined, {
|
||||
relation: 'oneToOneMorph',
|
||||
});
|
||||
expect(normalizedContent).toEqual(false);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,13 @@
|
||||
import isSingleRelation from '../isSingleRelation';
|
||||
|
||||
describe('isSingleRelation', () => {
|
||||
['oneToOne', 'manyToOne', 'oneToOneMorph'].forEach(type => {
|
||||
test(`is single relation: ${type}`, () => {
|
||||
expect(isSingleRelation(type)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
test('is not single relation', () => {
|
||||
expect(isSingleRelation('manyToMany')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
@ -40,7 +40,7 @@ const formatLayouts = (initialData, models) => {
|
||||
const formattedCTEditLayout = formatLayoutWithMetas(data.contentType, null, models);
|
||||
const ctUid = data.contentType.uid;
|
||||
const formattedEditRelationsLayout = formatEditRelationsLayoutWithMetas(data.contentType, models);
|
||||
const formattedListLayout = formatListLayoutWithMetas(data.contentType, models);
|
||||
const formattedListLayout = formatListLayoutWithMetas(data.contentType, data.components);
|
||||
|
||||
set(data, ['contentType', 'layouts', 'edit'], formattedCTEditLayout);
|
||||
set(data, ['contentType', 'layouts', 'editRelations'], formattedEditRelationsLayout);
|
||||
@ -146,7 +146,7 @@ const formatLayoutWithMetas = (contentTypeConfiguration, ctUid, models) => {
|
||||
return formatted;
|
||||
};
|
||||
|
||||
const formatListLayoutWithMetas = contentTypeConfiguration => {
|
||||
const formatListLayoutWithMetas = (contentTypeConfiguration, components) => {
|
||||
const formatted = contentTypeConfiguration.layouts.list.reduce((acc, current) => {
|
||||
const fieldSchema = get(contentTypeConfiguration, ['attributes', current], {});
|
||||
const metadatas = get(contentTypeConfiguration, ['metadatas', current, 'list'], {});
|
||||
@ -164,6 +164,27 @@ const formatListLayoutWithMetas = contentTypeConfiguration => {
|
||||
return acc;
|
||||
}
|
||||
|
||||
if (type === 'component') {
|
||||
const component = components[fieldSchema.component];
|
||||
const mainFieldName = component.settings.mainField;
|
||||
const mainFieldAttribute = component.attributes[mainFieldName];
|
||||
|
||||
acc.push({
|
||||
key: `__${current}_key__`,
|
||||
name: current,
|
||||
fieldSchema,
|
||||
metadatas: {
|
||||
...metadatas,
|
||||
mainField: {
|
||||
...mainFieldAttribute,
|
||||
name: mainFieldName,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return acc;
|
||||
}
|
||||
|
||||
acc.push({ key: `__${current}_key__`, name: current, fieldSchema, metadatas });
|
||||
|
||||
return acc;
|
||||
|
||||
@ -485,12 +485,22 @@ describe('Content Manager | hooks | useFetchContentTypeLayout | utils ', () => {
|
||||
const data = {
|
||||
uid: 'address',
|
||||
layouts: {
|
||||
list: ['test', 'categories'],
|
||||
list: ['test', 'categories', 'component'],
|
||||
},
|
||||
metadatas: {
|
||||
test: {
|
||||
list: { ok: true },
|
||||
},
|
||||
component: {
|
||||
list: {
|
||||
mainField: {
|
||||
name: 'name',
|
||||
schema: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
categories: {
|
||||
list: {
|
||||
ok: true,
|
||||
@ -509,6 +519,22 @@ describe('Content Manager | hooks | useFetchContentTypeLayout | utils ', () => {
|
||||
type: 'relation',
|
||||
targetModel: 'category',
|
||||
},
|
||||
component: {
|
||||
type: 'component',
|
||||
component: 'some.component',
|
||||
repeatable: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
const components = {
|
||||
'some.component': {
|
||||
settings: {
|
||||
mainField: 'name',
|
||||
},
|
||||
|
||||
attributes: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
};
|
||||
const expected = [
|
||||
@ -533,9 +559,23 @@ describe('Content Manager | hooks | useFetchContentTypeLayout | utils ', () => {
|
||||
fieldSchema: { type: 'relation', targetModel: 'category' },
|
||||
queryInfos: { defaultParams: {}, endPoint: 'collection-types/address' },
|
||||
},
|
||||
{
|
||||
name: 'component',
|
||||
key: '__component_key__',
|
||||
metadatas: {
|
||||
mainField: {
|
||||
name: 'name',
|
||||
},
|
||||
},
|
||||
fieldSchema: {
|
||||
type: 'component',
|
||||
component: 'some.component',
|
||||
repeatable: false,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
expect(formatListLayoutWithMetas(data)).toEqual(expected);
|
||||
expect(formatListLayoutWithMetas(data, components)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -21,11 +21,12 @@ export function resetProps() {
|
||||
return { type: RESET_PROPS };
|
||||
}
|
||||
|
||||
export const setLayout = contentType => {
|
||||
export const setLayout = ({ components, contentType }) => {
|
||||
const { layouts } = contentType;
|
||||
|
||||
return {
|
||||
contentType,
|
||||
components,
|
||||
displayedHeaders: layouts.list,
|
||||
type: SET_LIST_LAYOUT,
|
||||
};
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
*/
|
||||
|
||||
import produce from 'immer';
|
||||
import get from 'lodash/get';
|
||||
import {
|
||||
GET_DATA,
|
||||
GET_DATA_SUCCEEDED,
|
||||
@ -17,6 +18,7 @@ export const initialState = {
|
||||
data: [],
|
||||
isLoading: true,
|
||||
contentType: {},
|
||||
components: [],
|
||||
initialDisplayedHeaders: [],
|
||||
displayedHeaders: [],
|
||||
pagination: {
|
||||
@ -26,21 +28,22 @@ export const initialState = {
|
||||
|
||||
const listViewReducer = (state = initialState, action) =>
|
||||
// eslint-disable-next-line consistent-return
|
||||
produce(state, drafState => {
|
||||
produce(state, draftState => {
|
||||
switch (action.type) {
|
||||
case GET_DATA: {
|
||||
return {
|
||||
...initialState,
|
||||
contentType: state.contentType,
|
||||
components: state.components,
|
||||
initialDisplayedHeaders: state.initialDisplayedHeaders,
|
||||
displayedHeaders: state.displayedHeaders,
|
||||
};
|
||||
}
|
||||
|
||||
case GET_DATA_SUCCEEDED: {
|
||||
drafState.pagination = action.pagination;
|
||||
drafState.data = action.data;
|
||||
drafState.isLoading = false;
|
||||
draftState.pagination = action.pagination;
|
||||
draftState.data = action.data;
|
||||
draftState.isLoading = false;
|
||||
break;
|
||||
}
|
||||
|
||||
@ -59,19 +62,49 @@ const listViewReducer = (state = initialState, action) =>
|
||||
key: `__${name}_key__`,
|
||||
};
|
||||
|
||||
if (attributes[name].type === 'relation') {
|
||||
drafState.displayedHeaders.push({
|
||||
...header,
|
||||
queryInfos: {
|
||||
defaultParams: {},
|
||||
endPoint: `collection-types/${uid}`,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
drafState.displayedHeaders.push(header);
|
||||
switch (attributes[name].type) {
|
||||
case 'component': {
|
||||
const componentName = attributes[name].component;
|
||||
const mainFieldName = get(
|
||||
state,
|
||||
['components', componentName, 'settings', 'mainField'],
|
||||
null
|
||||
);
|
||||
const mainFieldAttribute = get(state, [
|
||||
'components',
|
||||
componentName,
|
||||
'attributes',
|
||||
mainFieldName,
|
||||
]);
|
||||
|
||||
draftState.displayedHeaders.push({
|
||||
...header,
|
||||
metadatas: {
|
||||
...metas,
|
||||
mainField: {
|
||||
...mainFieldAttribute,
|
||||
name: mainFieldName,
|
||||
},
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'relation':
|
||||
draftState.displayedHeaders.push({
|
||||
...header,
|
||||
queryInfos: {
|
||||
defaultParams: {},
|
||||
endPoint: `collection-types/${uid}`,
|
||||
},
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
draftState.displayedHeaders.push(header);
|
||||
}
|
||||
} else {
|
||||
drafState.displayedHeaders = state.displayedHeaders.filter(
|
||||
draftState.displayedHeaders = state.displayedHeaders.filter(
|
||||
header => header.name !== name
|
||||
);
|
||||
}
|
||||
@ -79,23 +112,24 @@ const listViewReducer = (state = initialState, action) =>
|
||||
break;
|
||||
}
|
||||
case ON_RESET_LIST_HEADERS: {
|
||||
drafState.displayedHeaders = state.initialDisplayedHeaders;
|
||||
draftState.displayedHeaders = state.initialDisplayedHeaders;
|
||||
break;
|
||||
}
|
||||
case RESET_PROPS: {
|
||||
return initialState;
|
||||
}
|
||||
case SET_LIST_LAYOUT: {
|
||||
const { contentType, displayedHeaders } = action;
|
||||
const { contentType, components, displayedHeaders } = action;
|
||||
|
||||
drafState.contentType = contentType;
|
||||
drafState.displayedHeaders = displayedHeaders;
|
||||
drafState.initialDisplayedHeaders = displayedHeaders;
|
||||
draftState.contentType = contentType;
|
||||
draftState.components = components;
|
||||
draftState.displayedHeaders = displayedHeaders;
|
||||
draftState.initialDisplayedHeaders = displayedHeaders;
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return drafState;
|
||||
return draftState;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -10,6 +10,7 @@ describe('CONTENT MANAGER | CONTAINERS | ListView | reducer', () => {
|
||||
state = {
|
||||
data: [],
|
||||
isLoading: true,
|
||||
components: [],
|
||||
contentType: {},
|
||||
initialDisplayedHeaders: [],
|
||||
displayedHeaders: [],
|
||||
|
||||
@ -21,7 +21,7 @@ const ListViewLayout = ({ layout, ...props }) => {
|
||||
}, [rawQuery, replace, redirectionLink]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(setLayout(layout.contentType));
|
||||
dispatch(setLayout(layout));
|
||||
}, [dispatch, layout]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@ -7,7 +7,7 @@ const checkIfAttributeIsDisplayable = attribute => {
|
||||
return !toLower(attribute.relationType).includes('morph');
|
||||
}
|
||||
|
||||
return !['json', 'component', 'dynamiczone', 'richtext', 'password'].includes(type) && !!type;
|
||||
return !['json', 'dynamiczone', 'richtext', 'password'].includes(type) && !!type;
|
||||
};
|
||||
|
||||
export default checkIfAttributeIsDisplayable;
|
||||
|
||||
@ -448,7 +448,9 @@
|
||||
"components.popUpWarning.message": "Are you sure you want to delete this?",
|
||||
"components.popUpWarning.title": "Please confirm",
|
||||
"content-manager.App.schemas.data-loaded": "The schemas have been successfully loaded",
|
||||
"content-manager.DynamicTable.relation-loaded": "The relations have been loaded",
|
||||
"content-manager.DynamicTable.relation-loaded": "Relations have been loaded",
|
||||
"content-manager.DynamicTable.relation-loading": "Relations are loading",
|
||||
"content-manager.DynamicTable.relation-more": "This relation contains more entities than displayed",
|
||||
"content-manager.EditRelations.title": "Relational data",
|
||||
"content-manager.HeaderLayout.button.label-add-entry": "Create new entry",
|
||||
"content-manager.api.id": "API ID",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user