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 ( + + ); +}; + +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",