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)",