mirror of
https://github.com/strapi/strapi.git
synced 2025-08-13 11:17:42 +00:00
Merge pull request #14589 from strapi/market-sort-filters/filters
Add filters popover on marketplace
This commit is contained in:
commit
cb41fde5f1
@ -1,11 +1,11 @@
|
|||||||
import { useQuery } from 'react-query';
|
import { useQuery } from 'react-query';
|
||||||
import { useNotification } from '@strapi/helper-plugin';
|
import { useNotification } from '@strapi/helper-plugin';
|
||||||
import { fetchMarketplacePlugins } from './utils/api';
|
import { fetchMarketplaceProviders } from './utils/api';
|
||||||
|
|
||||||
const useFetchMarketplaceProviders = (notifyLoad) => {
|
const useFetchMarketplaceProviders = (notifyLoad) => {
|
||||||
const toggleNotification = useNotification();
|
const toggleNotification = useNotification();
|
||||||
|
|
||||||
return useQuery('list-marketplace-providers', () => fetchMarketplacePlugins(), {
|
return useQuery('list-marketplace-providers', () => fetchMarketplaceProviders(), {
|
||||||
onSuccess() {
|
onSuccess() {
|
||||||
if (notifyLoad) {
|
if (notifyLoad) {
|
||||||
notifyLoad();
|
notifyLoad();
|
||||||
|
@ -2,10 +2,10 @@ import axios from 'axios';
|
|||||||
|
|
||||||
const MARKETPLACE_API_URL = 'https://market-api.strapi.io';
|
const MARKETPLACE_API_URL = 'https://market-api.strapi.io';
|
||||||
|
|
||||||
const fetchMarketplacePlugins = async () => {
|
const fetchMarketplaceProviders = async () => {
|
||||||
const { data } = await axios.get(`${MARKETPLACE_API_URL}/providers`);
|
const { data } = await axios.get(`${MARKETPLACE_API_URL}/providers`);
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export { fetchMarketplacePlugins };
|
export { fetchMarketplaceProviders };
|
||||||
|
@ -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 (
|
||||||
|
<Select
|
||||||
|
aria-label={message}
|
||||||
|
placeholder={message}
|
||||||
|
size="M"
|
||||||
|
onChange={onChange}
|
||||||
|
onClear={onClear}
|
||||||
|
value={value}
|
||||||
|
customizeContent={customizeContent}
|
||||||
|
multi
|
||||||
|
>
|
||||||
|
{Object.entries(possibleFilters).map(([filterName, count]) => {
|
||||||
|
return (
|
||||||
|
<Option key={filterName} value={filterName}>
|
||||||
|
{filterName} ({count})
|
||||||
|
</Option>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Select>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
@ -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 (
|
||||||
|
<Popover source={source} padding={3} spacing={4} onBlur={() => {}}>
|
||||||
|
<FocusTrap onEscape={onToggle}>
|
||||||
|
<Stack spacing={1}>
|
||||||
|
<Box>
|
||||||
|
<FilterSelect
|
||||||
|
message={formatMessage({
|
||||||
|
id: 'admin.pages.MarketPlacePage.filters.collections',
|
||||||
|
defaultMessage: 'Collections',
|
||||||
|
})}
|
||||||
|
value={query?.collections || []}
|
||||||
|
onChange={(newCollections) => 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 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
{npmPackageType === 'plugin' && (
|
||||||
|
<Box>
|
||||||
|
<FilterSelect
|
||||||
|
message={formatMessage({
|
||||||
|
id: 'admin.pages.MarketPlacePage.filters.categories',
|
||||||
|
defaultMessage: 'Categories',
|
||||||
|
})}
|
||||||
|
value={query?.categories || []}
|
||||||
|
onChange={(newCategories) => 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"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</FocusTrap>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
@ -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 (
|
||||||
|
<Box padding={1}>
|
||||||
|
<Tag icon={<Cross />} onClick={handleRemove}>
|
||||||
|
{name}
|
||||||
|
</Tag>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const NpmPackagesFilters = ({
|
||||||
|
possibleCollections,
|
||||||
|
possibleCategories,
|
||||||
|
npmPackageType,
|
||||||
|
query,
|
||||||
|
setQuery,
|
||||||
|
}) => {
|
||||||
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
|
const buttonRef = useRef();
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
|
const handleToggle = () => setIsVisible((prev) => !prev);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Box paddingTop={1} paddingBottom={1}>
|
||||||
|
<Button
|
||||||
|
variant="tertiary"
|
||||||
|
ref={buttonRef}
|
||||||
|
startIcon={<Filter />}
|
||||||
|
onClick={handleToggle}
|
||||||
|
size="S"
|
||||||
|
>
|
||||||
|
{formatMessage({ id: 'app.utils.filters', defaultMessage: 'Filters' })}
|
||||||
|
</Button>
|
||||||
|
{isVisible && (
|
||||||
|
<FiltersPopover
|
||||||
|
onToggle={handleToggle}
|
||||||
|
source={buttonRef}
|
||||||
|
query={query}
|
||||||
|
setQuery={setQuery}
|
||||||
|
possibleCollections={possibleCollections}
|
||||||
|
possibleCategories={possibleCategories}
|
||||||
|
npmPackageType={npmPackageType}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
{query.collections?.map((collection) => (
|
||||||
|
<FilterTag
|
||||||
|
name={collection}
|
||||||
|
key={collection}
|
||||||
|
handleRemove={() =>
|
||||||
|
setQuery({
|
||||||
|
collections: query.collections.filter(
|
||||||
|
(previousCollection) => previousCollection !== collection
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{npmPackageType === 'plugin' &&
|
||||||
|
query.categories?.map((category) => (
|
||||||
|
<FilterTag
|
||||||
|
name={category}
|
||||||
|
key={category}
|
||||||
|
handleRemove={() =>
|
||||||
|
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;
|
@ -10,6 +10,7 @@ import {
|
|||||||
LoadingIndicatorPage,
|
LoadingIndicatorPage,
|
||||||
useNotification,
|
useNotification,
|
||||||
useAppInfos,
|
useAppInfos,
|
||||||
|
useQueryParams,
|
||||||
} from '@strapi/helper-plugin';
|
} from '@strapi/helper-plugin';
|
||||||
import { Layout, ContentLayout } from '@strapi/design-system/Layout';
|
import { Layout, ContentLayout } from '@strapi/design-system/Layout';
|
||||||
import { Main } from '@strapi/design-system/Main';
|
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 useNavigatorOnLine from '../../hooks/useNavigatorOnLine';
|
||||||
import MissingPluginBanner from './components/MissingPluginBanner';
|
import MissingPluginBanner from './components/MissingPluginBanner';
|
||||||
import NpmPackagesGrid from './components/NpmPackagesGrid';
|
import NpmPackagesGrid from './components/NpmPackagesGrid';
|
||||||
|
import NpmPackagesFilters from './components/NpmPackagesFilters';
|
||||||
|
|
||||||
const matchSearch = (npmPackages, search) => {
|
const matchSearch = (npmPackages, search) => {
|
||||||
return matchSorter(npmPackages, search, {
|
return matchSorter(npmPackages, search, {
|
||||||
@ -49,9 +51,10 @@ const MarketPlacePage = () => {
|
|||||||
const trackUsageRef = useRef(trackUsage);
|
const trackUsageRef = useRef(trackUsage);
|
||||||
const toggleNotification = useNotification();
|
const toggleNotification = useNotification();
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [npmPackageType, setNpmPackageType] = useState('plugin');
|
|
||||||
const { autoReload: isInDevelopmentMode, dependencies, useYarn } = useAppInfos();
|
const { autoReload: isInDevelopmentMode, dependencies, useYarn } = useAppInfos();
|
||||||
const isOnline = useNavigatorOnLine();
|
const isOnline = useNavigatorOnLine();
|
||||||
|
const [{ query }, setQuery] = useQueryParams();
|
||||||
|
const npmPackageType = query?.npmPackageType || 'plugin';
|
||||||
|
|
||||||
useFocusWhenNavigate();
|
useFocusWhenNavigate();
|
||||||
|
|
||||||
@ -170,12 +173,23 @@ const MarketPlacePage = () => {
|
|||||||
|
|
||||||
const handleTabChange = (selected) => {
|
const handleTabChange = (selected) => {
|
||||||
const packageType = selected === 0 ? 'plugin' : 'provider';
|
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
|
// Check if plugins and providers are installed already
|
||||||
const installedPackageNames = Object.keys(dependencies);
|
const installedPackageNames = Object.keys(dependencies);
|
||||||
|
|
||||||
|
const possibleCollections =
|
||||||
|
npmPackageType === 'plugin'
|
||||||
|
? marketplacePluginsResponse.meta.collections
|
||||||
|
: marketplaceProvidersResponse.meta.collections;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<Main>
|
<Main>
|
||||||
@ -215,6 +229,7 @@ const MarketPlacePage = () => {
|
|||||||
})}
|
})}
|
||||||
id="tabs"
|
id="tabs"
|
||||||
variant="simple"
|
variant="simple"
|
||||||
|
initialSelectedTabIndex={['plugin', 'provider'].indexOf(npmPackageType)}
|
||||||
onTabChange={handleTabChange}
|
onTabChange={handleTabChange}
|
||||||
>
|
>
|
||||||
<Box paddingBottom={4}>
|
<Box paddingBottom={4}>
|
||||||
@ -235,6 +250,16 @@ const MarketPlacePage = () => {
|
|||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Box>
|
</Box>
|
||||||
|
<Flex paddingBottom={4} gap={2}>
|
||||||
|
<NpmPackagesFilters
|
||||||
|
npmPackageType={npmPackageType}
|
||||||
|
possibleCollections={possibleCollections}
|
||||||
|
possibleCategories={marketplacePluginsResponse.meta.categories}
|
||||||
|
query={query || {}}
|
||||||
|
setQuery={setQuery}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
<TabPanels>
|
<TabPanels>
|
||||||
{/* Plugins panel */}
|
{/* Plugins panel */}
|
||||||
<TabPanel>
|
<TabPanel>
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -11,6 +11,8 @@ import {
|
|||||||
} from '@testing-library/react';
|
} from '@testing-library/react';
|
||||||
import { IntlProvider } from 'react-intl';
|
import { IntlProvider } from 'react-intl';
|
||||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||||
|
import { Router } from 'react-router-dom';
|
||||||
|
import { createMemoryHistory } from 'history';
|
||||||
import { ThemeProvider, lightTheme } from '@strapi/design-system';
|
import { ThemeProvider, lightTheme } from '@strapi/design-system';
|
||||||
import { useTracking, useAppInfos } from '@strapi/helper-plugin';
|
import { useTracking, useAppInfos } from '@strapi/helper-plugin';
|
||||||
import useNavigatorOnLine from '../../../hooks/useNavigatorOnLine';
|
import useNavigatorOnLine from '../../../hooks/useNavigatorOnLine';
|
||||||
@ -47,11 +49,15 @@ const client = new QueryClient({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const history = createMemoryHistory();
|
||||||
|
|
||||||
const App = (
|
const App = (
|
||||||
<QueryClientProvider client={client}>
|
<QueryClientProvider client={client}>
|
||||||
<IntlProvider locale="en" messages={{}} textComponent="span">
|
<IntlProvider locale="en" messages={{}} textComponent="span">
|
||||||
<ThemeProvider theme={lightTheme}>
|
<ThemeProvider theme={lightTheme}>
|
||||||
|
<Router history={history}>
|
||||||
<MarketPlacePage />
|
<MarketPlacePage />
|
||||||
|
</Router>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</IntlProvider>
|
</IntlProvider>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
@ -65,19 +71,47 @@ describe('Marketplace page', () => {
|
|||||||
afterAll(() => server.close());
|
afterAll(() => server.close());
|
||||||
|
|
||||||
it('renders and matches the plugin tab snapshot', async () => {
|
it('renders and matches the plugin tab snapshot', async () => {
|
||||||
|
// Check snapshot
|
||||||
const { container, getByTestId, getByRole } = render(App);
|
const { container, getByTestId, getByRole } = render(App);
|
||||||
await waitForElementToBeRemoved(() => getByTestId('loader'));
|
await waitForElementToBeRemoved(() => getByTestId('loader'));
|
||||||
await waitFor(() => expect(getByRole('heading', { name: /marketplace/i })).toBeInTheDocument());
|
await waitFor(() => expect(getByRole('heading', { name: /marketplace/i })).toBeInTheDocument());
|
||||||
|
|
||||||
expect(container.firstChild).toMatchSnapshot();
|
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);
|
const { container, getByRole } = render(App);
|
||||||
await waitFor(() => expect(getByRole('heading', { name: /marketplace/i })).toBeInTheDocument());
|
const providersTab = getByRole('tab', { name: /providers/i });
|
||||||
const providersTab = screen.getByRole('tab', { selected: false });
|
|
||||||
fireEvent.click(providersTab);
|
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();
|
expect(container.firstChild).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -90,9 +124,12 @@ describe('Marketplace page', () => {
|
|||||||
expect(trackUsage).toHaveBeenCalledTimes(1);
|
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 { 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' } });
|
fireEvent.change(input, { target: { value: 'comment' } });
|
||||||
const match = screen.getByText('Comments');
|
const match = screen.getByText('Comments');
|
||||||
const notMatch = screen.queryByText('Sentry');
|
const notMatch = screen.queryByText('Sentry');
|
||||||
@ -103,12 +140,12 @@ describe('Marketplace page', () => {
|
|||||||
expect(provider).toEqual(null);
|
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 { container } = render(App);
|
||||||
const providersTab = screen.getByRole('tab', { selected: false });
|
const providersTab = screen.getByRole('tab', { name: /providers/i });
|
||||||
fireEvent.click(providersTab);
|
fireEvent.click(providersTab);
|
||||||
|
|
||||||
const input = await getByPlaceholderText(container, 'Search');
|
const input = getByPlaceholderText(container, 'Search');
|
||||||
fireEvent.change(input, { target: { value: 'cloudina' } });
|
fireEvent.change(input, { target: { value: 'cloudina' } });
|
||||||
const match = screen.getByText('Cloudinary');
|
const match = screen.getByText('Cloudinary');
|
||||||
const notMatch = screen.queryByText('Mailgun');
|
const notMatch = screen.queryByText('Mailgun');
|
||||||
@ -119,9 +156,9 @@ describe('Marketplace page', () => {
|
|||||||
expect(plugin).toEqual(null);
|
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 { container } = render(App);
|
||||||
const input = await getByPlaceholderText(container, 'Search');
|
const input = getByPlaceholderText(container, 'Search');
|
||||||
const badQuery = 'asdf';
|
const badQuery = 'asdf';
|
||||||
fireEvent.change(input, { target: { value: badQuery } });
|
fireEvent.change(input, { target: { value: badQuery } });
|
||||||
const noResult = screen.getByText(`No result for "${badQuery}"`);
|
const noResult = screen.getByText(`No result for "${badQuery}"`);
|
||||||
@ -129,11 +166,11 @@ describe('Marketplace page', () => {
|
|||||||
expect(noResult).toBeVisible();
|
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 { container } = render(App);
|
||||||
const providersTab = screen.getByRole('tab', { selected: false });
|
const providersTab = screen.getByRole('tab', { name: /providers/i });
|
||||||
fireEvent.click(providersTab);
|
fireEvent.click(providersTab);
|
||||||
const input = await getByPlaceholderText(container, 'Search');
|
const input = getByPlaceholderText(container, 'Search');
|
||||||
const badQuery = 'asdf';
|
const badQuery = 'asdf';
|
||||||
fireEvent.change(input, { target: { value: badQuery } });
|
fireEvent.change(input, { target: { value: badQuery } });
|
||||||
const noResult = screen.getByText(`No result for "${badQuery}"`);
|
const noResult = screen.getByText(`No result for "${badQuery}"`);
|
||||||
@ -141,6 +178,25 @@ describe('Marketplace page', () => {
|
|||||||
expect(noResult).toBeVisible();
|
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', () => {
|
it('handles production environment', () => {
|
||||||
// Simulate production environment
|
// Simulate production environment
|
||||||
useAppInfos.mockImplementationOnce(() => ({
|
useAppInfos.mockImplementationOnce(() => ({
|
||||||
@ -181,41 +237,7 @@ describe('Marketplace page', () => {
|
|||||||
expect(offlineText).toBeVisible();
|
expect(offlineText).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('defaults to plugins tab', async () => {
|
it('shows the installed text for installed plugins', () => {
|
||||||
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 () => {
|
|
||||||
render(App);
|
render(App);
|
||||||
const pluginsTab = screen.getByRole('tab', { name: /plugins/i });
|
const pluginsTab = screen.getByRole('tab', { name: /plugins/i });
|
||||||
fireEvent.click(pluginsTab);
|
fireEvent.click(pluginsTab);
|
||||||
@ -235,7 +257,7 @@ describe('Marketplace page', () => {
|
|||||||
expect(notInstalledText).toBeVisible();
|
expect(notInstalledText).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows the installed text for installed providers', async () => {
|
it('shows the installed text for installed providers', () => {
|
||||||
// Open providers tab
|
// Open providers tab
|
||||||
render(App);
|
render(App);
|
||||||
const providersTab = screen.getByRole('tab', { name: /providers/i });
|
const providersTab = screen.getByRole('tab', { name: /providers/i });
|
||||||
|
@ -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,
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
@ -100,11 +100,11 @@
|
|||||||
"Settings.apiTokens.duration.30-days": "30 days",
|
"Settings.apiTokens.duration.30-days": "30 days",
|
||||||
"Settings.apiTokens.duration.90-days": "90 days",
|
"Settings.apiTokens.duration.90-days": "90 days",
|
||||||
"Settings.apiTokens.duration.unlimited": "Unlimited",
|
"Settings.apiTokens.duration.unlimited": "Unlimited",
|
||||||
"Settings.apiTokens.form.duration":"Token duration",
|
"Settings.apiTokens.form.duration": "Token duration",
|
||||||
"Settings.apiTokens.form.type":"Token type",
|
"Settings.apiTokens.form.type": "Token type",
|
||||||
"Settings.apiTokens.duration.expiration-date":"Expiration date",
|
"Settings.apiTokens.duration.expiration-date": "Expiration date",
|
||||||
"Settings.apiTokens.createPage.permissions.title":"Permissions",
|
"Settings.apiTokens.createPage.permissions.title": "Permissions",
|
||||||
"Settings.apiTokens.createPage.permissions.description":"Only actions bound by a route are listed below.",
|
"Settings.apiTokens.createPage.permissions.description": "Only actions bound by a route are listed below.",
|
||||||
"Settings.apiTokens.RegenerateDialog.title": "Regenerate token",
|
"Settings.apiTokens.RegenerateDialog.title": "Regenerate token",
|
||||||
"Settings.apiTokens.popUpWarning.message": "Are you sure you want to regenerate this token?",
|
"Settings.apiTokens.popUpWarning.message": "Are you sure you want to regenerate this token?",
|
||||||
"Settings.apiTokens.Button.cancel": "Cancel",
|
"Settings.apiTokens.Button.cancel": "Cancel",
|
||||||
@ -280,6 +280,10 @@
|
|||||||
"admin.pages.MarketPlacePage.tab-group.label": "Plugins and Providers for Strapi",
|
"admin.pages.MarketPlacePage.tab-group.label": "Plugins and Providers for Strapi",
|
||||||
"admin.pages.MarketPlacePage.missingPlugin.title": "Missing a plugin?",
|
"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.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.",
|
"anErrorOccurred": "Woops! Something went wrong. Please, try again.",
|
||||||
"app.component.CopyToClipboard.label": "Copy to clipboard",
|
"app.component.CopyToClipboard.label": "Copy to clipboard",
|
||||||
"app.component.search.label": "Search for {target}",
|
"app.component.search.label": "Search for {target}",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user