diff --git a/examples/getstarted/api/address/content-types/address/schema.json b/examples/getstarted/api/address/content-types/address/schema.json
index 94a41e2ca1..1121090c1d 100755
--- a/examples/getstarted/api/address/content-types/address/schema.json
+++ b/examples/getstarted/api/address/content-types/address/schema.json
@@ -115,6 +115,30 @@
"component": "blog.test-como",
"required": false,
"min": 2
+ },
+ "temp_one_to_one": {
+ "type": "relation",
+ "relation": "oneToOne",
+ "target": "api::temp.temp",
+ "inversedBy": "one_to_one"
+ },
+ "temp": {
+ "type": "relation",
+ "relation": "manyToOne",
+ "target": "api::temp.temp",
+ "inversedBy": "one_to_many"
+ },
+ "temps": {
+ "type": "relation",
+ "relation": "oneToMany",
+ "target": "api::temp.temp",
+ "mappedBy": "many_to_one"
+ },
+ "many_to_many_temp": {
+ "type": "relation",
+ "relation": "manyToMany",
+ "target": "api::temp.temp",
+ "inversedBy": "many_to_many"
}
}
}
diff --git a/examples/getstarted/api/category/content-types/category/schema.json b/examples/getstarted/api/category/content-types/category/schema.json
index 56edad7ff4..dcec9a58e4 100755
--- a/examples/getstarted/api/category/content-types/category/schema.json
+++ b/examples/getstarted/api/category/content-types/category/schema.json
@@ -5,15 +5,26 @@
"displayName": "Category",
"singularName": "category",
"pluralName": "categories",
- "description": ""
+ "description": "",
+ "name": "Category"
},
"options": {
"draftAndPublish": true,
"comment": ""
},
+ "pluginOptions": {
+ "i18n": {
+ "localized": true
+ }
+ },
"attributes": {
"name": {
- "type": "string"
+ "type": "string",
+ "pluginOptions": {
+ "i18n": {
+ "localized": true
+ }
+ }
},
"addresses": {
"type": "relation",
diff --git a/examples/getstarted/api/temp/content-types/temp/schema.json b/examples/getstarted/api/temp/content-types/temp/schema.json
new file mode 100644
index 0000000000..41e854efd1
--- /dev/null
+++ b/examples/getstarted/api/temp/content-types/temp/schema.json
@@ -0,0 +1,115 @@
+{
+ "kind": "collectionType",
+ "collectionName": "temps",
+ "info": {
+ "singularName": "temp",
+ "pluralName": "temps",
+ "displayName": "temp",
+ "name": "temp",
+ "description": ""
+ },
+ "options": {
+ "draftAndPublish": true
+ },
+ "pluginOptions": {},
+ "attributes": {
+ "short": {
+ "type": "string"
+ },
+ "long": {
+ "type": "string"
+ },
+ "email": {
+ "type": "email"
+ },
+ "rich": {
+ "type": "richtext"
+ },
+ "password": {
+ "type": "password"
+ },
+ "number": {
+ "type": "integer"
+ },
+ "enum": {
+ "type": "enumeration",
+ "enum": [
+ "one",
+ "two",
+ "three"
+ ]
+ },
+ "date": {
+ "type": "date"
+ },
+ "datetime": {
+ "type": "datetime"
+ },
+ "time": {
+ "type": "time"
+ },
+ "single": {
+ "type": "media",
+ "multiple": false,
+ "required": false,
+ "allowedTypes": [
+ "images",
+ "files",
+ "videos"
+ ]
+ },
+ "multiple": {
+ "type": "media",
+ "multiple": true,
+ "required": false,
+ "allowedTypes": [
+ "images",
+ "files",
+ "videos"
+ ]
+ },
+ "bool": {
+ "type": "boolean"
+ },
+ "json": {
+ "type": "json"
+ },
+ "uid": {
+ "type": "uid"
+ },
+ "one_way": {
+ "type": "relation",
+ "relation": "oneToOne",
+ "target": "api::address.address"
+ },
+ "one_to_one": {
+ "type": "relation",
+ "relation": "oneToOne",
+ "target": "api::address.address",
+ "inversedBy": "temp_one_to_one"
+ },
+ "one_to_many": {
+ "type": "relation",
+ "relation": "oneToMany",
+ "target": "api::address.address",
+ "mappedBy": "temp"
+ },
+ "many_to_one": {
+ "type": "relation",
+ "relation": "manyToOne",
+ "target": "api::address.address",
+ "inversedBy": "temps"
+ },
+ "many_to_many": {
+ "type": "relation",
+ "relation": "manyToMany",
+ "target": "api::address.address",
+ "inversedBy": "many_to_many_temp"
+ },
+ "many_way": {
+ "type": "relation",
+ "relation": "oneToMany",
+ "target": "api::address.address"
+ }
+ }
+}
diff --git a/examples/getstarted/api/temp/controllers/temp.js b/examples/getstarted/api/temp/controllers/temp.js
new file mode 100644
index 0000000000..0fcb4071e7
--- /dev/null
+++ b/examples/getstarted/api/temp/controllers/temp.js
@@ -0,0 +1,15 @@
+'use strict';
+
+/**
+ * A set of functions called "actions" for `temp`
+ */
+
+module.exports = {
+ // exampleAction: async (ctx, next) => {
+ // try {
+ // ctx.body = 'ok';
+ // } catch (err) {
+ // ctx.body = err;
+ // }
+ // }
+};
diff --git a/examples/getstarted/api/temp/routes/temp.js b/examples/getstarted/api/temp/routes/temp.js
new file mode 100644
index 0000000000..ee3fc38343
--- /dev/null
+++ b/examples/getstarted/api/temp/routes/temp.js
@@ -0,0 +1,44 @@
+module.exports = {
+ routes: [
+ {
+ method: 'GET',
+ path: '/temps',
+ handler: 'temp.find',
+ config: {
+ policies: [],
+ },
+ },
+ {
+ method: 'GET',
+ path: '/temps/:id',
+ handler: 'temp.findOne',
+ config: {
+ policies: [],
+ },
+ },
+ {
+ method: 'POST',
+ path: '/temps',
+ handler: 'temp.create',
+ config: {
+ policies: [],
+ },
+ },
+ {
+ method: 'PUT',
+ path: '/temps/:id',
+ handler: 'temp.update',
+ config: {
+ policies: [],
+ },
+ },
+ {
+ method: 'DELETE',
+ path: '/temps/:id',
+ handler: 'temp.delete',
+ config: {
+ policies: [],
+ },
+ },
+ ],
+};
diff --git a/packages/core/admin/admin/src/content-manager/components/DynamicTable/CellContent/Media.js b/packages/core/admin/admin/src/content-manager/components/DynamicTable/CellContent/Media.js
new file mode 100644
index 0000000000..629f84fa86
--- /dev/null
+++ b/packages/core/admin/admin/src/content-manager/components/DynamicTable/CellContent/Media.js
@@ -0,0 +1,36 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Avatar } from '@strapi/parts/Avatar';
+import { Text } from '@strapi/parts/Text';
+import { getFileExtension, prefixFileUrlWithBackendUrl } from '@strapi/helper-plugin';
+
+const Media = ({ url, mime, alternativeText, name, ext, formats }) => {
+ const fileURL = prefixFileUrlWithBackendUrl(url);
+
+ if (mime.includes('image')) {
+ const thumbnail = formats?.thumbnail?.url || null;
+ const mediaURL = prefixFileUrlWithBackendUrl(thumbnail) || fileURL;
+
+ return ;
+ }
+
+ const fileExtension = getFileExtension(ext);
+
+ return {fileExtension};
+};
+
+Media.defaultProps = {
+ alternativeText: null,
+ formats: null,
+};
+
+Media.propTypes = {
+ alternativeText: PropTypes.string,
+ ext: PropTypes.string.isRequired,
+ formats: PropTypes.object,
+ mime: PropTypes.string.isRequired,
+ name: PropTypes.string.isRequired,
+ url: PropTypes.string.isRequired,
+};
+
+export default Media;
diff --git a/packages/core/admin/admin/src/content-manager/components/DynamicTable/CellContent/MultipleMedias.js b/packages/core/admin/admin/src/content-manager/components/DynamicTable/CellContent/MultipleMedias.js
new file mode 100644
index 0000000000..26daaceb98
--- /dev/null
+++ b/packages/core/admin/admin/src/content-manager/components/DynamicTable/CellContent/MultipleMedias.js
@@ -0,0 +1,8 @@
+import React from 'react';
+import { Text } from '@strapi/parts/Text';
+
+const MultipleMedia = () => {
+ return TODO;
+};
+
+export default MultipleMedia;
diff --git a/packages/core/admin/admin/src/content-manager/components/DynamicTable/CellContent/Relation/PopoverContent.js b/packages/core/admin/admin/src/content-manager/components/DynamicTable/CellContent/Relation/PopoverContent.js
new file mode 100644
index 0000000000..4b0a23b878
--- /dev/null
+++ b/packages/core/admin/admin/src/content-manager/components/DynamicTable/CellContent/Relation/PopoverContent.js
@@ -0,0 +1,78 @@
+import React from 'react';
+import { useQuery } from 'react-query';
+import { useIntl } from 'react-intl';
+import PropTypes from 'prop-types';
+import { Box } from '@strapi/parts/Box';
+import { Text } from '@strapi/parts/Text';
+import { Loader } from '@strapi/parts/Loader';
+import { useNotifyAT } from '@strapi/parts/LiveRegions';
+import { axiosInstance } from '../../../../../core/utils';
+import { getRequestUrl } from '../../../../utils';
+import formatDisplayedValue from '../utils/formatDisplayedValue';
+
+const fetchRelation = async (endPoint, notifyStatus) => {
+ const {
+ data: { results, pagination },
+ } = await axiosInstance.get(endPoint);
+
+ notifyStatus('The relations has been loaded');
+
+ return { results, pagination };
+};
+
+const PopoverContent = ({ fieldSchema, name, rowId, targetModel, queryInfos }) => {
+ const requestURL = getRequestUrl(`${queryInfos.endPoint}/${rowId}/${name}`);
+
+ const { formatDate, formatTime, formatNumber, formatMessage } = useIntl();
+ const { notifyStatus } = useNotifyAT();
+
+ const { data, status } = useQuery([targetModel, rowId], () =>
+ fetchRelation(requestURL, notifyStatus)
+ );
+
+ if (status !== 'success') {
+ return (
+
+ Loading content
+
+ );
+ }
+
+ return (
+
+ {data?.results.map(entry => {
+ const displayedValue = entry[fieldSchema.name]
+ ? formatDisplayedValue(entry[fieldSchema.name], fieldSchema.type, {
+ formatDate,
+ formatTime,
+ formatNumber,
+ formatMessage,
+ })
+ : '-';
+
+ return (
+
+ {displayedValue}
+
+ );
+ })}
+ {data?.pagination.total > 10 && (
+
+ [...]
+
+ )}
+
+ );
+};
+
+PopoverContent.propTypes = {
+ fieldSchema: PropTypes.shape({ name: PropTypes.string, type: PropTypes.string }).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;
diff --git a/packages/core/admin/admin/src/content-manager/components/DynamicTable/CellContent/Relation/index.js b/packages/core/admin/admin/src/content-manager/components/DynamicTable/CellContent/Relation/index.js
new file mode 100644
index 0000000000..37e44c76b4
--- /dev/null
+++ b/packages/core/admin/admin/src/content-manager/components/DynamicTable/CellContent/Relation/index.js
@@ -0,0 +1,114 @@
+import React, { useState, useRef } from 'react';
+import PropTypes from 'prop-types';
+import { useIntl } from 'react-intl';
+import toString from 'lodash/toString';
+import { IconButton } from '@strapi/parts/IconButton';
+import { Text } from '@strapi/parts/Text';
+import { Box } from '@strapi/parts/Box';
+import { Badge } from '@strapi/parts/Badge';
+import { Row } from '@strapi/parts/Row';
+import { Popover } from '@strapi/parts/Popover';
+import { SortIcon } from '@strapi/helper-plugin';
+import styled from 'styled-components';
+import formatDisplayedValue from '../utils/formatDisplayedValue';
+import PopoverContent from './PopoverContent';
+
+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 { formatDate, formatTime, formatNumber, formatMessage } = useIntl();
+ const [visible, setVisible] = useState(false);
+ const buttonRef = useRef();
+
+ if (SINGLE_RELATIONS.includes(fieldSchema.relation)) {
+ const formattedValue = formatDisplayedValue(
+ value[metadatas.mainField.name],
+ metadatas.mainField.schema.type,
+ {
+ formatDate,
+ formatTime,
+ formatNumber,
+ }
+ );
+
+ return {toString(formattedValue)};
+ }
+
+ const handleTogglePopover = () => setVisible(prev => !prev);
+
+ return (
+
+ {value.count}
+
+
+ {formatMessage(
+ {
+ id: 'content-manager.containers.ListPage.items',
+ defaultMessage: '{number, plural, =0 {items} one {item} other {items}}',
+ },
+ { number: value.count }
+ )}
+
+
+ {value.count > 0 && (
+
+ }
+ />
+ {visible && (
+
+
+
+ )}
+
+ )}
+
+ );
+};
+
+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;
diff --git a/packages/core/admin/admin/src/content-manager/components/DynamicTable/CellContent/index.js b/packages/core/admin/admin/src/content-manager/components/DynamicTable/CellContent/index.js
new file mode 100644
index 0000000000..8dfd744c3c
--- /dev/null
+++ b/packages/core/admin/admin/src/content-manager/components/DynamicTable/CellContent/index.js
@@ -0,0 +1,63 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Text } from '@strapi/parts/Text';
+import { useIntl } from 'react-intl';
+import toString from 'lodash/toString';
+import formatDisplayedValue from './utils/formatDisplayedValue';
+import Media from './Media';
+import MultipleMedias from './MultipleMedias';
+import Relation from './Relation';
+
+const CellContent = ({ content, fieldSchema, metadatas, name, queryInfos, rowId }) => {
+ const { formatDate, formatTime, formatNumber } = useIntl();
+
+ if (content === null || content === undefined) {
+ return -;
+ }
+
+ let formattedContent = formatDisplayedValue(content, fieldSchema.type, {
+ formatDate,
+ formatTime,
+ formatNumber,
+ });
+
+ if (fieldSchema.type === 'media' && !fieldSchema.multiple) {
+ return ;
+ }
+
+ if (fieldSchema.type === 'media' && fieldSchema.multiple) {
+ return ;
+ }
+
+ if (fieldSchema.type === 'relation') {
+ return (
+
+ );
+ }
+
+ return {toString(formattedContent)};
+};
+
+CellContent.defaultProps = {
+ content: undefined,
+ queryInfos: undefined,
+};
+
+CellContent.propTypes = {
+ content: PropTypes.any,
+ fieldSchema: PropTypes.shape({ multiple: PropTypes.bool, type: PropTypes.string.isRequired })
+ .isRequired,
+ metadatas: PropTypes.object.isRequired,
+ name: PropTypes.string.isRequired,
+ rowId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
+ queryInfos: PropTypes.shape({ endPoint: PropTypes.string.isRequired }),
+};
+
+export default CellContent;
diff --git a/packages/core/admin/admin/src/content-manager/components/DynamicTable/CellContent/utils/formatDisplayedValue.js b/packages/core/admin/admin/src/content-manager/components/DynamicTable/CellContent/utils/formatDisplayedValue.js
new file mode 100644
index 0000000000..e8273b87f6
--- /dev/null
+++ b/packages/core/admin/admin/src/content-manager/components/DynamicTable/CellContent/utils/formatDisplayedValue.js
@@ -0,0 +1,32 @@
+const formatDisplayedValue = (value, type, formatters) => {
+ let formattedValue = value;
+
+ if (type === 'date') {
+ formattedValue = formatters.formatDate(value, { dateStyle: 'full' });
+ }
+
+ if (type === 'datetime') {
+ formattedValue = formatters.formatDate(value, { dateStyle: 'full', timeStyle: 'short' });
+ }
+
+ if (type === 'time') {
+ const [hour, minute, second] = value.split(':');
+ const date = new Date();
+ date.setHours(hour);
+ date.setMinutes(minute);
+ date.setSeconds(second);
+
+ formattedValue = formatters.formatTime(date, {
+ numeric: 'auto',
+ style: 'short',
+ });
+ }
+
+ if (['float', 'integer', 'biginteger', 'decimal'].includes(type)) {
+ formattedValue = formatters.formatNumber(value);
+ }
+
+ return formattedValue;
+};
+
+export default formatDisplayedValue;
diff --git a/packages/core/admin/admin/src/content-manager/components/DynamicTable/TableRows/index.js b/packages/core/admin/admin/src/content-manager/components/DynamicTable/TableRows/index.js
index 30956d7143..58719fdceb 100644
--- a/packages/core/admin/admin/src/content-manager/components/DynamicTable/TableRows/index.js
+++ b/packages/core/admin/admin/src/content-manager/components/DynamicTable/TableRows/index.js
@@ -1,13 +1,12 @@
import React from 'react';
import PropTypes from 'prop-types';
-import { BaseCheckbox, Box, IconButton, Tbody, Td, Text, Tr, Row } from '@strapi/parts';
+import { BaseCheckbox, Box, IconButton, Tbody, Td, Tr, Row } from '@strapi/parts';
import { EditIcon, DeleteIcon, Duplicate } from '@strapi/icons';
import { useTracking } from '@strapi/helper-plugin';
import { useHistory } from 'react-router-dom';
import { useIntl } from 'react-intl';
import { usePluginsQueryParams } from '../../../hooks';
-// import { Tooltip } from '@strapi/parts/Tooltip';
-// import toString from 'lodash/toString';
+import CellContent from '../CellContent';
const TableRows = ({
canCreate,
@@ -25,6 +24,7 @@ const TableRows = ({
location: { pathname },
} = useHistory();
const { formatMessage } = useIntl();
+
const { trackUsage } = useTracking();
const pluginsQueryParams = usePluginsQueryParams();
@@ -65,7 +65,7 @@ const TableRows = ({
{typeof cellFormatter === 'function' ? (
cellFormatter(data, { key, name, ...rest })
) : (
- {data[name] || '-'}
+
)}
);
diff --git a/packages/core/admin/admin/src/content-manager/pages/ListView/index.js b/packages/core/admin/admin/src/content-manager/pages/ListView/index.js
index 8ef28c8db7..7641dbd60a 100644
--- a/packages/core/admin/admin/src/content-manager/pages/ListView/index.js
+++ b/packages/core/admin/admin/src/content-manager/pages/ListView/index.js
@@ -169,7 +169,7 @@ function ListView({
'{number, plural, =1 {# entry has} other {# entries have}} successfully been loaded',
},
// Using the plural form
- { number: 2 }
+ { number: pagination.count }
)
);
diff --git a/packages/core/admin/admin/src/translations/en.json b/packages/core/admin/admin/src/translations/en.json
index 968baa0ca1..a0e7e3238b 100644
--- a/packages/core/admin/admin/src/translations/en.json
+++ b/packages/core/admin/admin/src/translations/en.json
@@ -480,6 +480,7 @@
"content-manager.containers.Edit.returnList": "Return to list",
"content-manager.containers.Edit.seeDetails": "Details",
"content-manager.containers.Edit.submit": "Save",
+ "content-manager.popover.display-relations.label": "Display relations",
"content-manager.containers.EditSettingsView.modal-form.edit-field": "Edit the field",
"content-manager.containers.EditView.add.new": "ADD NEW ENTRY",
"content-manager.containers.EditView.components.missing.plural": "There is {count} missing components",
@@ -495,8 +496,7 @@
"content-manager.pages.ListView.header-subtitle": "{number, plural, =0 {# entries} one {# entry} other {# entries}} found",
"content-manager.containers.List.published": "Published",
"content-manager.containers.ListPage.displayedFields": "Displayed Fields",
- "content-manager.containers.ListPage.items.plural": "items",
- "content-manager.containers.ListPage.items.singular": "item",
+ "content-manager.containers.ListPage.items": "{number, plural, =0 {items} one {item} other {items}}",
"content-manager.containers.ListPage.table-headers.published_at": "State",
"content-manager.containers.ListSettingsView.modal-form.edit-label": "Edit the label",
"content-manager.containers.SettingPage.add.field": "Insert another field",