From 94cb61d62ff3bec3c39f50171eb1e4220f7f74a6 Mon Sep 17 00:00:00 2001 From: Fernando Chavez Date: Thu, 11 May 2023 11:16:23 +0200 Subject: [PATCH 01/21] add IconPicker field to components --- .../components/FormModal/component/form.js | 14 +++ .../admin/src/components/FormModal/index.js | 3 + .../admin/src/components/IconPicker/index.js | 119 ++++++++++++++++++ .../admin/src/translations/en.json | 4 +- 4 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 packages/core/content-type-builder/admin/src/components/IconPicker/index.js diff --git a/packages/core/content-type-builder/admin/src/components/FormModal/component/form.js b/packages/core/content-type-builder/admin/src/components/FormModal/component/form.js index 0472744d7da..18e51866177 100644 --- a/packages/core/content-type-builder/admin/src/components/FormModal/component/form.js +++ b/packages/core/content-type-builder/admin/src/components/FormModal/component/form.js @@ -24,6 +24,20 @@ const componentForm = { }, ], }, + { + sectionTitle: null, + items: [ + { + name: `${prefix}icon`, + type: 'icon-picker', + size: 12, + intlLabel: { + id: getTrad('modalForm.components.icon.label'), + defaultMessage: 'Icon', + }, + }, + ], + }, ]; return sections; diff --git a/packages/core/content-type-builder/admin/src/components/FormModal/index.js b/packages/core/content-type-builder/admin/src/components/FormModal/index.js index e23311f3665..11a86f78e52 100644 --- a/packages/core/content-type-builder/admin/src/components/FormModal/index.js +++ b/packages/core/content-type-builder/admin/src/components/FormModal/index.js @@ -45,6 +45,7 @@ import BooleanRadioGroup from '../BooleanRadioGroup'; import CheckboxWithNumberField from '../CheckboxWithNumberField'; import CustomRadioGroup from '../CustomRadioGroup'; import ContentTypeRadioGroup from '../ContentTypeRadioGroup'; +import IconPicker from '../IconPicker'; import Relation from '../Relation'; import PluralName from '../PluralName'; import SelectCategory from '../SelectCategory'; @@ -244,6 +245,7 @@ const FormModal = () => { data: { displayName: data.schema.displayName, category: data.category, + icon: data.schema.icon, }, }); } @@ -915,6 +917,7 @@ const FormModal = () => { 'allowed-types-select': AllowedTypesSelect, 'boolean-radio-group': BooleanRadioGroup, 'checkbox-with-number-field': CheckboxWithNumberField, + 'icon-picker': IconPicker, 'content-type-radio-group': ContentTypeRadioGroup, 'radio-group': CustomRadioGroup, relation: Relation, diff --git a/packages/core/content-type-builder/admin/src/components/IconPicker/index.js b/packages/core/content-type-builder/admin/src/components/IconPicker/index.js new file mode 100644 index 00000000000..79040eede52 --- /dev/null +++ b/packages/core/content-type-builder/admin/src/components/IconPicker/index.js @@ -0,0 +1,119 @@ +import React, { useState } from 'react'; +import { Box, Flex, Icon, Typography, Searchbar, IconButton } from '@strapi/design-system'; +import * as Icons from '@strapi/icons'; +import { useIntl } from 'react-intl'; +import { inputFocusStyle } from '@strapi/design-system'; +import styled from 'styled-components'; +import { getTrad } from '../../utils'; + +const IconPickerWrapper = styled(Flex)` + position: relative; + + label { + border-radius: 4px; + ${inputFocusStyle} + } + + input { + position: absolute; + opacity: 0; + } +`; + +const IconPick = ({ iconKey, name, onChange, isSelected }) => { + return ( + + ); +}; + +const IconPicker = ({ intlLabel, name, onChange, value }) => { + const { formatMessage } = useIntl(); + const [showSearch, setShowSearch] = useState(false); + const [search, setSearch] = useState(''); + const allIcons = Object.keys(Icons); + const [icons, setIcons] = useState(allIcons); + + const SearchIcon = Icons['Search']; + + const toggleSearch = () => { + setShowSearch(!showSearch); + }; + + const onChangeSearch = ({ target: { value } }) => { + setSearch(value); + setIcons(() => allIcons.filter((icon) => icon.toLowerCase().includes(value.toLowerCase()))); + }; + + const onClearSearch = () => { + setSearch(''); + setIcons(allIcons); + setShowSearch(false); + }; + + return ( + + + + {formatMessage(intlLabel)} + + {showSearch ? ( + { + if (!search) { + toggleSearch(); + } + }} + onChange={onChangeSearch} + value={search} + onClear={onClearSearch} + clearLabel={formatMessage({ + id: getTrad('IconPicker.search.clear.label'), + defaultMessage: 'Clearing the icon search', + })} + /> + ) : ( + } noBorder /> + )} + + + {icons.map((iconKey) => ( + + ))} + + + ); +}; + +export default IconPicker; 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 9a8cd9257a5..858d1bc6fb5 100644 --- a/packages/core/content-type-builder/admin/src/translations/en.json +++ b/packages/core/content-type-builder/admin/src/translations/en.json @@ -197,5 +197,7 @@ "table.button.no-fields": "Add new field", "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" + "table.content.no-fields.component": "Add your first field to this component", + "IconPicker.search.placeholder": "Search for an icon", + "IconPicker.search.clear.label": "Clearing the icon search" } From 3dba19426f081e3c71a7598516c7f3a2aedd6832 Mon Sep 17 00:00:00 2001 From: Fernando Chavez Date: Thu, 11 May 2023 15:18:22 +0200 Subject: [PATCH 02/21] add component icons on content type builders dynamic zones --- .../ComponentIcon/ComponentIcon.js | 25 +++++++++++-------- .../src/components/ComponentCard/index.js | 10 ++++++-- .../server/controllers/validation/common.js | 8 ++++++ .../controllers/validation/component.js | 3 ++- .../server/services/components.js | 1 + .../schema-builder/component-builder.js | 2 ++ 6 files changed, 36 insertions(+), 13 deletions(-) diff --git a/packages/core/content-type-builder/admin/src/components/ComponentCard/ComponentIcon/ComponentIcon.js b/packages/core/content-type-builder/admin/src/components/ComponentCard/ComponentIcon/ComponentIcon.js index 74db2d3ee21..0161fa2f992 100644 --- a/packages/core/content-type-builder/admin/src/components/ComponentCard/ComponentIcon/ComponentIcon.js +++ b/packages/core/content-type-builder/admin/src/components/ComponentCard/ComponentIcon/ComponentIcon.js @@ -2,11 +2,11 @@ import PropTypes from 'prop-types'; import React from 'react'; import styled from 'styled-components'; -import { Flex } from '@strapi/design-system'; +import { Flex, Icon } from '@strapi/design-system'; +import * as Icons from '@strapi/icons'; const Wrapper = styled(Flex)` border-radius: 50%; - color: ${({ theme, isActive }) => (isActive ? theme.colors.primary600 : theme.colors.neutral600)}; height: ${({ theme }) => theme.spaces[8]}; width: ${({ theme }) => theme.spaces[8]}; @@ -16,20 +16,25 @@ const Wrapper = styled(Flex)` } `; -export function ComponentIcon({ isActive }) { +const DefaultIcon = () => ( + + + +); + +export function ComponentIcon({ isActive, icon }) { + const ComponentIcon = Icons[icon] || DefaultIcon; + return ( - - - + ); } diff --git a/packages/core/content-type-builder/admin/src/components/ComponentCard/index.js b/packages/core/content-type-builder/admin/src/components/ComponentCard/index.js index 941ad957712..542be557824 100644 --- a/packages/core/content-type-builder/admin/src/components/ComponentCard/index.js +++ b/packages/core/content-type-builder/admin/src/components/ComponentCard/index.js @@ -59,6 +59,12 @@ const ComponentBox = styled(Flex)` > div:first-child { background: ${({ theme }) => theme.colors.primary200}; color: ${({ theme }) => theme.colors.primary600}; + + svg { + path { + fill: ${({ theme }) => theme.colors.primary600}; + } + } } } `; @@ -66,7 +72,7 @@ const ComponentBox = styled(Flex)` function ComponentCard({ component, dzName, index, isActive, isInDevelopmentMode, onClick }) { const { modifiedData, removeComponentFromDynamicZone } = useDataManager(); const { - schema: { displayName }, + schema: { icon, displayName }, } = get(modifiedData, ['components', component], { schema: {} }); const onClose = (e) => { @@ -88,7 +94,7 @@ function ComponentCard({ component, dzName, index, isActive, isInDevelopmentMode type="button" onClick={onClick} > - + diff --git a/packages/core/content-type-builder/server/controllers/validation/common.js b/packages/core/content-type-builder/server/controllers/validation/common.js index ec64a88d1e5..69e3627c7a6 100644 --- a/packages/core/content-type-builder/server/controllers/validation/common.js +++ b/packages/core/content-type-builder/server/controllers/validation/common.js @@ -13,6 +13,7 @@ const validators = { const NAME_REGEX = /^[A-Za-z][_0-9A-Za-z]*$/; const COLLECTION_NAME_REGEX = /^[A-Za-z][-_0-9A-Za-z]*$/; const CATEGORY_NAME_REGEX = /^[A-Za-z][-_0-9A-Za-z]*$/; +const ICON_REGEX = /^[A-Za-z0-9][-A-Za-z0-9]*$/; const UID_REGEX = /^[A-Za-z0-9-_.~]*$/; const isValidName = { @@ -21,6 +22,12 @@ const isValidName = { test: (val) => val === '' || NAME_REGEX.test(val), }; +const isValidIcon = { + name: 'isValidIcon', + message: `\${path} must match the following regex: ${ICON_REGEX}`, + test: (val) => val === '' || ICON_REGEX.test(val), +}; + const isValidUID = { name: 'isValidUID', message: `\${path} must match the following regex: ${UID_REGEX}`, @@ -96,6 +103,7 @@ module.exports = { isValidCategoryName, isValidDefaultJSON, isValidName, + isValidIcon, isValidKey, isValidEnum, isValidUID, diff --git a/packages/core/content-type-builder/server/controllers/validation/component.js b/packages/core/content-type-builder/server/controllers/validation/component.js index 960ef2f73e4..8de5db48d8f 100644 --- a/packages/core/content-type-builder/server/controllers/validation/component.js +++ b/packages/core/content-type-builder/server/controllers/validation/component.js @@ -4,7 +4,7 @@ const _ = require('lodash'); const { yup, validateYupSchema } = require('@strapi/utils'); const { modelTypes, DEFAULT_TYPES } = require('../../services/constants'); -const { isValidCategoryName } = require('./common'); +const { isValidCategoryName, isValidIcon } = require('./common'); const createSchema = require('./model-schema'); const { removeEmptyDefaults } = require('./data-transform'); @@ -16,6 +16,7 @@ const componentSchema = createSchema(VALID_TYPES, VALID_RELATIONS, { }) .shape({ displayName: yup.string().min(1).required('displayName.required'), + icon: yup.string().nullable().test(isValidIcon), category: yup.string().nullable().test(isValidCategoryName).required('category.required'), }) .required() diff --git a/packages/core/content-type-builder/server/services/components.js b/packages/core/content-type-builder/server/services/components.js index e0f3a29ef2f..8cc1dd1a0c3 100644 --- a/packages/core/content-type-builder/server/services/components.js +++ b/packages/core/content-type-builder/server/services/components.js @@ -20,6 +20,7 @@ const formatComponent = (component) => { schema: { displayName: _.get(info, 'displayName'), description: _.get(info, 'description', ''), + icon: _.get(info, 'icon'), connection, collectionName, pluginOptions: component.pluginOptions, diff --git a/packages/core/content-type-builder/server/services/schema-builder/component-builder.js b/packages/core/content-type-builder/server/services/schema-builder/component-builder.js index d4890af41bd..589cdb394cc 100644 --- a/packages/core/content-type-builder/server/services/schema-builder/component-builder.js +++ b/packages/core/content-type-builder/server/services/schema-builder/component-builder.js @@ -51,6 +51,7 @@ module.exports = function createComponentBuilder() { .setUID(uid) .set('collectionName', collectionName) .set(['info', 'displayName'], infos.displayName) + .set(['info', 'icon'], infos.icon) .set(['info', 'description'], infos.description) .set('pluginOptions', infos.pluginOptions) .set('config', infos.config) @@ -100,6 +101,7 @@ module.exports = function createComponentBuilder() { .setUID(newUID) .setDir(newDir) .set(['info', 'displayName'], infos.displayName) + .set(['info', 'icon'], infos.icon) .set(['info', 'description'], infos.description) .set('pluginOptions', infos.pluginOptions) .setAttributes(this.convertAttributes(newAttributes)); From 09febe2e2aefab3775c974bb2b40a6f7a1125574 Mon Sep 17 00:00:00 2001 From: Fernando Chavez Date: Thu, 11 May 2023 18:02:58 +0200 Subject: [PATCH 03/21] add component's icon back to the content manager --- .../components/ComponentIcon/ComponentIcon.js | 21 ++++-- .../DynamicZone/components/ComponentCard.js | 71 +++++++++++++++++++ .../components/ComponentCategory.js | 4 +- .../components/DynamicComponent.js | 2 + .../components/DynamicZoneList.js | 9 ++- .../ComponentIcon/ComponentIcon.js | 4 +- 6 files changed, 99 insertions(+), 12 deletions(-) create mode 100644 packages/core/admin/admin/src/content-manager/components/DynamicZone/components/ComponentCard.js diff --git a/packages/core/admin/admin/src/content-manager/components/ComponentIcon/ComponentIcon.js b/packages/core/admin/admin/src/content-manager/components/ComponentIcon/ComponentIcon.js index f1ccfa2d36f..b76c84d0e7e 100644 --- a/packages/core/admin/admin/src/content-manager/components/ComponentIcon/ComponentIcon.js +++ b/packages/core/admin/admin/src/content-manager/components/ComponentIcon/ComponentIcon.js @@ -3,6 +3,7 @@ import React from 'react'; import styled from 'styled-components'; import { Flex } from '@strapi/design-system'; +import * as Icons from '@strapi/icons'; const WIDTH_S = 5; const WIDTH_M = 8; @@ -19,7 +20,18 @@ const Wrapper = styled(Flex)` } `; -export function ComponentIcon({ showBackground = true, size = 'M' }) { +const DefaultIcon = () => ( + + + +); + +export function ComponentIcon({ showBackground = true, size = 'M', icon }) { + const ComponentIcon = Icons[icon] || DefaultIcon; + return ( - - - + ); } diff --git a/packages/core/admin/admin/src/content-manager/components/DynamicZone/components/ComponentCard.js b/packages/core/admin/admin/src/content-manager/components/DynamicZone/components/ComponentCard.js new file mode 100644 index 00000000000..c8e24ee33b0 --- /dev/null +++ b/packages/core/admin/admin/src/content-manager/components/DynamicZone/components/ComponentCard.js @@ -0,0 +1,71 @@ +/** + * + * ComponentCard + * + */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import styled from 'styled-components'; + +import { Box, Typography, Flex } from '@strapi/design-system'; +import { pxToRem } from '@strapi/helper-plugin'; + +import { ComponentIcon } from '../../ComponentIcon'; + +const ComponentBox = styled(Box)` + flex-shrink: 0; + height: ${pxToRem(84)}; + border: 1px solid ${({ theme }) => theme.colors.neutral200}; + background: ${({ theme }) => theme.colors.neutral100}; + border-radius: ${({ theme }) => theme.borderRadius}; + display: flex; + justify-content: center; + align-items: center; + + &:focus, + &:hover { + border: 1px solid ${({ theme }) => theme.colors.primary200}; + background: ${({ theme }) => theme.colors.primary100}; + + ${Typography} { + color: ${({ theme }) => theme.colors.primary600}; + } + + /* > Flex > ComponentIcon */ + > div > div:first-child { + background: ${({ theme }) => theme.colors.primary200}; + color: ${({ theme }) => theme.colors.primary600}; + + svg { + path { + fill: ${({ theme }) => theme.colors.primary600}; + } + } + } + } +`; + +export default function ComponentCard({ children, onClick, icon }) { + return ( + + + + + + {children} + + + + ); +} + +ComponentCard.defaultProps = { + onClick() {}, +}; + +ComponentCard.propTypes = { + children: PropTypes.node.isRequired, + onClick: PropTypes.func, + icon: PropTypes.string, +}; diff --git a/packages/core/admin/admin/src/content-manager/components/DynamicZone/components/ComponentCategory.js b/packages/core/admin/admin/src/content-manager/components/DynamicZone/components/ComponentCategory.js index 811cee33c37..0d3c4c3287c 100644 --- a/packages/core/admin/admin/src/content-manager/components/DynamicZone/components/ComponentCategory.js +++ b/packages/core/admin/admin/src/content-manager/components/DynamicZone/components/ComponentCategory.js @@ -38,7 +38,7 @@ export const ComponentCategory = ({ - {components.map(({ componentUid, info: { displayName } }) => ( + {components.map(({ componentUid, info: { displayName, icon } }) => ( - + {formatMessage({ id: displayName, defaultMessage: displayName })} diff --git a/packages/core/admin/admin/src/content-manager/components/DynamicZone/components/DynamicComponent.js b/packages/core/admin/admin/src/content-manager/components/DynamicZone/components/DynamicComponent.js index 3f921356e9e..80f7fb9d887 100644 --- a/packages/core/admin/admin/src/content-manager/components/DynamicZone/components/DynamicComponent.js +++ b/packages/core/admin/admin/src/content-manager/components/DynamicZone/components/DynamicComponent.js @@ -22,6 +22,7 @@ import { useContentTypeLayout, useDragAndDrop } from '../../../hooks'; import { composeRefs, getTrad, ItemTypes } from '../../../utils'; import FieldComponent from '../../FieldComponent'; +import { ComponentIcon } from '../../ComponentIcon'; export const DynamicComponent = ({ componentUid, @@ -206,6 +207,7 @@ export const DynamicComponent = ({ ) : ( } action={accordionActions} title={`${friendlyName}${mainValue}`} togglePosition="left" diff --git a/packages/core/admin/admin/src/content-manager/pages/EditSettingsView/components/DynamicZoneList.js b/packages/core/admin/admin/src/content-manager/pages/EditSettingsView/components/DynamicZoneList.js index 380c6bcc9fe..2350a699a9d 100644 --- a/packages/core/admin/admin/src/content-manager/pages/EditSettingsView/components/DynamicZoneList.js +++ b/packages/core/admin/admin/src/content-manager/pages/EditSettingsView/components/DynamicZoneList.js @@ -1,4 +1,5 @@ import React from 'react'; +import get from 'lodash/get'; import styled from 'styled-components'; import { Link } from 'react-router-dom'; import PropTypes from 'prop-types'; @@ -25,6 +26,12 @@ const CustomLink = styled(Flex)` > div:first-child { background: ${({ theme }) => theme.colors.primary200}; color: ${({ theme }) => theme.colors.primary600}; + + svg { + path { + fill: ${({ theme }) => theme.colors.primary600}; + } + } } } `; @@ -49,7 +56,7 @@ const DynamicZoneList = ({ components }) => { as={Link} to={`/content-manager/components/${componentUid}/configurations/edit`} > - + diff --git a/packages/core/content-type-builder/admin/src/components/ComponentCard/ComponentIcon/ComponentIcon.js b/packages/core/content-type-builder/admin/src/components/ComponentCard/ComponentIcon/ComponentIcon.js index 0161fa2f992..0324e66ece1 100644 --- a/packages/core/content-type-builder/admin/src/components/ComponentCard/ComponentIcon/ComponentIcon.js +++ b/packages/core/content-type-builder/admin/src/components/ComponentCard/ComponentIcon/ComponentIcon.js @@ -2,7 +2,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import styled from 'styled-components'; -import { Flex, Icon } from '@strapi/design-system'; +import { Flex } from '@strapi/design-system'; import * as Icons from '@strapi/icons'; const Wrapper = styled(Flex)` @@ -34,7 +34,7 @@ export function ComponentIcon({ isActive, icon }) { background={isActive ? 'primary200' : 'neutral200'} justifyContent="center" > - + ); } From 1444c3e134f50ec817c64d3dc2ac933c0f81b430 Mon Sep 17 00:00:00 2001 From: Fernando Chavez Date: Fri, 12 May 2023 09:40:37 +0200 Subject: [PATCH 04/21] Fix bug on searchIcon on components icons picker and add remove icon btn --- .../admin/src/components/IconPicker/index.js | 78 +++++++++++++------ 1 file changed, 53 insertions(+), 25 deletions(-) diff --git a/packages/core/content-type-builder/admin/src/components/IconPicker/index.js b/packages/core/content-type-builder/admin/src/components/IconPicker/index.js index 79040eede52..dd340cce5f6 100644 --- a/packages/core/content-type-builder/admin/src/components/IconPicker/index.js +++ b/packages/core/content-type-builder/admin/src/components/IconPicker/index.js @@ -1,5 +1,5 @@ -import React, { useState } from 'react'; -import { Box, Flex, Icon, Typography, Searchbar, IconButton } from '@strapi/design-system'; +import React, { useState, useRef, useEffect } from 'react'; +import { Box, Flex, Icon, Typography, Searchbar, IconButton, Popover } from '@strapi/design-system'; import * as Icons from '@strapi/icons'; import { useIntl } from 'react-intl'; import { inputFocusStyle } from '@strapi/design-system'; @@ -44,8 +44,10 @@ const IconPicker = ({ intlLabel, name, onChange, value }) => { const [search, setSearch] = useState(''); const allIcons = Object.keys(Icons); const [icons, setIcons] = useState(allIcons); + const searchIconRef = useRef(null); const SearchIcon = Icons['Search']; + const TrashIcon = Icons['Trash']; const toggleSearch = () => { setShowSearch(!showSearch); @@ -57,9 +59,13 @@ const IconPicker = ({ intlLabel, name, onChange, value }) => { }; const onClearSearch = () => { + toggleSearch(); setSearch(''); setIcons(allIcons); - setShowSearch(false); + }; + + const removeIconSelected = () => { + onChange({ target: { name, value: '' } }); }; return ( @@ -68,29 +74,51 @@ const IconPicker = ({ intlLabel, name, onChange, value }) => { {formatMessage(intlLabel)} - {showSearch ? ( - { - if (!search) { - toggleSearch(); - } - }} - onChange={onChangeSearch} - value={search} - onClear={onClearSearch} - clearLabel={formatMessage({ - id: getTrad('IconPicker.search.clear.label'), - defaultMessage: 'Clearing the icon search', - })} + + } + noBorder /> - ) : ( - } noBorder /> + {value && ( + } + noBorder + /> + )} + + {showSearch && ( + + { + if (!search) { + toggleSearch(); + } + }} + onChange={onChangeSearch} + value={search} + onClear={onClearSearch} + clearLabel={formatMessage({ + id: getTrad('IconPicker.search.clear.label'), + defaultMessage: 'Clearing the icon search', + })} + > + {formatMessage({ + id: getTrad('ComponentIconPicker.search.placeholder'), + defaultMessage: 'Search for an icon', + })} + + )} Date: Fri, 12 May 2023 14:18:55 +0200 Subject: [PATCH 05/21] update component card tests and update snapshots --- .../components.test.api.js | 4 + .../components/ComponentIcon/ComponentIcon.js | 1 + .../components/tests/ComponentCard.test.js | 35 +++++++ .../tests/__snapshots__/index.test.js.snap | 97 +++++++++---------- .../validation/__tests__/component.test.js | 3 + .../validation/__tests__/content-type.test.js | 2 + 6 files changed, 90 insertions(+), 52 deletions(-) create mode 100644 packages/core/admin/admin/src/content-manager/components/DynamicZone/components/tests/ComponentCard.test.js diff --git a/api-tests/core/content-type-builder/components.test.api.js b/api-tests/core/content-type-builder/components.test.api.js index 572bcc940ee..c1b526892f0 100644 --- a/api-tests/core/content-type-builder/components.test.api.js +++ b/api-tests/core/content-type-builder/components.test.api.js @@ -68,6 +68,7 @@ describe('Content Type Builder - Components', () => { component: { category: 'default', displayName: 'Some Component', + icon: 'Calendar', pluginOptions: { pluginName: { option: true, @@ -107,6 +108,7 @@ describe('Content Type Builder - Components', () => { body: { component: { category: 'default', + icon: 'Calendar', displayName: 'someComponent', attributes: {}, }, @@ -172,6 +174,7 @@ describe('Content Type Builder - Components', () => { category: 'default', schema: { displayName: 'Some Component', + icon: 'Calendar', description: '', collectionName: 'components_default_some_components', pluginOptions: { @@ -254,6 +257,7 @@ describe('Content Type Builder - Components', () => { body: { component: { category: 'default', + icon: 'Calendar', displayName: 'New Component', attributes: { name: { diff --git a/packages/core/admin/admin/src/content-manager/components/ComponentIcon/ComponentIcon.js b/packages/core/admin/admin/src/content-manager/components/ComponentIcon/ComponentIcon.js index b76c84d0e7e..bee2c4e811b 100644 --- a/packages/core/admin/admin/src/content-manager/components/ComponentIcon/ComponentIcon.js +++ b/packages/core/admin/admin/src/content-manager/components/ComponentIcon/ComponentIcon.js @@ -39,6 +39,7 @@ export function ComponentIcon({ showBackground = true, size = 'M', icon }) { justifyContent="center" size={size} showBackground={showBackground} + data-testid={icon ? 'component-card-icon' : 'component-card-icon-default'} > diff --git a/packages/core/admin/admin/src/content-manager/components/DynamicZone/components/tests/ComponentCard.test.js b/packages/core/admin/admin/src/content-manager/components/DynamicZone/components/tests/ComponentCard.test.js new file mode 100644 index 00000000000..0365e75cba5 --- /dev/null +++ b/packages/core/admin/admin/src/content-manager/components/DynamicZone/components/tests/ComponentCard.test.js @@ -0,0 +1,35 @@ +import React from 'react'; +import { fireEvent, render } from '@testing-library/react'; + +import { ThemeProvider, lightTheme } from '@strapi/design-system'; + +import GlobalStyle from '../../../../../components/GlobalStyle'; + +import ComponentCard from '../ComponentCard'; + +describe('ComponentCard', () => { + const setup = (props) => + render( + + test + + + ); + + it('should render default icon if not icon is passed', () => { + const { getByTestId } = setup(); + expect(getByTestId('component-card-icon-default')).toBeInTheDocument(); + }); + + it('should render the passed icon', () => { + const { getByTestId } = setup({ icon: 'Calendar' }); + expect(getByTestId('component-card-icon')).toBeInTheDocument(); + }); + + it('should call the onClick handler when passed', () => { + const onClick = jest.fn(); + const { getByText } = setup({ onClick }); + fireEvent.click(getByText('test')); + expect(onClick).toHaveBeenCalled(); + }); +}); diff --git a/packages/core/content-type-builder/admin/src/pages/ListView/tests/__snapshots__/index.test.js.snap b/packages/core/content-type-builder/admin/src/pages/ListView/tests/__snapshots__/index.test.js.snap index 7e5db316652..af950567aaf 100644 --- a/packages/core/content-type-builder/admin/src/pages/ListView/tests/__snapshots__/index.test.js.snap +++ b/packages/core/content-type-builder/admin/src/pages/ListView/tests/__snapshots__/index.test.js.snap @@ -110,11 +110,11 @@ exports[` renders and matches the snapshot 1`] = ` max-width: 100%; } -.c68 { +.c67 { background: #eaeaef; } -.c70 { +.c69 { background: #f0f0ff; padding: 20px; } @@ -579,7 +579,7 @@ exports[` renders and matches the snapshot 1`] = ` fill: #ffffff; } -.c69 { +.c68 { height: 1px; border: none; -webkit-flex-shrink: 0; @@ -607,7 +607,7 @@ exports[` renders and matches the snapshot 1`] = ` fill: #666687; } -.c72 { +.c71 { height: 1.5rem; width: 1.5rem; border-radius: 50%; @@ -625,16 +625,16 @@ exports[` renders and matches the snapshot 1`] = ` align-items: center; } -.c72 svg { +.c71 svg { height: 0.625rem; width: 0.625rem; } -.c72 svg path { +.c71 svg path { fill: #4945ff; } -.c71 { +.c70 { border-radius: 0 0 4px 4px; display: block; width: 100%; @@ -711,7 +711,6 @@ exports[` renders and matches the snapshot 1`] = ` .c59 { border-radius: 50%; - color: #4945ff; height: 40px; width: 40px; } @@ -721,18 +720,6 @@ exports[` renders and matches the snapshot 1`] = ` width: 20px; } -.c64 { - border-radius: 50%; - color: #666687; - height: 40px; - width: 40px; -} - -.c64 svg { - height: 20px; - width: 20px; -} - .c63 { position: absolute; display: none; @@ -785,6 +772,12 @@ exports[` renders and matches the snapshot 1`] = ` color: #4945ff; } +.c57.active > div:first-child svg path, +.c57:focus > div:first-child svg path, +.c57:hover > div:first-child svg path { + fill: #4945ff; +} + .c37.component-row, .c37.dynamiczone-row { position: relative; @@ -876,7 +869,7 @@ exports[` renders and matches the snapshot 1`] = ` overflow-x: auto; } -.c65 { +.c64 { padding-top: 5.625rem; } @@ -958,7 +951,7 @@ exports[` renders and matches the snapshot 1`] = ` fill: #666687; } -.c67 { +.c66 { height: 1.5rem; width: 1.5rem; border-radius: 50%; @@ -976,12 +969,12 @@ exports[` renders and matches the snapshot 1`] = ` align-items: center; } -.c67 svg { +.c66 svg { height: 0.625rem; width: 0.625rem; } -.c67 svg path { +.c66 svg path { fill: #4945ff; } @@ -1021,7 +1014,7 @@ exports[` renders and matches the snapshot 1`] = ` fill: #eaeaef; } -.c66 { +.c65 { position: relative; -webkit-flex-shrink: 0; -ms-flex-negative: 0; @@ -1031,7 +1024,7 @@ exports[` renders and matches the snapshot 1`] = ` transform: translate(-0.5px,-1px); } -.c66 * { +.c65 * { fill: #d9d8ff; } @@ -3594,7 +3587,7 @@ exports[` renders and matches the snapshot 1`] = ` type="button" >
renders and matches the snapshot 1`] = ` type="button" >
renders and matches the snapshot 1`] = ` type="button" >
renders and matches the snapshot 1`] = `
renders and matches the snapshot 1`] = ` class="c39" > renders and matches the snapshot 1`] = ` class="c39" > renders and matches the snapshot 1`] = ` class="c39" > renders and matches the snapshot 1`] = ` > +`; diff --git a/packages/core/content-type-builder/admin/src/components/IconPicker/tests/index.test.js b/packages/core/content-type-builder/admin/src/components/IconPicker/tests/index.test.js new file mode 100644 index 00000000000..1bb9ffa3c1a --- /dev/null +++ b/packages/core/content-type-builder/admin/src/components/IconPicker/tests/index.test.js @@ -0,0 +1,112 @@ +import React from 'react'; +import { ThemeProvider, lightTheme } from '@strapi/design-system'; +import { IntlProvider } from 'react-intl'; +import { render, screen, fireEvent } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { createMemoryHistory } from 'history'; +import { Router } from 'react-router-dom'; +import IconPicker from '../index'; + +const makeApp = (props) => { + const history = createMemoryHistory(); + + const defaultProps = { + intlLabel: { + id: 'content-type-builder.modalForm.components.icon.label', + defaultMessage: 'Icon', + }, + name: 'componentToCreate.icon', + onChange: jest.fn(), + value: '', + }; + + return ( + + + + + + + + ); +}; + +describe('IconPicker', () => { + it('should render', () => { + const App = makeApp(); + const { container } = render(App); + + expect(container).toMatchSnapshot(); + }); + + it('should show the search icon by default and no search bar', () => { + const App = makeApp(); + render(App); + + expect(screen.getByText('Search icon button')).toBeInTheDocument(); + }); + + it('should show the searchbar if the search icon is clicked', async () => { + const App = makeApp(); + render(App); + + await userEvent.click(screen.getByText('Search icon button')); + + expect(screen.getByPlaceholderText('Search for an icon')).toBeInTheDocument(); + }); + + it('should filter icons when write on the searchbar', async () => { + const App = makeApp(); + render(App); + + await userEvent.click(screen.getByText('Search icon button')); + await userEvent.type(screen.getByPlaceholderText('Search for an icon'), 'calendar'); + + expect(screen.getByText('Select Calendar icon')).toBeInTheDocument(); + expect(screen.queryByText('Select Trash icon')).not.toBeInTheDocument(); + }); + + it('should not render delete button if there is no icon selected', () => { + const App = makeApp(); + render(App); + + expect(screen.queryByText('Remove the selected icon')).not.toBeInTheDocument(); + }); + + it('should render delete button if there is an icon selected', async () => { + const App = makeApp({ value: 'Calendar' }); + render(App); + + expect(screen.getByText('Remove the selected icon')).toBeInTheDocument(); + }); + + it('should call onChange with an empty string when clicking on the delete button', async () => { + const onChangeMock = jest.fn(); + const App = makeApp({ value: 'Calendar', onChange: onChangeMock }); + render(App); + + await userEvent.click(screen.getByText('Remove the selected icon')); + + expect(onChangeMock).toHaveBeenCalledWith({ + target: { name: 'componentToCreate.icon', value: '' }, + }); + }); + + it('should call onChange with the icon name when clicking on an icon', async () => { + const onChangeMock = jest.fn(); + const App = makeApp({ onChange: onChangeMock }); + render(App); + + fireEvent.click(screen.getByLabelText('Select Calendar icon')); + + expect(onChangeMock); + expect(onChangeMock).toHaveBeenCalledWith( + expect.objectContaining({ + target: expect.objectContaining({ + name: 'componentToCreate.icon', + value: 'Calendar', + }), + }) + ); + }); +}); diff --git a/packages/core/content-type-builder/admin/src/pages/ListView/tests/__snapshots__/index.test.js.snap b/packages/core/content-type-builder/admin/src/pages/ListView/tests/__snapshots__/index.test.js.snap index af950567aaf..f345fcaf70b 100644 --- a/packages/core/content-type-builder/admin/src/pages/ListView/tests/__snapshots__/index.test.js.snap +++ b/packages/core/content-type-builder/admin/src/pages/ListView/tests/__snapshots__/index.test.js.snap @@ -106,15 +106,19 @@ exports[` renders and matches the snapshot 1`] = ` } .c60 { + color: #666687; +} + +.c62 { margin-top: 4px; max-width: 100%; } -.c67 { +.c69 { background: #eaeaef; } -.c69 { +.c71 { background: #f0f0ff; padding: 20px; } @@ -175,7 +179,7 @@ exports[` renders and matches the snapshot 1`] = ` color: #4945ff; } -.c61 { +.c63 { font-size: 0.75rem; line-height: 1.33; display: block; @@ -350,6 +354,10 @@ exports[` renders and matches the snapshot 1`] = ` justify-content: center; } +.c61 path { + fill: #666687; +} + .c12 { position: relative; outline: none; @@ -579,7 +587,7 @@ exports[` renders and matches the snapshot 1`] = ` fill: #ffffff; } -.c68 { +.c70 { height: 1px; border: none; -webkit-flex-shrink: 0; @@ -607,7 +615,7 @@ exports[` renders and matches the snapshot 1`] = ` fill: #666687; } -.c71 { +.c73 { height: 1.5rem; width: 1.5rem; border-radius: 50%; @@ -625,16 +633,16 @@ exports[` renders and matches the snapshot 1`] = ` align-items: center; } -.c71 svg { +.c73 svg { height: 0.625rem; width: 0.625rem; } -.c71 svg path { +.c73 svg path { fill: #4945ff; } -.c70 { +.c72 { border-radius: 0 0 4px 4px; display: block; width: 100%; @@ -720,19 +728,19 @@ exports[` renders and matches the snapshot 1`] = ` width: 20px; } -.c63 { +.c65 { position: absolute; display: none; top: 5px; right: 0.5rem; } -.c63 svg { +.c65 svg { width: 0.625rem; height: 0.625rem; } -.c63 svg path { +.c65 svg path { fill: #4945ff; } @@ -753,9 +761,9 @@ exports[` renders and matches the snapshot 1`] = ` background: #f0f0ff; } -.c57.active .c62, -.c57:focus .c62, -.c57:hover .c62 { +.c57.active .c64, +.c57:focus .c64, +.c57:hover .c64 { display: block; } @@ -869,7 +877,7 @@ exports[` renders and matches the snapshot 1`] = ` overflow-x: auto; } -.c64 { +.c66 { padding-top: 5.625rem; } @@ -951,7 +959,7 @@ exports[` renders and matches the snapshot 1`] = ` fill: #666687; } -.c66 { +.c68 { height: 1.5rem; width: 1.5rem; border-radius: 50%; @@ -969,12 +977,12 @@ exports[` renders and matches the snapshot 1`] = ` align-items: center; } -.c66 svg { +.c68 svg { height: 0.625rem; width: 0.625rem; } -.c66 svg path { +.c68 svg path { fill: #4945ff; } @@ -1014,7 +1022,7 @@ exports[` renders and matches the snapshot 1`] = ` fill: #eaeaef; } -.c65 { +.c67 { position: relative; -webkit-flex-shrink: 0; -ms-flex-negative: 0; @@ -1024,7 +1032,7 @@ exports[` renders and matches the snapshot 1`] = ` transform: translate(-0.5px,-1px); } -.c65 * { +.c67 * { fill: #d9d8ff; } @@ -3547,25 +3555,19 @@ exports[` renders and matches the snapshot 1`] = `
- - - +
- + + + +
renders and matches the snapshot 1`] = ` class="c39" > renders and matches the snapshot 1`] = ` class="c39" > renders and matches the snapshot 1`] = ` class="c39" > renders and matches the snapshot 1`] = ` >
@@ -1757,8 +1755,8 @@ exports[`IconPicker should render 1`] = ` >
@@ -1807,8 +1804,8 @@ exports[`IconPicker should render 1`] = ` >
@@ -1857,8 +1854,8 @@ exports[`IconPicker should render 1`] = ` >
@@ -1909,8 +1904,8 @@ exports[`IconPicker should render 1`] = ` >
@@ -1959,8 +1954,8 @@ exports[`IconPicker should render 1`] = ` >
@@ -2009,8 +2004,8 @@ exports[`IconPicker should render 1`] = ` >
@@ -2059,8 +2054,8 @@ exports[`IconPicker should render 1`] = ` >
@@ -2111,8 +2104,8 @@ exports[`IconPicker should render 1`] = ` >