+
{formatMessage({
id: getTrad('modalForm.empty.sub-heading'),
defaultMessage:
diff --git a/packages/core/content-type-builder/admin/src/components/AttributeOptions/tests/index.test.js b/packages/core/content-type-builder/admin/src/components/AttributeOptions/tests/index.test.js
index f376410d11..faa5d58500 100644
--- a/packages/core/content-type-builder/admin/src/components/AttributeOptions/tests/index.test.js
+++ b/packages/core/content-type-builder/admin/src/components/AttributeOptions/tests/index.test.js
@@ -3,16 +3,37 @@ import { Router } from 'react-router-dom';
import { createMemoryHistory } from 'history';
import { render, screen, getByText, fireEvent } from '@testing-library/react';
import { lightTheme, ThemeProvider } from '@strapi/design-system';
-import { useCustomFields } from '@strapi/helper-plugin';
import { IntlProvider } from 'react-intl';
import FormModalNavigationProvider from '../../FormModalNavigationProvider';
import AttributeOptions from '../index';
+const mockCustomField = {
+ 'plugin::mycustomfields.test': {
+ name: 'color',
+ pluginId: 'mycustomfields',
+ type: 'text',
+ icon: jest.fn(),
+ intlLabel: {
+ id: 'mycustomfields.color.label',
+ defaultMessage: 'Color',
+ },
+ intlDescription: {
+ id: 'mycustomfields.color.description',
+ defaultMessage: 'Select any color',
+ },
+ components: {
+ Input: jest.fn(),
+ },
+ },
+};
+
+const getAll = jest.fn().mockReturnValue({});
jest.mock('@strapi/helper-plugin', () => ({
...jest.requireActual('@strapi/helper-plugin'),
- useCustomFields: jest.fn(() => ({
- getAll: jest.fn(() => ({})),
- })),
+ useCustomFields: () => ({
+ get: jest.fn().mockReturnValue(mockCustomField),
+ getAll,
+ }),
}));
const mockAttributes = [
@@ -85,6 +106,8 @@ describe('', () => {
const App = makeApp();
render(App);
+ getAll.mockReturnValueOnce({});
+
const customTab = screen.getByRole('tab', { selected: false });
fireEvent.click(customTab);
const customTabSelected = screen.getByRole('tab', { selected: true });
@@ -96,29 +119,7 @@ describe('', () => {
});
it('switches to the custom tab with custom fields', () => {
- useCustomFields.mockImplementationOnce(
- jest.fn(() => ({
- getAll: jest.fn(() => ({
- 'plugin::mycustomfields.test': {
- name: 'color',
- pluginId: 'mycustomfields',
- type: 'text',
- intlLabel: {
- id: 'mycustomfields.color.label',
- defaultMessage: 'Color',
- },
- intlDescription: {
- id: 'mycustomfields.color.description',
- defaultMessage: 'Select any color',
- },
- components: {
- Input: jest.fn(),
- },
- },
- })),
- }))
- );
-
+ getAll.mockReturnValue(mockCustomField);
const App = makeApp();
render(App);
diff --git a/packages/core/content-type-builder/admin/src/components/ListRow/DisplayedType.js b/packages/core/content-type-builder/admin/src/components/ListRow/DisplayedType.js
new file mode 100644
index 0000000000..e74fe0bafb
--- /dev/null
+++ b/packages/core/content-type-builder/admin/src/components/ListRow/DisplayedType.js
@@ -0,0 +1,56 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { useIntl } from 'react-intl';
+import { Typography } from '@strapi/design-system/Typography';
+import getTrad from '../../utils/getTrad';
+
+const DisplayedType = ({ type, customField, repeatable }) => {
+ const { formatMessage } = useIntl();
+
+ let readableType = type;
+
+ if (['integer', 'biginteger', 'float', 'decimal'].includes(type)) {
+ readableType = 'number';
+ } else if (['string'].includes(type)) {
+ readableType = 'text';
+ }
+
+ if (customField) {
+ return (
+
+ {formatMessage({
+ id: getTrad('attribute.customField'),
+ defaultMessage: 'Custom field',
+ })}
+
+ );
+ }
+
+ return (
+
+ {formatMessage({
+ id: getTrad(`attribute.${readableType}`),
+ defaultMessage: type,
+ })}
+
+ {repeatable &&
+ formatMessage({
+ id: getTrad('component.repeatable'),
+ defaultMessage: '(repeatable)',
+ })}
+
+ );
+};
+
+DisplayedType.defaultProps = {
+ customField: null,
+ repeatable: false,
+};
+
+DisplayedType.propTypes = {
+ type: PropTypes.string.isRequired,
+ customField: PropTypes.bool,
+ repeatable: PropTypes.bool,
+};
+
+export default DisplayedType;
diff --git a/packages/core/content-type-builder/admin/src/components/ListRow/index.js b/packages/core/content-type-builder/admin/src/components/ListRow/index.js
index 8cf7c8cbff..75e9c377aa 100644
--- a/packages/core/content-type-builder/admin/src/components/ListRow/index.js
+++ b/packages/core/content-type-builder/admin/src/components/ListRow/index.js
@@ -17,9 +17,11 @@ import Curve from '../../icons/Curve';
import UpperFist from '../UpperFirst';
import BoxWrapper from './BoxWrapper';
import AttributeIcon from '../AttributeIcon';
+import DisplayedType from './DisplayedType';
function ListRow({
configurable,
+ customField,
editTarget,
firstLoopComponentUid,
isFromDynamicZone,
@@ -38,14 +40,6 @@ function ListRow({
const isMorph = type === 'relation' && relation.includes('morph');
const ico = ['integer', 'biginteger', 'float', 'decimal'].includes(type) ? 'number' : type;
- let readableType = type;
-
- if (['integer', 'biginteger', 'float', 'decimal'].includes(type)) {
- readableType = 'number';
- } else if (['string'].includes(type)) {
- readableType = 'text';
- }
-
const contentType = get(contentTypes, [target], {});
const contentTypeFriendlyName = get(contentType, ['schema', 'displayName'], '');
const isPluginContentType = get(contentType, 'plugin');
@@ -93,7 +87,7 @@ function ListRow({
|
{loopNumber !== 0 && }
-
+
{name}
|
@@ -118,18 +112,7 @@ function ListRow({
) : (
-
- {formatMessage({
- id: getTrad(`attribute.${readableType}`),
- defaultMessage: type,
- })}
-
- {repeatable &&
- formatMessage({
- id: getTrad('component.repeatable'),
- defaultMessage: '(repeatable)',
- })}
-
+
)}
@@ -184,6 +167,7 @@ function ListRow({
ListRow.defaultProps = {
configurable: true,
+ customField: null,
firstLoopComponentUid: null,
isFromDynamicZone: false,
onClick: () => {},
@@ -197,6 +181,7 @@ ListRow.defaultProps = {
ListRow.propTypes = {
configurable: PropTypes.bool,
+ customField: PropTypes.string,
editTarget: PropTypes.string.isRequired,
firstLoopComponentUid: PropTypes.string,
isFromDynamicZone: PropTypes.bool,
diff --git a/packages/core/content-type-builder/admin/src/translations/en.json b/packages/core/content-type-builder/admin/src/translations/en.json
index 0d996fe63f..0a3f36e36e 100644
--- a/packages/core/content-type-builder/admin/src/translations/en.json
+++ b/packages/core/content-type-builder/admin/src/translations/en.json
@@ -4,6 +4,7 @@
"attribute.boolean.description": "Yes or no, 1 or 0, true or false",
"attribute.component": "Component",
"attribute.component.description": "Group of fields that you can repeat or reuse",
+ "attribute.customField": "Custom field",
"attribute.date": "Date",
"attribute.date.description": "A date picker with hours, minutes and seconds",
"attribute.datetime": "Datetime",
@@ -192,4 +193,4 @@
"table.content.create-first-content-type": "Create your first Collection-Type",
"table.content.no-fields.collection-type": "Add your first field to this Collection-Type",
"table.content.no-fields.component": "Add your first field to this component"
-}
\ No newline at end of file
+}
diff --git a/packages/core/content-type-builder/server/utils/__tests__/attributes.test.js b/packages/core/content-type-builder/server/utils/__tests__/attributes.test.js
new file mode 100644
index 0000000000..66525d33ad
--- /dev/null
+++ b/packages/core/content-type-builder/server/utils/__tests__/attributes.test.js
@@ -0,0 +1,34 @@
+'use strict';
+
+const { formatAttribute } = require('../attributes');
+
+describe('format attributes', () => {
+ it('replaces type customField with the underlying data type', () => {
+ const mockAttribute = {
+ type: 'customField',
+ customField: 'plugin::mycustomfields.color',
+ };
+
+ global.strapi = {
+ container: {
+ // mock container.get('custom-fields')
+ get: jest.fn(() => ({
+ // mock container.get('custom-fields').get(uid)
+ get: jest.fn(() => ({
+ name: 'color',
+ plugin: 'mycustomfields',
+ type: 'text',
+ })),
+ })),
+ },
+ };
+
+ const formattedAttribute = formatAttribute('key', mockAttribute);
+
+ const expected = {
+ type: 'text',
+ customField: 'plugin::mycustomfields.color',
+ };
+ expect(formattedAttribute).toEqual(expected);
+ });
+});
diff --git a/packages/core/content-type-builder/server/utils/attributes.js b/packages/core/content-type-builder/server/utils/attributes.js
index e90fc1fd1a..620a537811 100644
--- a/packages/core/content-type-builder/server/utils/attributes.js
+++ b/packages/core/content-type-builder/server/utils/attributes.js
@@ -67,6 +67,15 @@ const formatAttribute = (key, attribute) => {
};
}
+ if (attribute.type === 'customField') {
+ const customField = strapi.container.get('custom-fields').get(attribute.customField);
+
+ return {
+ ...attribute,
+ type: customField.type,
+ };
+ }
+
return attribute;
};
diff --git a/packages/core/strapi/lib/core/registries/__tests__/custom-fields.test.js b/packages/core/strapi/lib/core/registries/__tests__/custom-fields.test.js
index b4330905de..60cab67922 100644
--- a/packages/core/strapi/lib/core/registries/__tests__/custom-fields.test.js
+++ b/packages/core/strapi/lib/core/registries/__tests__/custom-fields.test.js
@@ -8,99 +8,123 @@ const strapi = {
};
describe('Custom fields registry', () => {
- it('adds a custom field registered in a plugin', () => {
- const mockCF = {
- name: 'test',
- plugin: 'plugintest',
- type: 'text',
- };
+ describe('add', () => {
+ it('adds a custom field registered in a plugin', () => {
+ const mockCF = {
+ name: 'test',
+ plugin: 'plugintest',
+ type: 'text',
+ };
- const customFields = customFieldsRegistry(strapi);
- customFields.add(mockCF);
+ const customFields = customFieldsRegistry(strapi);
+ customFields.add(mockCF);
- const expected = {
- 'plugin::plugintest.test': mockCF,
- };
- expect(customFields.getAll()).toEqual(expected);
+ const expected = {
+ 'plugin::plugintest.test': mockCF,
+ };
+ expect(customFields.getAll()).toEqual(expected);
+ });
+
+ it('adds a custom field not registered in a plugin', () => {
+ const mockCF = {
+ name: 'test',
+ type: 'text',
+ };
+
+ const customFields = customFieldsRegistry(strapi);
+ customFields.add(mockCF);
+
+ const expected = {
+ 'global::test': mockCF,
+ };
+ expect(customFields.getAll()).toEqual(expected);
+ });
+
+ it('requires a name key on the custom field', () => {
+ const mockCF = {
+ type: 'test',
+ };
+
+ const customFields = customFieldsRegistry(strapi);
+
+ expect(() => customFields.add(mockCF)).toThrowError(
+ `Custom fields require a 'name' and 'type' key`
+ );
+ });
+
+ it('requires a type key on the custom field', () => {
+ const mockCF = {
+ name: 'test',
+ };
+
+ const customFields = customFieldsRegistry(strapi);
+
+ expect(() => customFields.add(mockCF)).toThrowError(
+ `Custom fields require a 'name' and 'type' key`
+ );
+ });
+
+ it('validates the name can be used as an object key', () => {
+ const mockCF = {
+ name: 'test.boom',
+ type: 'text',
+ };
+
+ const customFields = customFieldsRegistry(strapi);
+
+ expect(() => customFields.add(mockCF)).toThrowError(
+ `Custom field name: 'test.boom' is not a valid object key`
+ );
+ });
+
+ it('validates the type is a Strapi type', () => {
+ const mockCF = {
+ name: 'test',
+ type: 'geojson',
+ };
+
+ const customFields = customFieldsRegistry(strapi);
+
+ expect(() => customFields.add(mockCF)).toThrowError(
+ `Custom field type: 'geojson' is not a valid Strapi type`
+ );
+ });
+
+ it('confirms the custom field does not already exist', () => {
+ const mockCF = {
+ name: 'test',
+ plugin: 'plugintest',
+ type: 'text',
+ };
+
+ const customFields = customFieldsRegistry(strapi);
+
+ customFields.add(mockCF);
+ expect(() => customFields.add(mockCF)).toThrowError(
+ `Custom field: 'plugin::plugintest.test' has already been registered`
+ );
+ });
});
+ describe('get', () => {
+ it('gets a registered custom field', () => {
+ const mockCF = {
+ name: 'test',
+ plugin: 'plugintest',
+ type: 'text',
+ };
- it('adds a custom field not registered in a plugin', () => {
- const mockCF = {
- name: 'test',
- type: 'text',
- };
+ const customFields = customFieldsRegistry(strapi);
+ customFields.add(mockCF);
- const customFields = customFieldsRegistry(strapi);
- customFields.add(mockCF);
+ expect(customFields.get('plugin::plugintest.test')).toEqual(mockCF);
+ });
- const expected = {
- 'global::test': mockCF,
- };
- expect(customFields.getAll()).toEqual(expected);
- });
+ it('throws when a custom field is not registered', () => {
+ const customFields = customFieldsRegistry(strapi);
- it('requires a name key on the custom field', () => {
- const mockCF = {
- type: 'test',
- };
-
- const customFields = customFieldsRegistry(strapi);
-
- expect(() => customFields.add(mockCF)).toThrowError(
- `Custom fields require a 'name' and 'type' key`
- );
- });
-
- it('requires a type key on the custom field', () => {
- const mockCF = {
- name: 'test',
- };
-
- const customFields = customFieldsRegistry(strapi);
-
- expect(() => customFields.add(mockCF)).toThrowError(
- `Custom fields require a 'name' and 'type' key`
- );
- });
-
- it('validates the name can be used as an object key', () => {
- const mockCF = {
- name: 'test.boom',
- type: 'text',
- };
-
- const customFields = customFieldsRegistry(strapi);
-
- expect(() => customFields.add(mockCF)).toThrowError(
- `Custom field name: 'test.boom' is not a valid object key`
- );
- });
-
- it('validates the type is a Strapi type', () => {
- const mockCF = {
- name: 'test',
- type: 'geojson',
- };
-
- const customFields = customFieldsRegistry(strapi);
-
- expect(() => customFields.add(mockCF)).toThrowError(
- `Custom field type: 'geojson' is not a valid Strapi type`
- );
- });
-
- it('confirms the custom field does not already exist', () => {
- const mockCF = {
- name: 'test',
- plugin: 'plugintest',
- type: 'text',
- };
-
- const customFields = customFieldsRegistry(strapi);
-
- customFields.add(mockCF);
- expect(() => customFields.add(mockCF)).toThrowError(
- `Custom field: 'plugin::plugintest.test' has already been registered`
- );
+ expect(() => customFields.get('plugin::plugintest.test')).toThrowError(
+ `Could not find Custom Field: plugin::plugintest.test`
+ );
+ });
});
});
diff --git a/packages/core/strapi/lib/core/registries/custom-fields.js b/packages/core/strapi/lib/core/registries/custom-fields.js
index db850c8773..f254c3f961 100644
--- a/packages/core/strapi/lib/core/registries/custom-fields.js
+++ b/packages/core/strapi/lib/core/registries/custom-fields.js
@@ -10,6 +10,14 @@ const customFieldsRegistry = strapi => {
getAll() {
return customFields;
},
+ get(customField) {
+ const registeredCustomField = customFields[customField];
+ if (!registeredCustomField) {
+ throw new Error(`Could not find Custom Field: ${customField}`);
+ }
+
+ return registeredCustomField;
+ },
add(customField) {
const customFieldList = Array.isArray(customField) ? customField : [customField];
diff --git a/packages/plugins/documentation/server/services/helpers/utils/clean-schema-attributes.js b/packages/plugins/documentation/server/services/helpers/utils/clean-schema-attributes.js
index 0c8bc4a467..b3fdc73925 100644
--- a/packages/plugins/documentation/server/services/helpers/utils/clean-schema-attributes.js
+++ b/packages/plugins/documentation/server/services/helpers/utils/clean-schema-attributes.js
@@ -19,6 +19,11 @@ const cleanSchemaAttributes = (attributes, { typeMap = new Map(), isRequest = fa
delete attributesCopy[prop].default;
}
+ if (attribute.type === 'customField') {
+ const customField = strapi.container.get('custom-fields').get(attribute.customField);
+ attribute.type = customField.type;
+ }
+
switch (attribute.type) {
case 'password': {
if (!isRequest) {
|