diff --git a/examples/getstarted/src/plugins/mycustomfields/admin/src/components/Map/MapInput/index.js b/examples/getstarted/src/plugins/mycustomfields/admin/src/components/Map/MapInput/index.js
new file mode 100644
index 0000000000..d49d65a00a
--- /dev/null
+++ b/examples/getstarted/src/plugins/mycustomfields/admin/src/components/Map/MapInput/index.js
@@ -0,0 +1,7 @@
+import React from 'react';
+
+const ColorPickerInput = () => {
+ return
TODO: Map Input Component
;
+};
+
+export default ColorPickerInput;
diff --git a/examples/getstarted/src/plugins/mycustomfields/admin/src/index.js b/examples/getstarted/src/plugins/mycustomfields/admin/src/index.js
index 76e5020977..fdbfaf32c1 100644
--- a/examples/getstarted/src/plugins/mycustomfields/admin/src/index.js
+++ b/examples/getstarted/src/plugins/mycustomfields/admin/src/index.js
@@ -4,26 +4,45 @@ import ColorPickerIcon from './components/ColorPicker/ColorPickerIcon';
export default {
register(app) {
- app.customFields.register({
- name: 'color',
- pluginId: 'mycustomfields',
- type: 'text',
- icon: ColorPickerIcon,
- intlLabel: {
- id: 'mycustomfields.color.label',
- defaultMessage: 'Color',
+ app.customFields.register([
+ {
+ name: 'map',
+ pluginId: 'mycustomfields',
+ type: 'json',
+ intlLabel: {
+ id: 'mycustomfields.map.label',
+ defaultMessage: 'Map',
+ },
+ intlDescription: {
+ id: 'mycustomfields.map.description',
+ defaultMessage: 'Select any location',
+ },
+ components: {
+ Input: async () =>
+ import(/* webpackChunkName: "input-component" */ './components/Map/MapInput'),
+ },
},
- intlDescription: {
- id: 'mycustomfields.color.description',
- defaultMessage: 'Select any color',
+ {
+ name: 'color',
+ pluginId: 'mycustomfields',
+ type: 'text',
+ icon: ColorPickerIcon,
+ intlLabel: {
+ id: 'mycustomfields.color.label',
+ defaultMessage: 'Color',
+ },
+ intlDescription: {
+ id: 'mycustomfields.color.description',
+ defaultMessage: 'Select any color',
+ },
+ components: {
+ Input: async () =>
+ import(
+ /* webpackChunkName: "input-component" */ './components/ColorPicker/ColorPickerInput'
+ ),
+ },
},
- components: {
- Input: async () =>
- import(
- /* webpackChunkName: "input-component" */ './components/ColorPicker/ColorPickerInput'
- ),
- },
- });
+ ]);
},
bootstrap(app) {},
async registerTrads({ locales }) {
diff --git a/packages/core/content-type-builder/admin/src/components/AttributeOptions/AttributeList/index.js b/packages/core/content-type-builder/admin/src/components/AttributeOptions/AttributeList/index.js
new file mode 100644
index 0000000000..7d1270f4e0
--- /dev/null
+++ b/packages/core/content-type-builder/admin/src/components/AttributeOptions/AttributeList/index.js
@@ -0,0 +1,44 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Box } from '@strapi/design-system/Box';
+import { Grid, GridItem } from '@strapi/design-system/Grid';
+import { KeyboardNavigable } from '@strapi/design-system/KeyboardNavigable';
+import { Stack } from '@strapi/design-system/Stack';
+import AttributeOption from '../AttributeOption';
+import getPadding from '../utils/getPadding';
+
+const AttributeList = ({ attributes }) => (
+
+
+ {attributes.map((attributeRow, index) => {
+ return (
+ // eslint-disable-next-line react/no-array-index-key
+
+ {attributeRow.map((attribute, index) => {
+ const { paddingLeft, paddingRight } = getPadding(index);
+
+ return (
+
+
+
+
+
+ );
+ })}
+
+ );
+ })}
+
+
+);
+
+AttributeList.propTypes = {
+ attributes: PropTypes.array.isRequired,
+};
+
+export default AttributeList;
diff --git a/packages/core/content-type-builder/admin/src/components/AttributeOptions/AttributeOption/index.js b/packages/core/content-type-builder/admin/src/components/AttributeOptions/AttributeOption/index.js
index c5a8f64f31..f9bb31a38f 100644
--- a/packages/core/content-type-builder/admin/src/components/AttributeOptions/AttributeOption/index.js
+++ b/packages/core/content-type-builder/admin/src/components/AttributeOptions/AttributeOption/index.js
@@ -13,7 +13,7 @@ import { Typography } from '@strapi/design-system/Typography';
import useFormModalNavigation from '../../../hooks/useFormModalNavigation';
import getTrad from '../../../utils/getTrad';
import AttributeIcon from '../../AttributeIcon';
-import BoxWrapper from './BoxWrapper';
+import OptionBoxWrapper from '../OptionBoxWrapper';
const AttributeOption = ({ type }) => {
const { formatMessage } = useIntl();
@@ -30,7 +30,7 @@ const AttributeOption = ({ type }) => {
};
return (
-
+
@@ -50,7 +50,7 @@ const AttributeOption = ({ type }) => {
-
+
);
};
diff --git a/packages/core/content-type-builder/admin/src/components/AttributeOptions/CustomFieldOption/index.js b/packages/core/content-type-builder/admin/src/components/AttributeOptions/CustomFieldOption/index.js
new file mode 100644
index 0000000000..73a70e5cfd
--- /dev/null
+++ b/packages/core/content-type-builder/admin/src/components/AttributeOptions/CustomFieldOption/index.js
@@ -0,0 +1,65 @@
+/**
+ *
+ * AttributeOption
+ *
+ */
+
+import React from 'react';
+import PropTypes from 'prop-types';
+import { useIntl } from 'react-intl';
+import { Box } from '@strapi/design-system/Box';
+import { Flex } from '@strapi/design-system/Flex';
+import { Typography } from '@strapi/design-system/Typography';
+import OptionBoxWrapper from '../OptionBoxWrapper';
+import AttributeIcon from '../../AttributeIcon';
+import useFormModalNavigation from '../../../hooks/useFormModalNavigation';
+
+const CustomFieldOption = ({ customFieldUid, customField }) => {
+ const { type, intlLabel, intlDescription } = customField;
+ const { formatMessage } = useIntl();
+
+ const { onClickSelectCustomField } = useFormModalNavigation();
+
+ const handleClick = () => {
+ onClickSelectCustomField({
+ attributeType: type,
+ customFieldUid,
+ });
+ };
+
+ return (
+
+
+
+
+
+ {formatMessage(intlLabel)}
+
+
+
+ {formatMessage(intlDescription)}
+
+
+
+
+
+ );
+};
+
+CustomFieldOption.propTypes = {
+ customFieldUid: PropTypes.string.isRequired,
+ customField: PropTypes.shape({
+ type: PropTypes.string.isRequired,
+ icon: PropTypes.func,
+ intlLabel: PropTypes.shape({
+ id: PropTypes.string.isRequired,
+ defaultMessage: PropTypes.string.isRequired,
+ }).isRequired,
+ intlDescription: PropTypes.shape({
+ id: PropTypes.string.isRequired,
+ defaultMessage: PropTypes.string.isRequired,
+ }).isRequired,
+ }).isRequired,
+};
+
+export default CustomFieldOption;
diff --git a/packages/core/content-type-builder/admin/src/components/AttributeOptions/CustomFieldsList/index.js b/packages/core/content-type-builder/admin/src/components/AttributeOptions/CustomFieldsList/index.js
new file mode 100644
index 0000000000..8f452f0fdf
--- /dev/null
+++ b/packages/core/content-type-builder/admin/src/components/AttributeOptions/CustomFieldsList/index.js
@@ -0,0 +1,61 @@
+import React from 'react';
+import { useCustomFields } from '@strapi/helper-plugin';
+import { Box } from '@strapi/design-system/Box';
+import { Grid, GridItem } from '@strapi/design-system/Grid';
+import { KeyboardNavigable } from '@strapi/design-system/KeyboardNavigable';
+import { Stack } from '@strapi/design-system/Stack';
+import { Link } from '@strapi/design-system/Link';
+import { useIntl } from 'react-intl';
+import EmptyAttributes from '../EmptyAttributes';
+import CustomFieldOption from '../CustomFieldOption';
+import getPadding from '../utils/getPadding';
+import { getTrad } from '../../../utils';
+
+const CustomFieldsList = () => {
+ const { formatMessage } = useIntl();
+ const customFields = useCustomFields();
+ const registeredCustomFields = Object.entries(customFields.getAll());
+
+ if (!registeredCustomFields.length) return ;
+
+ // Sort the array alphabetically by customField name
+ const sortedCustomFields = registeredCustomFields.sort((a, b) =>
+ a[1].name > b[1].name ? 1 : -1
+ );
+
+ return (
+
+
+
+ {sortedCustomFields.map(([uid, customField], index) => {
+ const { paddingLeft, paddingRight } = getPadding(index);
+
+ return (
+
+
+
+
+
+ );
+ })}
+
+
+ {formatMessage({
+ id: getTrad('modalForm.tabs.custom.howToLink'),
+ defaultMessage: 'How to add custom fields',
+ })}
+
+
+
+ );
+};
+
+export default CustomFieldsList;
diff --git a/packages/core/content-type-builder/admin/src/components/AttributeOptions/AttributeOption/BoxWrapper.js b/packages/core/content-type-builder/admin/src/components/AttributeOptions/OptionBoxWrapper/index.js
similarity index 100%
rename from packages/core/content-type-builder/admin/src/components/AttributeOptions/AttributeOption/BoxWrapper.js
rename to packages/core/content-type-builder/admin/src/components/AttributeOptions/OptionBoxWrapper/index.js
diff --git a/packages/core/content-type-builder/admin/src/components/AttributeOptions/index.js b/packages/core/content-type-builder/admin/src/components/AttributeOptions/index.js
index a5aa3bd5db..9c47a3a8cd 100644
--- a/packages/core/content-type-builder/admin/src/components/AttributeOptions/index.js
+++ b/packages/core/content-type-builder/admin/src/components/AttributeOptions/index.js
@@ -9,16 +9,13 @@ import { useIntl } from 'react-intl';
import PropTypes from 'prop-types';
import { Box } from '@strapi/design-system/Box';
import { Divider } from '@strapi/design-system/Divider';
-import { Grid, GridItem } from '@strapi/design-system/Grid';
-import { KeyboardNavigable } from '@strapi/design-system/KeyboardNavigable';
import { ModalBody } from '@strapi/design-system/ModalLayout';
-import { Stack } from '@strapi/design-system/Stack';
import { Flex } from '@strapi/design-system/Flex';
import { Typography } from '@strapi/design-system/Typography';
import { Tabs, Tab, TabGroup, TabPanels, TabPanel } from '@strapi/design-system/Tabs';
import { getTrad } from '../../utils';
-import AttributeOption from './AttributeOption';
-import EmptyAttributes from './EmptyAttributes';
+import AttributeList from './AttributeList';
+import CustomFieldsList from './CustomFieldsList';
const AttributeOptions = ({ attributes, forTarget, kind }) => {
const { formatMessage } = useIntl();
@@ -53,39 +50,10 @@ const AttributeOptions = ({ attributes, forTarget, kind }) => {
-
-
- {attributes.map((attributeRow, index) => {
- const key = index;
-
- return (
-
- {attributeRow.map((attribute, index) => {
- const isOdd = index % 2 === 1;
- const paddingLeft = isOdd ? 2 : 0;
- const paddingRight = isOdd ? 0 : 2;
-
- return (
-
-
-
-
-
- );
- })}
-
- );
- })}
-
-
+
-
+
diff --git a/packages/core/content-type-builder/admin/src/components/AttributeOptions/tests/__snapshots__/index.test.js.snap b/packages/core/content-type-builder/admin/src/components/AttributeOptions/tests/__snapshots__/index.test.js.snap
index a1fe23434d..9a157eaefe 100644
--- a/packages/core/content-type-builder/admin/src/components/AttributeOptions/tests/__snapshots__/index.test.js.snap
+++ b/packages/core/content-type-builder/admin/src/components/AttributeOptions/tests/__snapshots__/index.test.js.snap
@@ -123,17 +123,6 @@ exports[` renders and matches the snapshot 1`] = `
margin: 0;
}
-.c15 {
- display: grid;
- grid-template-columns: repeat(12,1fr);
- gap: 0px;
-}
-
-.c16 {
- grid-column: span 6;
- max-width: 100%;
-}
-
.c0 {
padding: 24px;
}
@@ -175,6 +164,17 @@ exports[` renders and matches the snapshot 1`] = `
cursor: not-allowed;
}
+.c15 {
+ display: grid;
+ grid-template-columns: repeat(12,1fr);
+ gap: 0px;
+}
+
+.c16 {
+ grid-column: span 6;
+ max-width: 100%;
+}
+
.c21 {
width: 2rem;
height: 1.5rem;
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 5cb6bac918..181cca5591 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
@@ -1,12 +1,41 @@
import React from 'react';
import { Router } from 'react-router-dom';
import { createMemoryHistory } from 'history';
-import { render, screen, getByText, fireEvent } from '@testing-library/react';
+import { render, screen, fireEvent } from '@testing-library/react';
import { lightTheme, ThemeProvider } from '@strapi/design-system';
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: () => ({
+ get: jest.fn().mockReturnValue(mockCustomField),
+ getAll,
+ }),
+}));
+
const mockAttributes = [
[
'text',
@@ -57,8 +86,8 @@ describe('', () => {
const App = makeApp();
render(App);
- const defaultTab = screen.getByRole('tab', { selected: true });
- const customTab = screen.getByRole('tab', { selected: false });
+ const defaultTab = screen.getByRole('tab', { selected: true, name: 'Default' });
+ const customTab = screen.getByRole('tab', { selected: false, name: 'Custom' });
expect(defaultTab).toBeVisible();
expect(customTab).toBeVisible();
@@ -73,17 +102,34 @@ describe('', () => {
expect(comingSoonText).toEqual(null);
});
- it('switches to the custom tab', () => {
+ it('switches to the custom tab without custom fields', () => {
const App = makeApp();
render(App);
- const customTab = screen.getByRole('tab', { selected: false });
+ getAll.mockReturnValueOnce({});
+
+ const customTab = screen.getByRole('tab', { selected: false, name: 'Custom' });
fireEvent.click(customTab);
- const customTabSelected = screen.getByRole('tab', { selected: true });
- const customTabText = getByText(customTabSelected, 'Custom');
+ const customTabSelected = screen.getByRole('tab', { selected: true, name: 'Custom' });
const comingSoonText = screen.getByText('Nothing in here yet.');
- expect(customTabText).not.toBe(null);
+ expect(customTabSelected).toBeVisible();
expect(comingSoonText).toBeVisible();
});
+
+ it('switches to the custom tab with custom fields', () => {
+ getAll.mockReturnValue(mockCustomField);
+ const App = makeApp();
+ render(App);
+
+ const customTab = screen.getByRole('tab', { selected: false, name: 'Custom' });
+ fireEvent.click(customTab);
+ const customTabSelected = screen.getByRole('tab', { selected: true, name: 'Custom' });
+ const customFieldText = screen.getByText('Color');
+ const howToAddLink = screen.getByRole('link', { name: 'How to add custom fields' });
+
+ expect(customTabSelected).toBeVisible();
+ expect(customFieldText).toBeVisible();
+ expect(howToAddLink).toBeVisible();
+ });
});
diff --git a/packages/core/content-type-builder/admin/src/components/AttributeOptions/utils/getPadding.js b/packages/core/content-type-builder/admin/src/components/AttributeOptions/utils/getPadding.js
new file mode 100644
index 0000000000..ff0c16f884
--- /dev/null
+++ b/packages/core/content-type-builder/admin/src/components/AttributeOptions/utils/getPadding.js
@@ -0,0 +1,9 @@
+const getPadding = index => {
+ const isOdd = index % 2 === 1;
+ const paddingLeft = isOdd ? 2 : 0;
+ const paddingRight = isOdd ? 0 : 2;
+
+ return { paddingLeft, paddingRight };
+};
+
+export default getPadding;
diff --git a/packages/core/content-type-builder/admin/src/components/FormModalNavigationProvider/constants.js b/packages/core/content-type-builder/admin/src/components/FormModalNavigationProvider/constants.js
index 7ea01dd974..4bf90e8b37 100644
--- a/packages/core/content-type-builder/admin/src/components/FormModalNavigationProvider/constants.js
+++ b/packages/core/content-type-builder/admin/src/components/FormModalNavigationProvider/constants.js
@@ -10,6 +10,7 @@ const INITIAL_STATE_DATA = {
kind: null,
step: null,
targetUid: null,
+ customFieldUid: null,
};
export { INITIAL_STATE_DATA };
diff --git a/packages/core/content-type-builder/admin/src/components/FormModalNavigationProvider/index.js b/packages/core/content-type-builder/admin/src/components/FormModalNavigationProvider/index.js
index 993b4723ba..013f74a3ce 100644
--- a/packages/core/content-type-builder/admin/src/components/FormModalNavigationProvider/index.js
+++ b/packages/core/content-type-builder/admin/src/components/FormModalNavigationProvider/index.js
@@ -8,6 +8,20 @@ const FormModalNavigationProvider = ({ children }) => {
const [state, setFormModalNavigationState] = useState(INITIAL_STATE_DATA);
const { trackUsage } = useTracking();
+ const onClickSelectCustomField = ({ attributeType, customFieldUid }) => {
+ // TODO: Add tracking for custom fields
+ setFormModalNavigationState(prevState => {
+ return {
+ ...prevState,
+ actionType: 'create',
+ // TODO: Create a new modalType on EXPANSION-245
+ modalType: 'attribute',
+ attributeType,
+ customFieldUid,
+ };
+ });
+ };
+
const onClickSelectField = ({ attributeType, step }) => {
if (state.forTarget === 'contentType') {
trackUsage('didSelectContentTypeFieldType', { type: attributeType });
@@ -47,7 +61,6 @@ const FormModalNavigationProvider = ({ children }) => {
forTarget,
targetUid,
modalType: 'chooseAttribute',
-
isOpen: true,
};
});
@@ -146,6 +159,7 @@ const FormModalNavigationProvider = ({ children }) => {
value={{
...state,
onClickSelectField,
+ onClickSelectCustomField,
onCloseModal,
onNavigateToChooseAttributeModal,
onNavigateToAddCompoToDZModal,
diff --git a/packages/core/content-type-builder/admin/src/components/FormModalNavigationProvider/tests/index.test.js b/packages/core/content-type-builder/admin/src/components/FormModalNavigationProvider/tests/index.test.js
new file mode 100644
index 0000000000..8ccd80f09e
--- /dev/null
+++ b/packages/core/content-type-builder/admin/src/components/FormModalNavigationProvider/tests/index.test.js
@@ -0,0 +1,47 @@
+import { renderHook, act } from '@testing-library/react-hooks';
+import { INITIAL_STATE_DATA } from '../constants';
+import FormModalNavigationProvider from '../index';
+import useFormModalNavigation from '../../../hooks/useFormModalNavigation';
+
+const removeFunctionsFromObject = state => {
+ const stringified = JSON.stringify(state);
+ const parsed = JSON.parse(stringified);
+
+ return parsed;
+};
+
+describe('FromModalNavigationProvider', () => {
+ it('sets the initial state', () => {
+ const { result } = renderHook(() => useFormModalNavigation(), {
+ wrapper: FormModalNavigationProvider,
+ });
+
+ const currentStateWithoutFunctions = removeFunctionsFromObject(result.current);
+
+ expect(currentStateWithoutFunctions).toEqual(INITIAL_STATE_DATA);
+ });
+
+ it('updates the form navigation state when selecting a custom field', () => {
+ const { result } = renderHook(() => useFormModalNavigation(), {
+ wrapper: FormModalNavigationProvider,
+ });
+
+ act(() => {
+ result.current.onClickSelectCustomField({
+ attributeType: 'text',
+ customFieldUid: 'plugin::mycustomfields.color',
+ });
+ });
+
+ const currentStateWithoutFunctions = removeFunctionsFromObject(result.current);
+ const expected = {
+ ...INITIAL_STATE_DATA,
+ actionType: 'create',
+ modalType: 'attribute',
+ attributeType: 'text',
+ customFieldUid: 'plugin::mycustomfields.color',
+ };
+
+ expect(currentStateWithoutFunctions).toEqual(expected);
+ });
+});
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
index e74fe0bafb..6b515f9fe1 100644
--- a/packages/core/content-type-builder/admin/src/components/ListRow/DisplayedType.js
+++ b/packages/core/content-type-builder/admin/src/components/ListRow/DisplayedType.js
@@ -49,7 +49,7 @@ DisplayedType.defaultProps = {
DisplayedType.propTypes = {
type: PropTypes.string.isRequired,
- customField: PropTypes.bool,
+ customField: PropTypes.string,
repeatable: 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 8cf4989ec4..0a3f36e36e 100644
--- a/packages/core/content-type-builder/admin/src/translations/en.json
+++ b/packages/core/content-type-builder/admin/src/translations/en.json
@@ -160,6 +160,7 @@
"modalForm.sub-header.chooseAttribute.component": "Select a field for your component",
"modalForm.sub-header.chooseAttribute.singleType": "Select a field for your single type",
"modalForm.tabs.custom": "Custom",
+ "modalForm.tabs.custom.howToLink": "How to add custom fields",
"modalForm.tabs.default": "Default",
"modalForm.tabs.label": "Default and Custom types tabs",
"modelPage.attribute.relation-polymorphic": "Relation (polymorphic)",