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..f55d3d1d2d2 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 f1ccfa2d36f..21ceb566f9a 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 @@ -1,49 +1,39 @@ 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 { COMPONENT_ICONS } from './constants'; -const WIDTH_S = 5; -const WIDTH_M = 8; - -const Wrapper = styled(Flex)` - border-radius: ${({ showBackground }) => (showBackground ? `50%` : 0)}; - color: ${({ theme }) => theme.colors.neutral600}; - height: ${({ theme, size }) => theme.spaces[size === 'S' ? WIDTH_S : WIDTH_M]}; - width: ${({ theme, size }) => theme.spaces[size === 'S' ? WIDTH_S : WIDTH_M]}; - - svg { - height: ${({ theme, size }) => theme.spaces[size === 'S' ? WIDTH_S - 2 : WIDTH_M - 3]}; - width: ${({ theme, size }) => theme.spaces[size === 'S' ? WIDTH_S - 2 : WIDTH_M - 3]}; - } -`; - -export function ComponentIcon({ showBackground = true, size = 'M' }) { +export function ComponentIcon({ showBackground = true, size = 'M', icon }) { return ( - - - - - + + ); } ComponentIcon.defaultProps = { showBackground: true, size: 'M', + icon: 'Cube', }; ComponentIcon.propTypes = { showBackground: PropTypes.bool, size: PropTypes.string, + icon: PropTypes.string, }; diff --git a/packages/core/admin/admin/src/content-manager/components/ComponentIcon/constants.js b/packages/core/admin/admin/src/content-manager/components/ComponentIcon/constants.js new file mode 100644 index 00000000000..12781393cfb --- /dev/null +++ b/packages/core/admin/admin/src/content-manager/components/ComponentIcon/constants.js @@ -0,0 +1,133 @@ +import * as Icons from '@strapi/icons'; + +const COMPONENT_ICONS = { + alien: Icons.Alien, + apps: Icons.Apps, + archive: Icons.Archive, + arrowDown: Icons.ArrowDown, + arrowLeft: Icons.ArrowLeft, + arrowRight: Icons.ArrowRight, + arrowUp: Icons.ArrowUp, + attachment: Icons.Attachment, + bell: Icons.Bell, + bold: Icons.Bold, + book: Icons.Book, + briefcase: Icons.Briefcase, + brush: Icons.Brush, + bulletList: Icons.BulletList, + calendar: Icons.Calendar, + car: Icons.Car, + cast: Icons.Cast, + chartBubble: Icons.ChartBubble, + chartCircle: Icons.ChartCircle, + chartPie: Icons.ChartPie, + check: Icons.Check, + clock: Icons.Clock, + cloud: Icons.Cloud, + code: Icons.Code, + cog: Icons.Cog, + collapse: Icons.Collapse, + command: Icons.Command, + connector: Icons.Connector, + crop: Icons.Crop, + crown: Icons.Crown, + cube: Icons.Cube, + cup: Icons.Cup, + cursor: Icons.Cursor, + dashboard: Icons.Dashboard, + database: Icons.Database, + discuss: Icons.Discuss, + doctor: Icons.Doctor, + earth: Icons.Earth, + emotionHappy: Icons.EmotionHappy, + emotionUnhappy: Icons.EmotionUnhappy, + envelop: Icons.Envelop, + exit: Icons.Exit, + expand: Icons.Expand, + eye: Icons.Eye, + feather: Icons.Feather, + file: Icons.File, + fileError: Icons.FileError, + filePdf: Icons.FilePdf, + filter: Icons.Filter, + folder: Icons.Folder, + gate: Icons.Gate, + gift: Icons.Gift, + globe: Icons.Globe, + grid: Icons.Grid, + handHeart: Icons.HandHeart, + hashtag: Icons.Hashtag, + headphone: Icons.Headphone, + heart: Icons.Heart, + house: Icons.House, + information: Icons.Information, + italic: Icons.Italic, + key: Icons.Key, + landscape: Icons.Landscape, + layer: Icons.Layer, + layout: Icons.Layout, + lightbulb: Icons.Lightbulb, + link: Icons.Link, + lock: Icons.Lock, + magic: Icons.Magic, + manyToMany: Icons.ManyToMany, + manyToOne: Icons.ManyToOne, + manyWays: Icons.ManyWays, + medium: Icons.Medium, + message: Icons.Message, + microphone: Icons.Microphone, + monitor: Icons.Monitor, + moon: Icons.Moon, + music: Icons.Music, + oneToMany: Icons.OneToMany, + oneToOne: Icons.OneToOne, + oneWay: Icons.OneWay, + paint: Icons.Paint, + paintBrush: Icons.PaintBrush, + paperPlane: Icons.PaperPlane, + pencil: Icons.Pencil, + phone: Icons.Phone, + picture: Icons.Picture, + pin: Icons.Pin, + pinMap: Icons.PinMap, + plane: Icons.Plane, + play: Icons.Play, + plus: Icons.Plus, + priceTag: Icons.PriceTag, + puzzle: Icons.Puzzle, + question: Icons.Question, + quote: Icons.Quote, + refresh: Icons.Refresh, + repeat: Icons.Repeat, + restaurant: Icons.Restaurant, + rocket: Icons.Rocket, + rotate: Icons.Rotate, + scissors: Icons.Scissors, + search: Icons.Search, + seed: Icons.Seed, + server: Icons.Server, + shield: Icons.Shield, + shirt: Icons.Shirt, + shoppingCart: Icons.ShoppingCart, + slideshow: Icons.Slideshow, + stack: Icons.Stack, + star: Icons.Star, + store: Icons.Store, + strikeThrough: Icons.StrikeThrough, + sun: Icons.Sun, + television: Icons.Television, + thumbDown: Icons.ThumbDown, + thumbUp: Icons.ThumbUp, + train: Icons.Train, + twitter: Icons.Twitter, + typhoon: Icons.Typhoon, + underline: Icons.Underline, + user: Icons.User, + volumeMute: Icons.VolumeMute, + volumeUp: Icons.VolumeUp, + walk: Icons.Walk, + wheelchair: Icons.Wheelchair, + write: Icons.Write, +}; + +export { COMPONENT_ICONS }; 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..e3bfc6c1fc1 --- /dev/null +++ b/packages/core/admin/admin/src/content-manager/components/DynamicZone/components/ComponentCard.js @@ -0,0 +1,72 @@ +/** + * + * 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() {}, + icon: 'Cube', +}; + +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/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..d50b0feeeac --- /dev/null +++ b/packages/core/admin/admin/src/content-manager/components/DynamicZone/components/tests/ComponentCard.test.js @@ -0,0 +1,25 @@ +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 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/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..5b08501e545 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 @@ -25,6 +25,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}; + } + } } } `; @@ -32,6 +38,7 @@ const CustomLink = styled(Flex)` const DynamicZoneList = ({ components }) => { const { componentLayouts } = useLayoutDnd(); + return ( {components.map((componentUid) => ( @@ -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 74db2d3ee21..8d8b389af9e 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 @@ -1,43 +1,30 @@ 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 { COMPONENT_ICONS } from '../../IconPicker/constants'; -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]}; - - svg { - height: ${({ theme }) => theme.spaces[5]}; - width: ${({ theme }) => theme.spaces[5]}; - } -`; - -export function ComponentIcon({ isActive }) { +export function ComponentIcon({ isActive, icon }) { return ( - - - - - + + ); } ComponentIcon.defaultProps = { isActive: false, + icon: 'Cube', }; ComponentIcon.propTypes = { isActive: PropTypes.bool, + icon: PropTypes.string, }; 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..3f83bcad120 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) => { @@ -76,7 +82,6 @@ function ComponentCard({ component, dzName, index, isActive, isInDevelopmentMode return ( - + @@ -97,7 +107,7 @@ function ComponentCard({ component, dzName, index, isActive, isInDevelopmentMode {isInDevelopmentMode && ( - + )} diff --git a/packages/core/content-type-builder/admin/src/components/DynamicZoneList/index.js b/packages/core/content-type-builder/admin/src/components/DynamicZoneList/index.js index 6967eb8d920..541569e6b75 100644 --- a/packages/core/content-type-builder/admin/src/components/DynamicZoneList/index.js +++ b/packages/core/content-type-builder/admin/src/components/DynamicZoneList/index.js @@ -87,19 +87,21 @@ function DynamicZoneList({ customRowComponent, components, addComponent, name, t )} - {components.map((component, index) => { - return ( - toggle(index)} - /> - ); - })} + + {components.map((component, index) => { + return ( + toggle(index)} + /> + ); + })} + @@ -111,7 +113,10 @@ function DynamicZoneList({ customRowComponent, components, addComponent, name, t return ( 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/constants.js b/packages/core/content-type-builder/admin/src/components/IconPicker/constants.js new file mode 100644 index 00000000000..7496aeaafa8 --- /dev/null +++ b/packages/core/content-type-builder/admin/src/components/IconPicker/constants.js @@ -0,0 +1,132 @@ +import * as Icons from '@strapi/icons'; + +const COMPONENT_ICONS = { + alien: Icons.Alien, + apps: Icons.Apps, + archive: Icons.Archive, + arrowDown: Icons.ArrowDown, + arrowLeft: Icons.ArrowLeft, + arrowRight: Icons.ArrowRight, + arrowUp: Icons.ArrowUp, + attachment: Icons.Attachment, + bell: Icons.Bell, + bold: Icons.Bold, + book: Icons.Book, + briefcase: Icons.Briefcase, + brush: Icons.Brush, + bulletList: Icons.BulletList, + calendar: Icons.Calendar, + car: Icons.Car, + cast: Icons.Cast, + chartBubble: Icons.ChartBubble, + chartCircle: Icons.ChartCircle, + chartPie: Icons.ChartPie, + check: Icons.Check, + clock: Icons.Clock, + cloud: Icons.Cloud, + code: Icons.Code, + cog: Icons.Cog, + collapse: Icons.Collapse, + command: Icons.Command, + connector: Icons.Connector, + crop: Icons.Crop, + crown: Icons.Crown, + cube: Icons.Cube, + cup: Icons.Cup, + cursor: Icons.Cursor, + dashboard: Icons.Dashboard, + database: Icons.Database, + discuss: Icons.Discuss, + doctor: Icons.Doctor, + earth: Icons.Earth, + emotionHappy: Icons.EmotionHappy, + emotionUnhappy: Icons.EmotionUnhappy, + envelop: Icons.Envelop, + exit: Icons.Exit, + expand: Icons.Expand, + eye: Icons.Eye, + feather: Icons.Feather, + file: Icons.File, + fileError: Icons.FileError, + filePdf: Icons.FilePdf, + filter: Icons.Filter, + folder: Icons.Folder, + gate: Icons.Gate, + gift: Icons.Gift, + globe: Icons.Globe, + grid: Icons.Grid, + handHeart: Icons.HandHeart, + hashtag: Icons.Hashtag, + headphone: Icons.Headphone, + heart: Icons.Heart, + house: Icons.House, + information: Icons.Information, + italic: Icons.Italic, + key: Icons.Key, + landscape: Icons.Landscape, + layer: Icons.Layer, + layout: Icons.Layout, + lightbulb: Icons.Lightbulb, + link: Icons.Link, + lock: Icons.Lock, + magic: Icons.Magic, + manyToMany: Icons.ManyToMany, + manyToOne: Icons.ManyToOne, + manyWays: Icons.ManyWays, + medium: Icons.Medium, + message: Icons.Message, + microphone: Icons.Microphone, + monitor: Icons.Monitor, + moon: Icons.Moon, + music: Icons.Music, + oneToMany: Icons.OneToMany, + oneToOne: Icons.OneToOne, + oneWay: Icons.OneWay, + paint: Icons.Paint, + paintBrush: Icons.PaintBrush, + paperPlane: Icons.PaperPlane, + pencil: Icons.Pencil, + phone: Icons.Phone, + picture: Icons.Picture, + pin: Icons.Pin, + pinMap: Icons.PinMap, + plane: Icons.Plane, + play: Icons.Play, + plus: Icons.Plus, + priceTag: Icons.PriceTag, + puzzle: Icons.Puzzle, + question: Icons.Question, + quote: Icons.Quote, + repeat: Icons.Repeat, + restaurant: Icons.Restaurant, + rocket: Icons.Rocket, + rotate: Icons.Rotate, + scissors: Icons.Scissors, + search: Icons.Search, + seed: Icons.Seed, + server: Icons.Server, + shield: Icons.Shield, + shirt: Icons.Shirt, + shoppingCart: Icons.ShoppingCart, + slideshow: Icons.Slideshow, + stack: Icons.Stack, + star: Icons.Star, + store: Icons.Store, + strikeThrough: Icons.StrikeThrough, + sun: Icons.Sun, + television: Icons.Television, + thumbDown: Icons.ThumbDown, + thumbUp: Icons.ThumbUp, + train: Icons.Train, + twitter: Icons.Twitter, + typhoon: Icons.Typhoon, + underline: Icons.Underline, + user: Icons.User, + volumeMute: Icons.VolumeMute, + volumeUp: Icons.VolumeUp, + walk: Icons.Walk, + wheelchair: Icons.Wheelchair, + write: Icons.Write, +}; + +export { COMPONENT_ICONS }; 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..ee0dda8d528 --- /dev/null +++ b/packages/core/content-type-builder/admin/src/components/IconPicker/index.js @@ -0,0 +1,219 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { + Box, + Flex, + Icon, + Typography, + Searchbar, + IconButton, + inputFocusStyle, + VisuallyHidden, + Field, + FieldLabel, + FieldInput, + Tooltip, +} from '@strapi/design-system'; +import { Trash, Search } from '@strapi/icons'; +import { useIntl } from 'react-intl'; +import styled from 'styled-components'; +import PropTypes from 'prop-types'; + +import { getTrad } from '../../utils'; +import { COMPONENT_ICONS } from './constants'; + +const IconPickerWrapper = styled(Flex)` + label { + ${inputFocusStyle} + border-radius: ${({ theme }) => theme.borderRadius}; + border: 1px solid ${({ theme }) => theme.colors.neutral100}; + } +`; + +const IconPick = ({ iconKey, name, onChange, isSelected, ariaLabel }) => { + return ( + + + + + {ariaLabel} + + + + + + + ); +}; + +IconPick.propTypes = { + iconKey: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + onChange: PropTypes.func.isRequired, + isSelected: PropTypes.bool.isRequired, + ariaLabel: PropTypes.string.isRequired, +}; + +const IconPicker = ({ intlLabel, name, onChange, value }) => { + const { formatMessage } = useIntl(); + const [showSearch, setShowSearch] = useState(false); + const [search, setSearch] = useState(''); + const allIcons = Object.keys(COMPONENT_ICONS); + const [icons, setIcons] = useState(allIcons); + const searchIconRef = useRef(null); + const searchBarRef = useRef(null); + + const toggleSearch = () => { + setShowSearch(!showSearch); + }; + + const onChangeSearch = ({ target: { value } }) => { + setSearch(value); + setIcons(() => allIcons.filter((icon) => icon.toLowerCase().includes(value.toLowerCase()))); + }; + + const onClearSearch = () => { + toggleSearch(); + setSearch(''); + setIcons(allIcons); + }; + + const removeIconSelected = () => { + onChange({ target: { name, value: '' } }); + }; + + useEffect(() => { + if (showSearch) { + searchBarRef.current.focus(); + } + }, [showSearch]); + + return ( + <> + + + {formatMessage(intlLabel)} + + + {showSearch ? ( + { + if (!search) { + toggleSearch(); + } + }} + onChange={onChangeSearch} + value={search} + onClear={onClearSearch} + clearLabel={formatMessage({ + id: getTrad('IconPicker.search.clear.label'), + defaultMessage: 'Clear the icon search', + })} + > + {formatMessage({ + id: getTrad('IconPicker.search.placeholder.label'), + defaultMessage: 'Search for an icon', + })} + + ) : ( + } + noBorder + /> + )} + {value && ( + + } + noBorder + /> + + )} + + + + {icons.length > 0 ? ( + icons.map((iconKey) => ( + + )) + ) : ( + + + {formatMessage({ + id: getTrad('IconPicker.emptyState.label'), + defaultMessage: 'No icon found', + })} + + + )} + + + ); +}; + +IconPicker.defaultProps = { + value: '', +}; + +IconPicker.propTypes = { + intlLabel: PropTypes.object.isRequired, + name: PropTypes.string.isRequired, + onChange: PropTypes.func.isRequired, + value: PropTypes.string, +}; + +export default IconPicker; 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..4d86884a235 --- /dev/null +++ b/packages/core/content-type-builder/admin/src/components/IconPicker/tests/index.test.js @@ -0,0 +1,98 @@ +import React from 'react'; +import { ThemeProvider, lightTheme } from '@strapi/design-system'; +import { IntlProvider } from 'react-intl'; +import { render, fireEvent } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { MemoryRouter } from 'react-router-dom'; +import IconPicker from '../index'; + +const defaultProps = { + intlLabel: { + id: 'content-type-builder.modalForm.components.icon.label', + defaultMessage: 'Icon', + }, + name: 'componentToCreate.icon', + onChange: jest.fn(), + value: '', +}; + +const setup = (props) => { + return { + ...render(, { + wrapper: ({ children }) => ( + + + {children} + + + ), + }), + user: userEvent.setup(), + }; +}; + +describe('IconPicker', () => { + it('should show the search icon by default and no search bar', () => { + const { getByText } = setup(); + + expect(getByText('Search icon button')).toBeInTheDocument(); + }); + + it('should show the searchbar if the search icon is clicked', async () => { + const { getByText, getByPlaceholderText, user } = setup(); + + await user.click(getByText('Search icon button')); + + expect(getByPlaceholderText('Search for an icon')).toBeInTheDocument(); + }); + + it('should filter icons when write on the searchbar', async () => { + const { user, getByText, getByPlaceholderText, queryByText } = setup(); + + await user.click(getByText('Search icon button')); + await user.type(getByPlaceholderText('Search for an icon'), 'calendar'); + + expect(getByText('Select calendar icon')).toBeInTheDocument(); + expect(queryByText('Select Trash icon')).not.toBeInTheDocument(); + }); + + it('should not render delete button if there is no icon selected', () => { + const { queryByText } = setup(); + + expect(queryByText('Remove the selected icon')).not.toBeInTheDocument(); + }); + + it('should render delete button if there is an icon selected', async () => { + const { getByText } = setup({ value: 'calendar' }); + + expect(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 { user, getByText } = setup({ value: 'calendar', onChange: onChangeMock }); + + await user.click(getByText('Remove the selected icon button')); + + 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 { getByLabelText } = setup({ onChange: onChangeMock }); + + fireEvent.click(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 7e5db316652..e252af753c6 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 @@ -99,22 +99,43 @@ exports[` renders and matches the snapshot 1`] = ` -webkit-flex-shrink: 0; -ms-flex-negative: 0; flex-shrink: 0; + cursor: pointer; } .c58 { background: #d9d8ff; + border-radius: 50%; + width: 40px; + height: 40px; +} + +.c59 { + color: #666687; + width: 20px; + height: 20px; } -.c60 { +.c61 { margin-top: 4px; max-width: 100%; } +.c65 { + background: #dcdce4; + border-radius: 50%; + width: 40px; + height: 40px; +} + .c68 { - background: #eaeaef; + background: #d9d8ff; } .c70 { + background: #eaeaef; +} + +.c72 { background: #f0f0ff; padding: 20px; } @@ -175,7 +196,7 @@ exports[` renders and matches the snapshot 1`] = ` color: #4945ff; } -.c61 { +.c62 { font-size: 0.75rem; line-height: 1.33; display: block; @@ -350,6 +371,10 @@ exports[` renders and matches the snapshot 1`] = ` justify-content: center; } +.c60 path { + fill: #666687; +} + .c12 { position: relative; outline: none; @@ -579,7 +604,7 @@ exports[` renders and matches the snapshot 1`] = ` fill: #ffffff; } -.c69 { +.c71 { height: 1px; border: none; -webkit-flex-shrink: 0; @@ -607,7 +632,7 @@ exports[` renders and matches the snapshot 1`] = ` fill: #666687; } -.c72 { +.c74 { height: 1.5rem; width: 1.5rem; border-radius: 50%; @@ -625,16 +650,16 @@ exports[` renders and matches the snapshot 1`] = ` align-items: center; } -.c72 svg { +.c74 svg { height: 0.625rem; width: 0.625rem; } -.c72 svg path { +.c74 svg path { fill: #4945ff; } -.c71 { +.c73 { border-radius: 0 0 4px 4px; display: block; width: 100%; @@ -709,43 +734,19 @@ exports[` renders and matches the snapshot 1`] = ` border: 2px solid #4945ff; } -.c59 { - border-radius: 50%; - color: #4945ff; - height: 40px; - width: 40px; -} - -.c59 svg { - height: 20px; - width: 20px; -} - .c64 { - border-radius: 50%; - color: #666687; - height: 40px; - width: 40px; -} - -.c64 svg { - height: 20px; - width: 20px; -} - -.c63 { position: absolute; display: none; top: 5px; right: 0.5rem; } -.c63 svg { +.c64 svg { width: 0.625rem; height: 0.625rem; } -.c63 svg path { +.c64 svg path { fill: #4945ff; } @@ -766,9 +767,9 @@ exports[` renders and matches the snapshot 1`] = ` background: #f0f0ff; } -.c57.active .c62, -.c57:focus .c62, -.c57:hover .c62 { +.c57.active .c63, +.c57:focus .c63, +.c57:hover .c63 { display: block; } @@ -785,6 +786,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 +883,7 @@ exports[` renders and matches the snapshot 1`] = ` overflow-x: auto; } -.c65 { +.c66 { padding-top: 5.625rem; } @@ -958,7 +965,7 @@ exports[` renders and matches the snapshot 1`] = ` fill: #666687; } -.c67 { +.c69 { height: 1.5rem; width: 1.5rem; border-radius: 50%; @@ -976,12 +983,12 @@ exports[` renders and matches the snapshot 1`] = ` align-items: center; } -.c67 svg { +.c69 svg { height: 0.625rem; width: 0.625rem; } -.c67 svg path { +.c69 svg path { fill: #4945ff; } @@ -1021,7 +1028,7 @@ exports[` renders and matches the snapshot 1`] = ` fill: #eaeaef; } -.c66 { +.c67 { position: relative; -webkit-flex-shrink: 0; -ms-flex-negative: 0; @@ -1031,7 +1038,7 @@ exports[` renders and matches the snapshot 1`] = ` transform: translate(-0.5px,-1px); } -.c66 * { +.c67 * { fill: #d9d8ff; } @@ -3547,181 +3554,216 @@ exports[` renders and matches the snapshot 1`] = ` - - -
- -
- - - - - -
- -
- - + + + +
@@ -3751,7 +3793,7 @@ exports[` 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`] = ` >