mirror of
https://github.com/strapi/strapi.git
synced 2025-08-12 18:53:23 +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 { 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();
|
||||
|
@ -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 };
|
||||
|
@ -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,
|
||||
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 (
|
||||
<Layout>
|
||||
<Main>
|
||||
@ -215,6 +229,7 @@ const MarketPlacePage = () => {
|
||||
})}
|
||||
id="tabs"
|
||||
variant="simple"
|
||||
initialSelectedTabIndex={['plugin', 'provider'].indexOf(npmPackageType)}
|
||||
onTabChange={handleTabChange}
|
||||
>
|
||||
<Box paddingBottom={4}>
|
||||
@ -235,6 +250,16 @@ const MarketPlacePage = () => {
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</Box>
|
||||
<Flex paddingBottom={4} gap={2}>
|
||||
<NpmPackagesFilters
|
||||
npmPackageType={npmPackageType}
|
||||
possibleCollections={possibleCollections}
|
||||
possibleCategories={marketplacePluginsResponse.meta.categories}
|
||||
query={query || {}}
|
||||
setQuery={setQuery}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
<TabPanels>
|
||||
{/* Plugins panel */}
|
||||
<TabPanel>
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -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 = (
|
||||
<QueryClientProvider client={client}>
|
||||
<IntlProvider locale="en" messages={{}} textComponent="span">
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<MarketPlacePage />
|
||||
<Router history={history}>
|
||||
<MarketPlacePage />
|
||||
</Router>
|
||||
</ThemeProvider>
|
||||
</IntlProvider>
|
||||
</QueryClientProvider>
|
||||
@ -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 });
|
||||
|
@ -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.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}",
|
||||
|
Loading…
x
Reference in New Issue
Block a user