diff --git a/packages/core/admin/admin/src/hooks/useFetchMarketplaceProviders/index.js b/packages/core/admin/admin/src/hooks/useFetchMarketplaceProviders/index.js index 9970a335c5..ddb7d26f4e 100644 --- a/packages/core/admin/admin/src/hooks/useFetchMarketplaceProviders/index.js +++ b/packages/core/admin/admin/src/hooks/useFetchMarketplaceProviders/index.js @@ -1,11 +1,11 @@ import { useQuery } from 'react-query'; import { useNotification } from '@strapi/helper-plugin'; -import { fetchMarketplacePlugins } from './utils/api'; +import { fetchMarketplaceProviders } from './utils/api'; const useFetchMarketplaceProviders = (notifyLoad) => { const toggleNotification = useNotification(); - return useQuery('list-marketplace-providers', () => fetchMarketplacePlugins(), { + return useQuery('list-marketplace-providers', () => fetchMarketplaceProviders(), { onSuccess() { if (notifyLoad) { notifyLoad(); diff --git a/packages/core/admin/admin/src/hooks/useFetchMarketplaceProviders/utils/api.js b/packages/core/admin/admin/src/hooks/useFetchMarketplaceProviders/utils/api.js index c1776b1bfd..2c01ea2c94 100644 --- a/packages/core/admin/admin/src/hooks/useFetchMarketplaceProviders/utils/api.js +++ b/packages/core/admin/admin/src/hooks/useFetchMarketplaceProviders/utils/api.js @@ -2,10 +2,10 @@ import axios from 'axios'; const MARKETPLACE_API_URL = 'https://market-api.strapi.io'; -const fetchMarketplacePlugins = async () => { +const fetchMarketplaceProviders = async () => { const { data } = await axios.get(`${MARKETPLACE_API_URL}/providers`); return data; }; -export { fetchMarketplacePlugins }; +export { fetchMarketplaceProviders }; diff --git a/packages/core/admin/admin/src/pages/MarketplacePage/components/NpmPackagesFilters/FilterSelect.js b/packages/core/admin/admin/src/pages/MarketplacePage/components/NpmPackagesFilters/FilterSelect.js new file mode 100644 index 0000000000..115b95c7db --- /dev/null +++ b/packages/core/admin/admin/src/pages/MarketplacePage/components/NpmPackagesFilters/FilterSelect.js @@ -0,0 +1,37 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Select, Option } from '@strapi/design-system/Select'; + +const FilterSelect = ({ message, value, onChange, possibleFilters, onClear, customizeContent }) => { + return ( + + ); +}; + +FilterSelect.propTypes = { + message: PropTypes.string.isRequired, + value: PropTypes.array.isRequired, + onChange: PropTypes.func.isRequired, + possibleFilters: PropTypes.object.isRequired, + onClear: PropTypes.func.isRequired, + customizeContent: PropTypes.func.isRequired, +}; + +export default FilterSelect; diff --git a/packages/core/admin/admin/src/pages/MarketplacePage/components/NpmPackagesFilters/FiltersPopover.js b/packages/core/admin/admin/src/pages/MarketplacePage/components/NpmPackagesFilters/FiltersPopover.js new file mode 100644 index 0000000000..71220e843c --- /dev/null +++ b/packages/core/admin/admin/src/pages/MarketplacePage/components/NpmPackagesFilters/FiltersPopover.js @@ -0,0 +1,88 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Box } from '@strapi/design-system/Box'; +import { Popover } from '@strapi/design-system/Popover'; +import { Stack } from '@strapi/design-system/Stack'; +import { FocusTrap } from '@strapi/design-system/FocusTrap'; +import { useIntl } from 'react-intl'; +import FilterSelect from './FilterSelect'; + +const FiltersPopover = ({ + source, + onToggle, + query, + setQuery, + npmPackageType, + possibleCategories, + possibleCollections, +}) => { + const { formatMessage } = useIntl(); + + return ( + {}}> + + + + setQuery({ collections: newCollections })} + onClear={() => setQuery({ collections: [] }, 'remove')} + possibleFilters={possibleCollections} + customizeContent={(values) => + formatMessage( + { + id: 'admin.pages.MarketPlacePage.filters.collectionsSelected', + defaultMessage: + '{count, plural, =0 {No collections} one {# collection} other {# collections}} selected', + }, + { count: values.length } + ) + } + /> + + {npmPackageType === 'plugin' && ( + + setQuery({ categories: newCategories })} + onClear={() => setQuery({ categories: [] }, 'remove')} + possibleFilters={possibleCategories} + customizeContent={(values) => + formatMessage( + { + id: 'admin.pages.MarketPlacePage.filters.categoriesSelected', + defaultMessage: + '{count, plural, =0 {No categories} one {# category} other {# categories}} selected', + }, + { count: values.length } + ) + } + name="categories" + /> + + )} + + + + ); +}; + +FiltersPopover.propTypes = { + onToggle: PropTypes.func.isRequired, + source: PropTypes.shape({ current: PropTypes.instanceOf(Element) }).isRequired, + query: PropTypes.object.isRequired, + setQuery: PropTypes.func.isRequired, + npmPackageType: PropTypes.oneOf(['plugin', 'provider']).isRequired, + possibleCollections: PropTypes.object.isRequired, + possibleCategories: PropTypes.object.isRequired, +}; + +export default FiltersPopover; diff --git a/packages/core/admin/admin/src/pages/MarketplacePage/components/NpmPackagesFilters/index.js b/packages/core/admin/admin/src/pages/MarketplacePage/components/NpmPackagesFilters/index.js new file mode 100644 index 0000000000..7086b08679 --- /dev/null +++ b/packages/core/admin/admin/src/pages/MarketplacePage/components/NpmPackagesFilters/index.js @@ -0,0 +1,102 @@ +import React, { useState, useRef } from 'react'; +import PropTypes from 'prop-types'; +import { useIntl } from 'react-intl'; +import { Box } from '@strapi/design-system/Box'; +import { Button } from '@strapi/design-system/Button'; +import { Tag } from '@strapi/design-system/Tag'; +import Cross from '@strapi/icons/Cross'; +import Filter from '@strapi/icons/Filter'; +import FiltersPopover from './FiltersPopover'; + +const FilterTag = ({ name, handleRemove }) => { + return ( + + } onClick={handleRemove}> + {name} + + + ); +}; + +const NpmPackagesFilters = ({ + possibleCollections, + possibleCategories, + npmPackageType, + query, + setQuery, +}) => { + const [isVisible, setIsVisible] = useState(false); + const buttonRef = useRef(); + const { formatMessage } = useIntl(); + + const handleToggle = () => setIsVisible((prev) => !prev); + + return ( + <> + + + {isVisible && ( + + )} + + {query.collections?.map((collection) => ( + + setQuery({ + collections: query.collections.filter( + (previousCollection) => previousCollection !== collection + ), + }) + } + /> + ))} + {npmPackageType === 'plugin' && + query.categories?.map((category) => ( + + setQuery({ + categories: query.categories.filter( + (previousCategory) => previousCategory !== category + ), + }) + } + /> + ))} + + ); +}; + +FilterTag.propTypes = { + name: PropTypes.string.isRequired, + handleRemove: PropTypes.func.isRequired, +}; + +NpmPackagesFilters.propTypes = { + npmPackageType: PropTypes.oneOf(['plugin', 'provider']).isRequired, + possibleCollections: PropTypes.object.isRequired, + possibleCategories: PropTypes.object.isRequired, + query: PropTypes.object.isRequired, + setQuery: PropTypes.func.isRequired, +}; + +export default NpmPackagesFilters; diff --git a/packages/core/admin/admin/src/pages/MarketplacePage/index.js b/packages/core/admin/admin/src/pages/MarketplacePage/index.js index 1d5552fda4..89ab68053d 100644 --- a/packages/core/admin/admin/src/pages/MarketplacePage/index.js +++ b/packages/core/admin/admin/src/pages/MarketplacePage/index.js @@ -10,6 +10,7 @@ import { LoadingIndicatorPage, useNotification, useAppInfos, + useQueryParams, } from '@strapi/helper-plugin'; import { Layout, ContentLayout } from '@strapi/design-system/Layout'; import { Main } from '@strapi/design-system/Main'; @@ -29,6 +30,7 @@ import offlineCloud from '../../assets/images/icon_offline-cloud.svg'; import useNavigatorOnLine from '../../hooks/useNavigatorOnLine'; import MissingPluginBanner from './components/MissingPluginBanner'; import NpmPackagesGrid from './components/NpmPackagesGrid'; +import NpmPackagesFilters from './components/NpmPackagesFilters'; const matchSearch = (npmPackages, search) => { return matchSorter(npmPackages, search, { @@ -49,9 +51,10 @@ const MarketPlacePage = () => { const trackUsageRef = useRef(trackUsage); const toggleNotification = useNotification(); const [searchQuery, setSearchQuery] = useState(''); - const [npmPackageType, setNpmPackageType] = useState('plugin'); const { autoReload: isInDevelopmentMode, dependencies, useYarn } = useAppInfos(); const isOnline = useNavigatorOnLine(); + const [{ query }, setQuery] = useQueryParams(); + const npmPackageType = query?.npmPackageType || 'plugin'; useFocusWhenNavigate(); @@ -170,12 +173,23 @@ const MarketPlacePage = () => { const handleTabChange = (selected) => { const packageType = selected === 0 ? 'plugin' : 'provider'; - setNpmPackageType(packageType); + setQuery({ + // Save new tab in the query params + npmPackageType: packageType, + // Clear filters + collections: [], + categories: [], + }); }; // Check if plugins and providers are installed already const installedPackageNames = Object.keys(dependencies); + const possibleCollections = + npmPackageType === 'plugin' + ? marketplacePluginsResponse.meta.collections + : marketplaceProvidersResponse.meta.collections; + return (
@@ -215,6 +229,7 @@ const MarketPlacePage = () => { })} id="tabs" variant="simple" + initialSelectedTabIndex={['plugin', 'provider'].indexOf(npmPackageType)} onTabChange={handleTabChange} > @@ -235,6 +250,16 @@ const MarketPlacePage = () => { + + + + {/* Plugins panel */} diff --git a/packages/core/admin/admin/src/pages/MarketplacePage/tests/__snapshots__/index.test.js.snap b/packages/core/admin/admin/src/pages/MarketplacePage/tests/__snapshots__/index.test.js.snap index 6f3179d01f..6ff7022def 100644 --- a/packages/core/admin/admin/src/pages/MarketplacePage/tests/__snapshots__/index.test.js.snap +++ b/packages/core/admin/admin/src/pages/MarketplacePage/tests/__snapshots__/index.test.js.snap @@ -10,46 +10,51 @@ exports[`Marketplace page renders and matches the plugin tab snapshot 1`] = ` padding-bottom: 16px; } -.c40 { +.c38 { + padding-top: 4px; + padding-bottom: 4px; +} + +.c50 { border-radius: 4px; width: 64px; height: 64px; } -.c41 { +.c51 { padding-top: 16px; } -.c44 { +.c54 { padding-top: 8px; } -.c60 { +.c64 { margin-left: 4px; width: 24px; height: auto; } -.c61 { +.c65 { padding-left: 16px; } -.c64 { +.c68 { padding-top: 32px; } -.c57 { +.c45 { font-weight: 600; color: #32324d; font-size: 0.75rem; line-height: 1.33; } -.c54 { +.c42 { padding-right: 8px; } -.c51 { +.c39 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -63,21 +68,21 @@ exports[`Marketplace page renders and matches the plugin tab snapshot 1`] = ` outline: none; } -.c51 svg { +.c39 svg { height: 12px; width: 12px; } -.c51 svg > g, -.c51 svg path { +.c39 svg > g, +.c39 svg path { fill: #ffffff; } -.c51[aria-disabled='true'] { +.c39[aria-disabled='true'] { pointer-events: none; } -.c51:after { +.c39:after { -webkit-transition-property: all; transition-property: all; -webkit-transition-duration: 0.2s; @@ -92,11 +97,11 @@ exports[`Marketplace page renders and matches the plugin tab snapshot 1`] = ` border: 2px solid transparent; } -.c51:focus-visible { +.c39:focus-visible { outline: none; } -.c51:focus-visible:after { +.c39:focus-visible:after { border-radius: 8px; content: ''; position: absolute; @@ -107,11 +112,83 @@ exports[`Marketplace page renders and matches the plugin tab snapshot 1`] = ` border: 2px solid #4945ff; } -.c55 { +.c43 { height: 100%; } -.c52 { +.c40 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + padding: 8px 16px; + background: #4945ff; + border: 1px solid #4945ff; + border: 1px solid #dcdce4; + background: #ffffff; +} + +.c40 .c41 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; +} + +.c40 .c44 { + color: #ffffff; +} + +.c40[aria-disabled='true'] { + border: 1px solid #dcdce4; + background: #eaeaef; +} + +.c40[aria-disabled='true'] .c44 { + color: #666687; +} + +.c40[aria-disabled='true'] svg > g, +.c40[aria-disabled='true'] svg path { + fill: #666687; +} + +.c40[aria-disabled='true']:active { + border: 1px solid #dcdce4; + background: #eaeaef; +} + +.c40[aria-disabled='true']:active .c44 { + color: #666687; +} + +.c40[aria-disabled='true']:active svg > g, +.c40[aria-disabled='true']:active svg path { + fill: #666687; +} + +.c40:hover { + background-color: #f6f6f9; +} + +.c40:active { + background-color: #eaeaef; +} + +.c40 .c44 { + color: #32324d; +} + +.c40 svg > g, +.c40 svg path { + fill: #32324d; +} + +.c61 { -webkit-align-items: center; -webkit-box-align: center; -ms-flex-align: center; @@ -123,7 +200,7 @@ exports[`Marketplace page renders and matches the plugin tab snapshot 1`] = ` background: #f0f0ff; } -.c52 .c53 { +.c61 .c41 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -134,66 +211,70 @@ exports[`Marketplace page renders and matches the plugin tab snapshot 1`] = ` align-items: center; } -.c52 .c56 { +.c61 .c44 { color: #ffffff; } -.c52[aria-disabled='true'] { +.c61[aria-disabled='true'] { border: 1px solid #dcdce4; background: #eaeaef; } -.c52[aria-disabled='true'] .c56 { +.c61[aria-disabled='true'] .c44 { color: #666687; } -.c52[aria-disabled='true'] svg > g, -.c52[aria-disabled='true'] svg path { +.c61[aria-disabled='true'] svg > g, +.c61[aria-disabled='true'] svg path { fill: #666687; } -.c52[aria-disabled='true']:active { +.c61[aria-disabled='true']:active { border: 1px solid #dcdce4; background: #eaeaef; } -.c52[aria-disabled='true']:active .c56 { +.c61[aria-disabled='true']:active .c44 { color: #666687; } -.c52[aria-disabled='true']:active svg > g, -.c52[aria-disabled='true']:active svg path { +.c61[aria-disabled='true']:active svg > g, +.c61[aria-disabled='true']:active svg path { fill: #666687; } -.c52:hover { +.c61:hover { background-color: #ffffff; } -.c52:active { +.c61:active { background-color: #ffffff; border: 1px solid #4945ff; } -.c52:active .c56 { +.c61:active .c44 { color: #4945ff; } -.c52:active svg > g, -.c52:active svg path { +.c61:active svg > g, +.c61:active svg path { fill: #4945ff; } -.c52 .c56 { +.c61 .c44 { color: #271fe0; } -.c52 svg > g, -.c52 svg path { +.c61 svg > g, +.c61 svg path { fill: #271fe0; } -.c38 { +.c36 { + padding-bottom: 16px; +} + +.c48 { background: #ffffff; padding-top: 16px; padding-right: 16px; @@ -204,20 +285,35 @@ exports[`Marketplace page renders and matches the plugin tab snapshot 1`] = ` height: 100%; } -.c65 { +.c69 { background: #ffffff; padding: 24px; border-radius: 4px; box-shadow: 0px 1px 4px rgba(33,33,52,0.1); } -.c66 { +.c70 { background: #f6ecfc; padding: 12px; border-radius: 4px; } -.c39 { +.c37 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + gap: 8px; +} + +.c49 { -webkit-align-items: normal; -webkit-box-align: normal; -ms-flex-align: normal; @@ -235,7 +331,7 @@ exports[`Marketplace page renders and matches the plugin tab snapshot 1`] = ` justify-content: space-between; } -.c43 { +.c53 { -webkit-align-items: center; -webkit-box-align: center; -ms-flex-align: center; @@ -249,30 +345,30 @@ exports[`Marketplace page renders and matches the plugin tab snapshot 1`] = ` flex-direction: row; } -.c58 { +.c62 { color: #328048; margin-left: 8px; } -.c62 { +.c66 { color: #328048; margin-right: 8px; width: 12; height: 12; } -.c72 { +.c76 { color: #666687; margin-left: 8px; width: 12px; height: 12px; } -.c59 path { +.c63 path { fill: #328048; } -.c73 path { +.c77 path { fill: #666687; } @@ -425,11 +521,11 @@ exports[`Marketplace page renders and matches the plugin tab snapshot 1`] = ` box-shadow: #4945ff 0px 0px 0px 2px; } -.c47 { +.c57 { padding-top: 24px; } -.c48 { +.c58 { -webkit-align-items: center; -webkit-box-align: center; -ms-flex-align: center; @@ -443,7 +539,7 @@ exports[`Marketplace page renders and matches the plugin tab snapshot 1`] = ` flex-direction: row; } -.c68 { +.c72 { -webkit-align-items: stretch; -webkit-box-align: stretch; -ms-flex-align: stretch; @@ -457,41 +553,41 @@ exports[`Marketplace page renders and matches the plugin tab snapshot 1`] = ` flex-direction: column; } -.c69 > * { +.c73 > * { margin-top: 0; margin-bottom: 0; } -.c49 > * { +.c59 > * { margin-left: 0; margin-right: 0; } -.c49 > * + * { +.c59 > * + * { margin-left: 8px; } -.c42 { +.c52 { color: #32324d; font-weight: 500; font-size: 1rem; line-height: 1.25; } -.c45 { +.c55 { color: #666687; font-size: 0.875rem; line-height: 1.43; } -.c63 { +.c67 { font-weight: 600; color: #328048; font-size: 0.875rem; line-height: 1.43; } -.c70 { +.c74 { font-weight: 500; color: #32324d; font-size: 0.75rem; @@ -509,7 +605,7 @@ exports[`Marketplace page renders and matches the plugin tab snapshot 1`] = ` padding-right: 8px; } -.c50 { +.c60 { padding-left: 8px; } @@ -650,16 +746,16 @@ exports[`Marketplace page renders and matches the plugin tab snapshot 1`] = ` fill: #32324d; } -.c67 { +.c71 { margin-right: 24px; } -.c67 svg { +.c71 svg { width: 2rem; height: 2rem; } -.c71 { +.c75 { word-break: break-all; } @@ -770,18 +866,18 @@ exports[`Marketplace page renders and matches the plugin tab snapshot 1`] = ` cursor: not-allowed; } -.c36 { +.c46 { display: grid; grid-template-columns: repeat(12,1fr); gap: 16px; } -.c37 { +.c47 { grid-column: span 4; max-width: 100%; } -.c46 { +.c56 { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; @@ -789,13 +885,13 @@ exports[`Marketplace page renders and matches the plugin tab snapshot 1`] = ` } @media (max-width:68.75rem) { - .c37 { + .c47 { grid-column: span 6; } } @media (max-width:34.375rem) { - .c37 { + .c47 { grid-column: span 12; } } @@ -992,6 +1088,44 @@ exports[`Marketplace page renders and matches the plugin tab snapshot 1`] = ` +
+
+ +
+
@@ -1019,36 +1153,36 @@ exports[`Marketplace page renders and matches the plugin tab snapshot 1`] = ` > Comments logo

Comments

Powerful Strapi based comments moderation tool for you and your users

@@ -1067,7 +1201,7 @@ exports[`Marketplace page renders and matches the plugin tab snapshot 1`] = `
@@ -1136,29 +1270,29 @@ exports[`Marketplace page renders and matches the plugin tab snapshot 1`] = ` > Config Sync logo

Config Sync

Migrate your config data across environments using the CLI or Strapi admin panel.

@@ -1207,7 +1341,7 @@ exports[`Marketplace page renders and matches the plugin tab snapshot 1`] = `
@@ -1276,36 +1410,36 @@ exports[`Marketplace page renders and matches the plugin tab snapshot 1`] = ` > Content Versioning logo

Content Versioning

This plugin enables you to versioning Content Types. It allows multiple draft versions✅ Keeps history of all changes (with time travel) ✅

@@ -1324,7 +1458,7 @@ exports[`Marketplace page renders and matches the plugin tab snapshot 1`] = `
@@ -1393,30 +1527,30 @@ exports[`Marketplace page renders and matches the plugin tab snapshot 1`] = ` > Documentation logo

Documentation
Made by Strapi

Create an OpenAPI Document and visualize your API with SWAGGER UI

@@ -1456,7 +1590,7 @@ exports[`Marketplace page renders and matches the plugin tab snapshot 1`] = `
@@ -1515,36 +1649,36 @@ exports[`Marketplace page renders and matches the plugin tab snapshot 1`] = ` > Transformer logo

Transformer

A plugin for Strapi Headless CMS that provides the ability to transform the API response.

@@ -1563,7 +1697,7 @@ exports[`Marketplace page renders and matches the plugin tab snapshot 1`] = `
Documentation
Tell us what plugin you are looking for and we'll let our community plugin developers know in case they are in search for inspiration! @@ -1703,46 +1837,51 @@ exports[`Marketplace page renders and matches the provider tab snapshot 1`] = ` padding-bottom: 16px; } -.c40 { +.c38 { + padding-top: 4px; + padding-bottom: 4px; +} + +.c50 { border-radius: 4px; width: 64px; height: 64px; } -.c41 { +.c51 { padding-top: 16px; } -.c45 { +.c55 { padding-top: 8px; } -.c44 { +.c54 { margin-left: 4px; width: 24px; height: auto; } -.c59 { +.c63 { padding-left: 16px; } -.c63 { +.c67 { padding-top: 32px; } -.c58 { +.c45 { font-weight: 600; color: #32324d; font-size: 0.75rem; line-height: 1.33; } -.c55 { +.c42 { padding-right: 8px; } -.c52 { +.c39 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -1756,21 +1895,21 @@ exports[`Marketplace page renders and matches the provider tab snapshot 1`] = ` outline: none; } -.c52 svg { +.c39 svg { height: 12px; width: 12px; } -.c52 svg > g, -.c52 svg path { +.c39 svg > g, +.c39 svg path { fill: #ffffff; } -.c52[aria-disabled='true'] { +.c39[aria-disabled='true'] { pointer-events: none; } -.c52:after { +.c39:after { -webkit-transition-property: all; transition-property: all; -webkit-transition-duration: 0.2s; @@ -1785,11 +1924,11 @@ exports[`Marketplace page renders and matches the provider tab snapshot 1`] = ` border: 2px solid transparent; } -.c52:focus-visible { +.c39:focus-visible { outline: none; } -.c52:focus-visible:after { +.c39:focus-visible:after { border-radius: 8px; content: ''; position: absolute; @@ -1800,11 +1939,83 @@ exports[`Marketplace page renders and matches the provider tab snapshot 1`] = ` border: 2px solid #4945ff; } -.c56 { +.c43 { height: 100%; } -.c53 { +.c40 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + padding: 8px 16px; + background: #4945ff; + border: 1px solid #4945ff; + border: 1px solid #dcdce4; + background: #ffffff; +} + +.c40 .c41 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; +} + +.c40 .c44 { + color: #ffffff; +} + +.c40[aria-disabled='true'] { + border: 1px solid #dcdce4; + background: #eaeaef; +} + +.c40[aria-disabled='true'] .c44 { + color: #666687; +} + +.c40[aria-disabled='true'] svg > g, +.c40[aria-disabled='true'] svg path { + fill: #666687; +} + +.c40[aria-disabled='true']:active { + border: 1px solid #dcdce4; + background: #eaeaef; +} + +.c40[aria-disabled='true']:active .c44 { + color: #666687; +} + +.c40[aria-disabled='true']:active svg > g, +.c40[aria-disabled='true']:active svg path { + fill: #666687; +} + +.c40:hover { + background-color: #f6f6f9; +} + +.c40:active { + background-color: #eaeaef; +} + +.c40 .c44 { + color: #32324d; +} + +.c40 svg > g, +.c40 svg path { + fill: #32324d; +} + +.c62 { -webkit-align-items: center; -webkit-box-align: center; -ms-flex-align: center; @@ -1816,7 +2027,7 @@ exports[`Marketplace page renders and matches the provider tab snapshot 1`] = ` background: #f0f0ff; } -.c53 .c54 { +.c62 .c41 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -1827,66 +2038,70 @@ exports[`Marketplace page renders and matches the provider tab snapshot 1`] = ` align-items: center; } -.c53 .c57 { +.c62 .c44 { color: #ffffff; } -.c53[aria-disabled='true'] { +.c62[aria-disabled='true'] { border: 1px solid #dcdce4; background: #eaeaef; } -.c53[aria-disabled='true'] .c57 { +.c62[aria-disabled='true'] .c44 { color: #666687; } -.c53[aria-disabled='true'] svg > g, -.c53[aria-disabled='true'] svg path { +.c62[aria-disabled='true'] svg > g, +.c62[aria-disabled='true'] svg path { fill: #666687; } -.c53[aria-disabled='true']:active { +.c62[aria-disabled='true']:active { border: 1px solid #dcdce4; background: #eaeaef; } -.c53[aria-disabled='true']:active .c57 { +.c62[aria-disabled='true']:active .c44 { color: #666687; } -.c53[aria-disabled='true']:active svg > g, -.c53[aria-disabled='true']:active svg path { +.c62[aria-disabled='true']:active svg > g, +.c62[aria-disabled='true']:active svg path { fill: #666687; } -.c53:hover { +.c62:hover { background-color: #ffffff; } -.c53:active { +.c62:active { background-color: #ffffff; border: 1px solid #4945ff; } -.c53:active .c57 { +.c62:active .c44 { color: #4945ff; } -.c53:active svg > g, -.c53:active svg path { +.c62:active svg > g, +.c62:active svg path { fill: #4945ff; } -.c53 .c57 { +.c62 .c44 { color: #271fe0; } -.c53 svg > g, -.c53 svg path { +.c62 svg > g, +.c62 svg path { fill: #271fe0; } -.c38 { +.c36 { + padding-bottom: 16px; +} + +.c48 { background: #ffffff; padding-top: 16px; padding-right: 16px; @@ -1897,20 +2112,35 @@ exports[`Marketplace page renders and matches the provider tab snapshot 1`] = ` height: 100%; } -.c64 { +.c68 { background: #ffffff; padding: 24px; border-radius: 4px; box-shadow: 0px 1px 4px rgba(33,33,52,0.1); } -.c65 { +.c69 { background: #f6ecfc; padding: 12px; border-radius: 4px; } -.c39 { +.c37 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + gap: 8px; +} + +.c49 { -webkit-align-items: normal; -webkit-box-align: normal; -ms-flex-align: normal; @@ -1928,7 +2158,7 @@ exports[`Marketplace page renders and matches the provider tab snapshot 1`] = ` justify-content: space-between; } -.c43 { +.c53 { -webkit-align-items: center; -webkit-box-align: center; -ms-flex-align: center; @@ -1942,25 +2172,25 @@ exports[`Marketplace page renders and matches the provider tab snapshot 1`] = ` flex-direction: row; } -.c60 { +.c64 { color: #328048; margin-right: 8px; width: 12; height: 12; } -.c71 { +.c75 { color: #666687; margin-left: 8px; width: 12px; height: 12px; } -.c61 path { +.c65 path { fill: #328048; } -.c72 path { +.c76 path { fill: #666687; } @@ -2113,11 +2343,11 @@ exports[`Marketplace page renders and matches the provider tab snapshot 1`] = ` box-shadow: #4945ff 0px 0px 0px 2px; } -.c48 { +.c58 { padding-top: 24px; } -.c49 { +.c59 { -webkit-align-items: center; -webkit-box-align: center; -ms-flex-align: center; @@ -2131,7 +2361,7 @@ exports[`Marketplace page renders and matches the provider tab snapshot 1`] = ` flex-direction: row; } -.c67 { +.c71 { -webkit-align-items: stretch; -webkit-box-align: stretch; -ms-flex-align: stretch; @@ -2145,41 +2375,41 @@ exports[`Marketplace page renders and matches the provider tab snapshot 1`] = ` flex-direction: column; } -.c68 > * { +.c72 > * { margin-top: 0; margin-bottom: 0; } -.c50 > * { +.c60 > * { margin-left: 0; margin-right: 0; } -.c50 > * + * { +.c60 > * + * { margin-left: 8px; } -.c42 { +.c52 { color: #32324d; font-weight: 500; font-size: 1rem; line-height: 1.25; } -.c46 { +.c56 { color: #666687; font-size: 0.875rem; line-height: 1.43; } -.c62 { +.c66 { font-weight: 600; color: #328048; font-size: 0.875rem; line-height: 1.43; } -.c69 { +.c73 { font-weight: 500; color: #32324d; font-size: 0.75rem; @@ -2197,7 +2427,7 @@ exports[`Marketplace page renders and matches the provider tab snapshot 1`] = ` padding-right: 8px; } -.c51 { +.c61 { padding-left: 8px; } @@ -2338,16 +2568,16 @@ exports[`Marketplace page renders and matches the provider tab snapshot 1`] = ` fill: #32324d; } -.c66 { +.c70 { margin-right: 24px; } -.c66 svg { +.c70 svg { width: 2rem; height: 2rem; } -.c70 { +.c74 { word-break: break-all; } @@ -2458,18 +2688,18 @@ exports[`Marketplace page renders and matches the provider tab snapshot 1`] = ` cursor: not-allowed; } -.c36 { +.c46 { display: grid; grid-template-columns: repeat(12,1fr); gap: 16px; } -.c37 { +.c47 { grid-column: span 4; max-width: 100%; } -.c47 { +.c57 { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; @@ -2477,13 +2707,13 @@ exports[`Marketplace page renders and matches the provider tab snapshot 1`] = ` } @media (max-width:68.75rem) { - .c37 { + .c47 { grid-column: span 6; } } @media (max-width:34.375rem) { - .c37 { + .c47 { grid-column: span 12; } } @@ -2680,6 +2910,44 @@ exports[`Marketplace page renders and matches the provider tab snapshot 1`] = `
+
+
+ +
+
@@ -2707,30 +2975,30 @@ exports[`Marketplace page renders and matches the provider tab snapshot 1`] = ` > Amazon Ses logo

Amazon Ses
Made by Strapi

Amazon Ses provider for Strapi

@@ -2770,7 +3038,7 @@ exports[`Marketplace page renders and matches the provider tab snapshot 1`] = `
@@ -2839,30 +3107,30 @@ exports[`Marketplace page renders and matches the provider tab snapshot 1`] = ` > AWS S3 logo

AWS S3
Made by Strapi

AWS S3 provider for Strapi uploads

@@ -2902,7 +3170,7 @@ exports[`Marketplace page renders and matches the provider tab snapshot 1`] = `
@@ -2971,30 +3239,30 @@ exports[`Marketplace page renders and matches the provider tab snapshot 1`] = ` > Cloudinary logo

Cloudinary
Made by Strapi

Cloudinary provider for Strapi uploads

@@ -3034,7 +3302,7 @@ exports[`Marketplace page renders and matches the provider tab snapshot 1`] = `
@@ -3093,30 +3361,30 @@ exports[`Marketplace page renders and matches the provider tab snapshot 1`] = ` > Local Upload logo

Local Upload
Made by Strapi

Local upload provider for Strapi

@@ -3156,7 +3424,7 @@ exports[`Marketplace page renders and matches the provider tab snapshot 1`] = `
@@ -3225,30 +3493,30 @@ exports[`Marketplace page renders and matches the provider tab snapshot 1`] = ` > Mailgun logo

Mailgun
Made by Strapi

Mailgun provider for Strapi

@@ -3288,7 +3556,7 @@ exports[`Marketplace page renders and matches the provider tab snapshot 1`] = `
@@ -3357,30 +3625,30 @@ exports[`Marketplace page renders and matches the provider tab snapshot 1`] = ` > Nodemailer logo

Nodemailer
Made by Strapi

Nodemailer provider for Strapi

@@ -3420,7 +3688,7 @@ exports[`Marketplace page renders and matches the provider tab snapshot 1`] = `
@@ -3489,30 +3757,30 @@ exports[`Marketplace page renders and matches the provider tab snapshot 1`] = ` > Rackspace logo

Rackspace
Made by Strapi

Rackspace provider for Strapi uploads

@@ -3552,7 +3820,7 @@ exports[`Marketplace page renders and matches the provider tab snapshot 1`] = `
@@ -3621,30 +3889,30 @@ exports[`Marketplace page renders and matches the provider tab snapshot 1`] = ` > SendGrid logo

SendGrid
Made by Strapi

SendGrid provider for Strapi

@@ -3684,7 +3952,7 @@ exports[`Marketplace page renders and matches the provider tab snapshot 1`] = `
@@ -3753,30 +4021,30 @@ exports[`Marketplace page renders and matches the provider tab snapshot 1`] = ` > Sendmail logo

Sendmail
Made by Strapi

Sendmail provider for Strapi

@@ -3816,7 +4084,7 @@ exports[`Marketplace page renders and matches the provider tab snapshot 1`] = `
Documentation
Tell us what plugin you are looking for and we'll let our community plugin developers know in case they are in search for inspiration! diff --git a/packages/core/admin/admin/src/pages/MarketplacePage/tests/index.test.js b/packages/core/admin/admin/src/pages/MarketplacePage/tests/index.test.js index 99413b5ba5..8e2bba5753 100644 --- a/packages/core/admin/admin/src/pages/MarketplacePage/tests/index.test.js +++ b/packages/core/admin/admin/src/pages/MarketplacePage/tests/index.test.js @@ -11,6 +11,8 @@ import { } from '@testing-library/react'; import { IntlProvider } from 'react-intl'; import { QueryClient, QueryClientProvider } from 'react-query'; +import { Router } from 'react-router-dom'; +import { createMemoryHistory } from 'history'; import { ThemeProvider, lightTheme } from '@strapi/design-system'; import { useTracking, useAppInfos } from '@strapi/helper-plugin'; import useNavigatorOnLine from '../../../hooks/useNavigatorOnLine'; @@ -47,11 +49,15 @@ const client = new QueryClient({ }, }); +const history = createMemoryHistory(); + const App = ( - + + + @@ -65,19 +71,47 @@ describe('Marketplace page', () => { afterAll(() => server.close()); it('renders and matches the plugin tab snapshot', async () => { + // Check snapshot const { container, getByTestId, getByRole } = render(App); await waitForElementToBeRemoved(() => getByTestId('loader')); await waitFor(() => expect(getByRole('heading', { name: /marketplace/i })).toBeInTheDocument()); expect(container.firstChild).toMatchSnapshot(); + + // Make sure it defaults to the plugins tab + const button = screen.getByRole('tab', { selected: true }); + const pluginsTabActive = getByText(button, /plugins/i); + + const tabPanel = screen.getByRole('tabpanel'); + const pluginCardText = getByText(tabPanel, 'Comments'); + const providerCardText = queryByText(tabPanel, 'Cloudinary'); + const submitPluginText = queryByText(container, 'Submit plugin'); + + expect(pluginsTabActive).not.toBe(null); + expect(pluginCardText).toBeVisible(); + expect(submitPluginText).toBeVisible(); + expect(providerCardText).toEqual(null); }); - it('renders and matches the provider tab snapshot', async () => { + it('renders and matches the provider tab snapshot', () => { + // Make sure it switches to the providers tab const { container, getByRole } = render(App); - await waitFor(() => expect(getByRole('heading', { name: /marketplace/i })).toBeInTheDocument()); - const providersTab = screen.getByRole('tab', { selected: false }); + const providersTab = getByRole('tab', { name: /providers/i }); fireEvent.click(providersTab); + const button = getByRole('tab', { selected: true }); + const providersTabActive = getByText(button, /Providers/i); + const tabPanel = getByRole('tabpanel'); + const providerCardText = getByText(tabPanel, 'Cloudinary'); + const pluginCardText = queryByText(tabPanel, 'Comments'); + const submitProviderText = queryByText(container, 'Submit provider'); + + expect(providersTabActive).not.toBe(null); + expect(providerCardText).toBeVisible(); + expect(submitProviderText).toBeVisible(); + expect(pluginCardText).toEqual(null); + + // Check snapshot expect(container.firstChild).toMatchSnapshot(); }); @@ -90,9 +124,12 @@ describe('Marketplace page', () => { expect(trackUsage).toHaveBeenCalledTimes(1); }); - it('should return plugin search results matching the query', async () => { + it('should return plugin search results matching the query', () => { const { container } = render(App); - const input = await getByPlaceholderText(container, 'Search'); + const pluginsTab = screen.getByRole('tab', { name: /plugins/i }); + fireEvent.click(pluginsTab); + + const input = getByPlaceholderText(container, 'Search'); fireEvent.change(input, { target: { value: 'comment' } }); const match = screen.getByText('Comments'); const notMatch = screen.queryByText('Sentry'); @@ -103,12 +140,12 @@ describe('Marketplace page', () => { expect(provider).toEqual(null); }); - it('should return provider search results matching the query', async () => { + it('should return provider search results matching the query', () => { const { container } = render(App); - const providersTab = screen.getByRole('tab', { selected: false }); + const providersTab = screen.getByRole('tab', { name: /providers/i }); fireEvent.click(providersTab); - const input = await getByPlaceholderText(container, 'Search'); + const input = getByPlaceholderText(container, 'Search'); fireEvent.change(input, { target: { value: 'cloudina' } }); const match = screen.getByText('Cloudinary'); const notMatch = screen.queryByText('Mailgun'); @@ -119,9 +156,9 @@ describe('Marketplace page', () => { expect(plugin).toEqual(null); }); - it('should return empty plugin search results given a bad query', async () => { + it('should return empty plugin search results given a bad query', () => { const { container } = render(App); - const input = await getByPlaceholderText(container, 'Search'); + const input = getByPlaceholderText(container, 'Search'); const badQuery = 'asdf'; fireEvent.change(input, { target: { value: badQuery } }); const noResult = screen.getByText(`No result for "${badQuery}"`); @@ -129,11 +166,11 @@ describe('Marketplace page', () => { expect(noResult).toBeVisible(); }); - it('should return empty provider search results given a bad query', async () => { + it('should return empty provider search results given a bad query', () => { const { container } = render(App); - const providersTab = screen.getByRole('tab', { selected: false }); + const providersTab = screen.getByRole('tab', { name: /providers/i }); fireEvent.click(providersTab); - const input = await getByPlaceholderText(container, 'Search'); + const input = getByPlaceholderText(container, 'Search'); const badQuery = 'asdf'; fireEvent.change(input, { target: { value: badQuery } }); const noResult = screen.getByText(`No result for "${badQuery}"`); @@ -141,6 +178,25 @@ describe('Marketplace page', () => { expect(noResult).toBeVisible(); }); + it('shows filters popover on plugins and providers', () => { + render(App); + + // Show collections and categories filters on plugins + const pluginsTab = screen.getByRole('tab', { name: /plugins/i }); + fireEvent.click(pluginsTab); + const filtersButton = screen.getByRole('button', { name: /filters/i }); + fireEvent.click(filtersButton); + screen.getByLabelText(/no collections selected/i); + screen.getByLabelText(/no categories selected/i); + fireEvent.click(filtersButton); + + // Only show collections filters on providers + const providersTab = screen.getByRole('tab', { name: /providers/i }); + fireEvent.click(providersTab); + fireEvent.click(filtersButton); + screen.getByLabelText(/no collections selected/i); + }); + it('handles production environment', () => { // Simulate production environment useAppInfos.mockImplementationOnce(() => ({ @@ -181,41 +237,7 @@ describe('Marketplace page', () => { expect(offlineText).toBeVisible(); }); - it('defaults to plugins tab', async () => { - const { container } = render(App); - const button = screen.getByRole('tab', { selected: true }); - const pluginsTabActive = await getByText(button, /Plugins/i); - - const tabPanel = screen.getByRole('tabpanel'); - const pluginCardText = await getByText(tabPanel, 'Comments'); - const providerCardText = await queryByText(tabPanel, 'Cloudinary'); - const submitPluginText = await queryByText(container, 'Submit plugin'); - - expect(pluginsTabActive).not.toBe(null); - expect(pluginCardText).toBeVisible(); - expect(submitPluginText).toBeVisible(); - expect(providerCardText).toEqual(null); - }); - - it('switches to providers tab', async () => { - const { container } = render(App); - const providersTab = screen.getByRole('tab', { selected: false }); - fireEvent.click(providersTab); - const button = screen.getByRole('tab', { selected: true }); - const providersTabActive = await getByText(button, /Providers/i); - - const tabPanel = screen.getByRole('tabpanel'); - const providerCardText = await getByText(tabPanel, 'Cloudinary'); - const pluginCardText = await queryByText(tabPanel, 'Comments'); - const submitProviderText = await queryByText(container, 'Submit provider'); - - expect(providersTabActive).not.toBe(null); - expect(providerCardText).toBeVisible(); - expect(submitProviderText).toBeVisible(); - expect(pluginCardText).toEqual(null); - }); - - it('shows the installed text for installed plugins', async () => { + it('shows the installed text for installed plugins', () => { render(App); const pluginsTab = screen.getByRole('tab', { name: /plugins/i }); fireEvent.click(pluginsTab); @@ -235,7 +257,7 @@ describe('Marketplace page', () => { expect(notInstalledText).toBeVisible(); }); - it('shows the installed text for installed providers', async () => { + it('shows the installed text for installed providers', () => { // Open providers tab render(App); const providersTab = screen.getByRole('tab', { name: /providers/i }); diff --git a/packages/core/admin/admin/src/pages/MarketplacePage/tests/server.js b/packages/core/admin/admin/src/pages/MarketplacePage/tests/server.js index d5f59d1eec..b3bc187a2a 100644 --- a/packages/core/admin/admin/src/pages/MarketplacePage/tests/server.js +++ b/packages/core/admin/admin/src/pages/MarketplacePage/tests/server.js @@ -491,6 +491,19 @@ const handlers = [ }, }, ], + meta: { + collections: { + 'Made by official partners': 9, + 'Made by Strapi': 13, + 'Made by the community': 69, + Verified: 29, + }, + categories: { + 'Custom fields': 4, + Deployment: 2, + Monitoring: 1, + }, + }, }) ); }), @@ -903,6 +916,14 @@ const handlers = [ }, }, ], + meta: { + collections: { + 'Made by official partners': 0, + 'Made by Strapi': 6, + 'Made by the community': 2, + Verified: 6, + }, + }, }) ); }), diff --git a/packages/core/admin/admin/src/translations/en.json b/packages/core/admin/admin/src/translations/en.json index 52320dfbe3..e0c7f58b08 100644 --- a/packages/core/admin/admin/src/translations/en.json +++ b/packages/core/admin/admin/src/translations/en.json @@ -100,11 +100,11 @@ "Settings.apiTokens.duration.30-days": "30 days", "Settings.apiTokens.duration.90-days": "90 days", "Settings.apiTokens.duration.unlimited": "Unlimited", - "Settings.apiTokens.form.duration":"Token duration", - "Settings.apiTokens.form.type":"Token type", - "Settings.apiTokens.duration.expiration-date":"Expiration date", - "Settings.apiTokens.createPage.permissions.title":"Permissions", - "Settings.apiTokens.createPage.permissions.description":"Only actions bound by a route are listed below.", + "Settings.apiTokens.form.duration": "Token duration", + "Settings.apiTokens.form.type": "Token type", + "Settings.apiTokens.duration.expiration-date": "Expiration date", + "Settings.apiTokens.createPage.permissions.title": "Permissions", + "Settings.apiTokens.createPage.permissions.description": "Only actions bound by a route are listed below.", "Settings.apiTokens.RegenerateDialog.title": "Regenerate token", "Settings.apiTokens.popUpWarning.message": "Are you sure you want to regenerate this token?", "Settings.apiTokens.Button.cancel": "Cancel", @@ -280,6 +280,10 @@ "admin.pages.MarketPlacePage.tab-group.label": "Plugins and Providers for Strapi", "admin.pages.MarketPlacePage.missingPlugin.title": "Missing a plugin?", "admin.pages.MarketPlacePage.missingPlugin.description": "Tell us what plugin you are looking for and we'll let our community plugin developers know in case they are in search for inspiration!", + "admin.pages.MarketPlacePage.filters.collections": "Collections", + "admin.pages.MarketPlacePage.filters.collectionsSelected": "{count, plural, =0 {No collections} one {# collection} other {# collections}} selected", + "admin.pages.MarketPlacePage.filters.categories": "Categories", + "admin.pages.MarketPlacePage.filters.categoriesSelected": "{count, plural, =0 {No categories} one {# category} other {# categories}} selected", "anErrorOccurred": "Woops! Something went wrong. Please, try again.", "app.component.CopyToClipboard.label": "Copy to clipboard", "app.component.search.label": "Search for {target}",