mirror of
https://github.com/strapi/strapi.git
synced 2025-11-08 14:19:40 +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(() => ({
|
'content-manager_listView': jest.fn(() => ({
|
||||||
data: [],
|
data: [],
|
||||||
isLoading: true,
|
isLoading: true,
|
||||||
|
components: [],
|
||||||
contentType: {},
|
contentType: {},
|
||||||
initialDisplayedHeaders: [],
|
initialDisplayedHeaders: [],
|
||||||
displayedHeaders: [],
|
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,29 +4,40 @@ import styled from 'styled-components';
|
|||||||
import { Typography } from '@strapi/design-system/Typography';
|
import { Typography } from '@strapi/design-system/Typography';
|
||||||
import Media from './Media';
|
import Media from './Media';
|
||||||
import MultipleMedias from './MultipleMedias';
|
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 CellValue from './CellValue';
|
||||||
|
import hasContent from './utils/hasContent';
|
||||||
|
import isSingleRelation from './utils/isSingleRelation';
|
||||||
|
|
||||||
const TypographyMaxWidth = styled(Typography)`
|
const TypographyMaxWidth = styled(Typography)`
|
||||||
max-width: 300px;
|
max-width: 300px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const CellContent = ({ content, fieldSchema, metadatas, name, queryInfos, rowId }) => {
|
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>;
|
return <Typography textColor="neutral800">-</Typography>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fieldSchema.type === 'media' && !fieldSchema.multiple) {
|
switch (type) {
|
||||||
|
case 'media':
|
||||||
|
if (!fieldSchema.multiple) {
|
||||||
return <Media {...content} />;
|
return <Media {...content} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fieldSchema.type === 'media' && fieldSchema.multiple) {
|
|
||||||
return <MultipleMedias value={content} />;
|
return <MultipleMedias value={content} />;
|
||||||
|
|
||||||
|
case 'relation': {
|
||||||
|
if (isSingleRelation(fieldSchema.relation)) {
|
||||||
|
return <RelationSingle metadatas={metadatas} value={content} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fieldSchema.type === 'relation') {
|
|
||||||
return (
|
return (
|
||||||
<Relation
|
<RelationMultiple
|
||||||
fieldSchema={fieldSchema}
|
fieldSchema={fieldSchema}
|
||||||
queryInfos={queryInfos}
|
queryInfos={queryInfos}
|
||||||
metadatas={metadatas}
|
metadatas={metadatas}
|
||||||
@ -37,11 +48,20 @@ const CellContent = ({ content, fieldSchema, metadatas, name, queryInfos, rowId
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'component':
|
||||||
|
if (fieldSchema.repeatable === true) {
|
||||||
|
return <RepeatableComponent value={content} metadatas={metadatas} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <SingleComponent value={content} metadatas={metadatas} />;
|
||||||
|
|
||||||
|
default:
|
||||||
return (
|
return (
|
||||||
<TypographyMaxWidth ellipsis textColor="neutral800">
|
<TypographyMaxWidth ellipsis textColor="neutral800">
|
||||||
<CellValue type={fieldSchema.type} value={content} />
|
<CellValue type={type} value={content} />
|
||||||
</TypographyMaxWidth>
|
</TypographyMaxWidth>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
CellContent.defaultProps = {
|
CellContent.defaultProps = {
|
||||||
@ -51,8 +71,13 @@ CellContent.defaultProps = {
|
|||||||
|
|
||||||
CellContent.propTypes = {
|
CellContent.propTypes = {
|
||||||
content: PropTypes.any,
|
content: PropTypes.any,
|
||||||
fieldSchema: PropTypes.shape({ multiple: PropTypes.bool, type: PropTypes.string.isRequired })
|
fieldSchema: PropTypes.shape({
|
||||||
.isRequired,
|
component: PropTypes.string,
|
||||||
|
multiple: PropTypes.bool,
|
||||||
|
type: PropTypes.string.isRequired,
|
||||||
|
repeatable: PropTypes.bool,
|
||||||
|
relation: PropTypes.string,
|
||||||
|
}).isRequired,
|
||||||
metadatas: PropTypes.object.isRequired,
|
metadatas: PropTypes.object.isRequired,
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
rowId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).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 formattedCTEditLayout = formatLayoutWithMetas(data.contentType, null, models);
|
||||||
const ctUid = data.contentType.uid;
|
const ctUid = data.contentType.uid;
|
||||||
const formattedEditRelationsLayout = formatEditRelationsLayoutWithMetas(data.contentType, models);
|
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', 'edit'], formattedCTEditLayout);
|
||||||
set(data, ['contentType', 'layouts', 'editRelations'], formattedEditRelationsLayout);
|
set(data, ['contentType', 'layouts', 'editRelations'], formattedEditRelationsLayout);
|
||||||
@ -146,7 +146,7 @@ const formatLayoutWithMetas = (contentTypeConfiguration, ctUid, models) => {
|
|||||||
return formatted;
|
return formatted;
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatListLayoutWithMetas = contentTypeConfiguration => {
|
const formatListLayoutWithMetas = (contentTypeConfiguration, components) => {
|
||||||
const formatted = contentTypeConfiguration.layouts.list.reduce((acc, current) => {
|
const formatted = contentTypeConfiguration.layouts.list.reduce((acc, current) => {
|
||||||
const fieldSchema = get(contentTypeConfiguration, ['attributes', current], {});
|
const fieldSchema = get(contentTypeConfiguration, ['attributes', current], {});
|
||||||
const metadatas = get(contentTypeConfiguration, ['metadatas', current, 'list'], {});
|
const metadatas = get(contentTypeConfiguration, ['metadatas', current, 'list'], {});
|
||||||
@ -164,6 +164,27 @@ const formatListLayoutWithMetas = contentTypeConfiguration => {
|
|||||||
return acc;
|
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 });
|
acc.push({ key: `__${current}_key__`, name: current, fieldSchema, metadatas });
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
|
|||||||
@ -485,12 +485,22 @@ describe('Content Manager | hooks | useFetchContentTypeLayout | utils ', () => {
|
|||||||
const data = {
|
const data = {
|
||||||
uid: 'address',
|
uid: 'address',
|
||||||
layouts: {
|
layouts: {
|
||||||
list: ['test', 'categories'],
|
list: ['test', 'categories', 'component'],
|
||||||
},
|
},
|
||||||
metadatas: {
|
metadatas: {
|
||||||
test: {
|
test: {
|
||||||
list: { ok: true },
|
list: { ok: true },
|
||||||
},
|
},
|
||||||
|
component: {
|
||||||
|
list: {
|
||||||
|
mainField: {
|
||||||
|
name: 'name',
|
||||||
|
schema: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
categories: {
|
categories: {
|
||||||
list: {
|
list: {
|
||||||
ok: true,
|
ok: true,
|
||||||
@ -509,6 +519,22 @@ describe('Content Manager | hooks | useFetchContentTypeLayout | utils ', () => {
|
|||||||
type: 'relation',
|
type: 'relation',
|
||||||
targetModel: 'category',
|
targetModel: 'category',
|
||||||
},
|
},
|
||||||
|
component: {
|
||||||
|
type: 'component',
|
||||||
|
component: 'some.component',
|
||||||
|
repeatable: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const components = {
|
||||||
|
'some.component': {
|
||||||
|
settings: {
|
||||||
|
mainField: 'name',
|
||||||
|
},
|
||||||
|
|
||||||
|
attributes: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const expected = [
|
const expected = [
|
||||||
@ -533,9 +559,23 @@ describe('Content Manager | hooks | useFetchContentTypeLayout | utils ', () => {
|
|||||||
fieldSchema: { type: 'relation', targetModel: 'category' },
|
fieldSchema: { type: 'relation', targetModel: 'category' },
|
||||||
queryInfos: { defaultParams: {}, endPoint: 'collection-types/address' },
|
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 };
|
return { type: RESET_PROPS };
|
||||||
}
|
}
|
||||||
|
|
||||||
export const setLayout = contentType => {
|
export const setLayout = ({ components, contentType }) => {
|
||||||
const { layouts } = contentType;
|
const { layouts } = contentType;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
contentType,
|
contentType,
|
||||||
|
components,
|
||||||
displayedHeaders: layouts.list,
|
displayedHeaders: layouts.list,
|
||||||
type: SET_LIST_LAYOUT,
|
type: SET_LIST_LAYOUT,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import produce from 'immer';
|
import produce from 'immer';
|
||||||
|
import get from 'lodash/get';
|
||||||
import {
|
import {
|
||||||
GET_DATA,
|
GET_DATA,
|
||||||
GET_DATA_SUCCEEDED,
|
GET_DATA_SUCCEEDED,
|
||||||
@ -17,6 +18,7 @@ export const initialState = {
|
|||||||
data: [],
|
data: [],
|
||||||
isLoading: true,
|
isLoading: true,
|
||||||
contentType: {},
|
contentType: {},
|
||||||
|
components: [],
|
||||||
initialDisplayedHeaders: [],
|
initialDisplayedHeaders: [],
|
||||||
displayedHeaders: [],
|
displayedHeaders: [],
|
||||||
pagination: {
|
pagination: {
|
||||||
@ -26,21 +28,22 @@ export const initialState = {
|
|||||||
|
|
||||||
const listViewReducer = (state = initialState, action) =>
|
const listViewReducer = (state = initialState, action) =>
|
||||||
// eslint-disable-next-line consistent-return
|
// eslint-disable-next-line consistent-return
|
||||||
produce(state, drafState => {
|
produce(state, draftState => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case GET_DATA: {
|
case GET_DATA: {
|
||||||
return {
|
return {
|
||||||
...initialState,
|
...initialState,
|
||||||
contentType: state.contentType,
|
contentType: state.contentType,
|
||||||
|
components: state.components,
|
||||||
initialDisplayedHeaders: state.initialDisplayedHeaders,
|
initialDisplayedHeaders: state.initialDisplayedHeaders,
|
||||||
displayedHeaders: state.displayedHeaders,
|
displayedHeaders: state.displayedHeaders,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
case GET_DATA_SUCCEEDED: {
|
case GET_DATA_SUCCEEDED: {
|
||||||
drafState.pagination = action.pagination;
|
draftState.pagination = action.pagination;
|
||||||
drafState.data = action.data;
|
draftState.data = action.data;
|
||||||
drafState.isLoading = false;
|
draftState.isLoading = false;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,19 +62,49 @@ const listViewReducer = (state = initialState, action) =>
|
|||||||
key: `__${name}_key__`,
|
key: `__${name}_key__`,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (attributes[name].type === 'relation') {
|
switch (attributes[name].type) {
|
||||||
drafState.displayedHeaders.push({
|
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,
|
...header,
|
||||||
queryInfos: {
|
queryInfos: {
|
||||||
defaultParams: {},
|
defaultParams: {},
|
||||||
endPoint: `collection-types/${uid}`,
|
endPoint: `collection-types/${uid}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else {
|
break;
|
||||||
drafState.displayedHeaders.push(header);
|
|
||||||
|
default:
|
||||||
|
draftState.displayedHeaders.push(header);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
drafState.displayedHeaders = state.displayedHeaders.filter(
|
draftState.displayedHeaders = state.displayedHeaders.filter(
|
||||||
header => header.name !== name
|
header => header.name !== name
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -79,23 +112,24 @@ const listViewReducer = (state = initialState, action) =>
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case ON_RESET_LIST_HEADERS: {
|
case ON_RESET_LIST_HEADERS: {
|
||||||
drafState.displayedHeaders = state.initialDisplayedHeaders;
|
draftState.displayedHeaders = state.initialDisplayedHeaders;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case RESET_PROPS: {
|
case RESET_PROPS: {
|
||||||
return initialState;
|
return initialState;
|
||||||
}
|
}
|
||||||
case SET_LIST_LAYOUT: {
|
case SET_LIST_LAYOUT: {
|
||||||
const { contentType, displayedHeaders } = action;
|
const { contentType, components, displayedHeaders } = action;
|
||||||
|
|
||||||
drafState.contentType = contentType;
|
draftState.contentType = contentType;
|
||||||
drafState.displayedHeaders = displayedHeaders;
|
draftState.components = components;
|
||||||
drafState.initialDisplayedHeaders = displayedHeaders;
|
draftState.displayedHeaders = displayedHeaders;
|
||||||
|
draftState.initialDisplayedHeaders = displayedHeaders;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return drafState;
|
return draftState;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -10,6 +10,7 @@ describe('CONTENT MANAGER | CONTAINERS | ListView | reducer', () => {
|
|||||||
state = {
|
state = {
|
||||||
data: [],
|
data: [],
|
||||||
isLoading: true,
|
isLoading: true,
|
||||||
|
components: [],
|
||||||
contentType: {},
|
contentType: {},
|
||||||
initialDisplayedHeaders: [],
|
initialDisplayedHeaders: [],
|
||||||
displayedHeaders: [],
|
displayedHeaders: [],
|
||||||
|
|||||||
@ -21,7 +21,7 @@ const ListViewLayout = ({ layout, ...props }) => {
|
|||||||
}, [rawQuery, replace, redirectionLink]);
|
}, [rawQuery, replace, redirectionLink]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(setLayout(layout.contentType));
|
dispatch(setLayout(layout));
|
||||||
}, [dispatch, layout]);
|
}, [dispatch, layout]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@ -7,7 +7,7 @@ const checkIfAttributeIsDisplayable = attribute => {
|
|||||||
return !toLower(attribute.relationType).includes('morph');
|
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;
|
export default checkIfAttributeIsDisplayable;
|
||||||
|
|||||||
@ -448,7 +448,9 @@
|
|||||||
"components.popUpWarning.message": "Are you sure you want to delete this?",
|
"components.popUpWarning.message": "Are you sure you want to delete this?",
|
||||||
"components.popUpWarning.title": "Please confirm",
|
"components.popUpWarning.title": "Please confirm",
|
||||||
"content-manager.App.schemas.data-loaded": "The schemas have been successfully loaded",
|
"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.EditRelations.title": "Relational data",
|
||||||
"content-manager.HeaderLayout.button.label-add-entry": "Create new entry",
|
"content-manager.HeaderLayout.button.label-add-entry": "Create new entry",
|
||||||
"content-manager.api.id": "API ID",
|
"content-manager.api.id": "API ID",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user