mirror of
https://github.com/strapi/strapi.git
synced 2025-10-30 01:17:28 +00:00
Merge pull request #13004 from strapi/features/marketplace-offline-layout
[marketplace] add offline layout
This commit is contained in:
commit
2edd2ae00c
@ -0,0 +1,5 @@
|
||||
<svg width="88" height="88" viewBox="0 0 88 88" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="0.5" y="0.5" width="87" height="87" rx="43.5" fill="#F0F0FF"/>
|
||||
<path d="M34.0469 39.2969H30C27.4479 39.2969 25.2604 40.2448 23.4375 42.1406C21.6146 43.9635 20.7031 46.151 20.7031 48.7031C20.7031 51.2552 21.6146 53.4427 23.4375 55.2656C25.2604 57.0885 27.4479 58 30 58H52.75L34.0469 39.2969ZM23 28.25L25.9531 25.2969L65 64.3438L62.0469 67.2969L57.3438 62.7031H30C26.1354 62.7031 22.8177 61.3542 20.0469 58.6562C17.349 55.8854 16 52.5677 16 48.7031C16 44.9115 17.3125 41.6667 19.9375 38.9688C22.5625 36.2708 25.7344 34.849 29.4531 34.7031L23 28.25ZM61.1719 39.4062C64.1615 39.625 66.7135 40.8646 68.8281 43.125C70.9427 45.3125 72 47.9375 72 51C72 55.0104 70.3594 58.1823 67.0781 60.5156L63.6875 57.125C66.0938 55.8125 67.2969 53.7708 67.2969 51C67.2969 49.1042 66.6042 47.4635 65.2188 46.0781C63.8333 44.6927 62.1927 44 60.2969 44H56.7969V42.7969C56.7969 39.224 55.5573 36.1979 53.0781 33.7188C50.599 31.2396 47.5729 30 44 30C41.9583 30 39.9896 30.474 38.0938 31.4219L34.5938 28.0312C37.4375 26.2083 40.5729 25.2969 44 25.2969C47.9375 25.2969 51.5833 26.6823 54.9375 29.4531C58.3646 32.224 60.4427 35.5417 61.1719 39.4062Z" fill="#4945FF"/>
|
||||
<rect x="0.5" y="0.5" width="87" height="87" rx="43.5" stroke="#D9D8FF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@ -269,7 +269,7 @@ const EditViewDataManagerProvider = ({
|
||||
);
|
||||
|
||||
const createFormData = useCallback(
|
||||
(data) => {
|
||||
data => {
|
||||
// First we need to remove the added keys needed for the dnd
|
||||
const preparedData = removeKeyInObject(cloneDeep(data), '__temp_key__');
|
||||
// Then we need to apply our helper
|
||||
@ -293,7 +293,7 @@ const EditViewDataManagerProvider = ({
|
||||
}, [hasDraftAndPublish, shouldNotRunValidations]);
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
async (e) => {
|
||||
async e => {
|
||||
e.preventDefault();
|
||||
let errors = {};
|
||||
|
||||
@ -363,8 +363,8 @@ const EditViewDataManagerProvider = ({
|
||||
}, [allLayoutData, currentContentTypeLayout, isCreatingEntry, modifiedData, onPublish]);
|
||||
|
||||
const shouldCheckDZErrors = useCallback(
|
||||
(dzName) => {
|
||||
const doesDZHaveError = Object.keys(formErrors).some((key) => key.split('.')[0] === dzName);
|
||||
dzName => {
|
||||
const doesDZHaveError = Object.keys(formErrors).some(key => key.split('.')[0] === dzName);
|
||||
const shouldCheckErrors = !isEmpty(formErrors) && doesDZHaveError;
|
||||
|
||||
return shouldCheckErrors;
|
||||
@ -418,7 +418,7 @@ const EditViewDataManagerProvider = ({
|
||||
});
|
||||
}, []);
|
||||
|
||||
const onRemoveRelation = useCallback((keys) => {
|
||||
const onRemoveRelation = useCallback(keys => {
|
||||
dispatch({
|
||||
type: 'REMOVE_RELATION',
|
||||
keys,
|
||||
|
||||
@ -10,11 +10,11 @@ import { translatedErrors as errorsTrads } from '@strapi/helper-plugin';
|
||||
import isFieldTypeNumber from '../../../utils/isFieldTypeNumber';
|
||||
|
||||
yup.addMethod(yup.mixed, 'defined', function() {
|
||||
return this.test('defined', errorsTrads.required, (value) => value !== undefined);
|
||||
return this.test('defined', errorsTrads.required, value => value !== undefined);
|
||||
});
|
||||
|
||||
yup.addMethod(yup.array, 'notEmptyMin', function(min) {
|
||||
return this.test('notEmptyMin', errorsTrads.min, (value) => {
|
||||
return this.test('notEmptyMin', errorsTrads.min, value => {
|
||||
if (isEmpty(value)) {
|
||||
return true;
|
||||
}
|
||||
@ -51,7 +51,7 @@ yup.addMethod(yup.string, 'isSuperior', function(message, min) {
|
||||
});
|
||||
});
|
||||
|
||||
const getAttributes = (data) => get(data, ['attributes'], {});
|
||||
const getAttributes = data => get(data, ['attributes'], {});
|
||||
|
||||
const createYupSchema = (
|
||||
model,
|
||||
@ -97,7 +97,7 @@ const createYupSchema = (
|
||||
if (attribute.repeatable === true) {
|
||||
const { min, max, required } = attribute;
|
||||
|
||||
let componentSchema = yup.lazy((value) => {
|
||||
let componentSchema = yup.lazy(value => {
|
||||
let baseSchema = yup.array().of(componentFieldSchema);
|
||||
|
||||
if (min) {
|
||||
@ -123,7 +123,7 @@ const createYupSchema = (
|
||||
|
||||
return acc;
|
||||
}
|
||||
const componentSchema = yup.lazy((obj) => {
|
||||
const componentSchema = yup.lazy(obj => {
|
||||
if (obj !== undefined) {
|
||||
return attribute.required === true && !options.isDraft
|
||||
? componentFieldSchema.defined()
|
||||
@ -154,7 +154,7 @@ const createYupSchema = (
|
||||
if (min) {
|
||||
if (attribute.required) {
|
||||
dynamicZoneSchema = dynamicZoneSchema
|
||||
.test('min', errorsTrads.min, (value) => {
|
||||
.test('min', errorsTrads.min, value => {
|
||||
if (options.isCreatingEntry) {
|
||||
return value && value.length >= min;
|
||||
}
|
||||
@ -165,7 +165,7 @@ const createYupSchema = (
|
||||
|
||||
return value !== null && value.length >= min;
|
||||
})
|
||||
.test('required', errorsTrads.required, (value) => {
|
||||
.test('required', errorsTrads.required, value => {
|
||||
if (options.isCreatingEntry) {
|
||||
return value !== null || value !== undefined;
|
||||
}
|
||||
@ -180,7 +180,7 @@ const createYupSchema = (
|
||||
dynamicZoneSchema = dynamicZoneSchema.notEmptyMin(min);
|
||||
}
|
||||
} else if (attribute.required && !options.isDraft) {
|
||||
dynamicZoneSchema = dynamicZoneSchema.test('required', errorsTrads.required, (value) => {
|
||||
dynamicZoneSchema = dynamicZoneSchema.test('required', errorsTrads.required, value => {
|
||||
if (options.isCreatingEntry) {
|
||||
return value !== null || value !== undefined;
|
||||
}
|
||||
@ -215,7 +215,7 @@ const createYupSchemaAttribute = (type, validations, options) => {
|
||||
if (type === 'json') {
|
||||
schema = yup
|
||||
.mixed(errorsTrads.json)
|
||||
.test('isJSON', errorsTrads.json, (value) => {
|
||||
.test('isJSON', errorsTrads.json, value => {
|
||||
if (value === undefined) {
|
||||
return true;
|
||||
}
|
||||
@ -238,7 +238,7 @@ const createYupSchemaAttribute = (type, validations, options) => {
|
||||
if (['number', 'integer', 'float', 'decimal'].includes(type)) {
|
||||
schema = yup
|
||||
.number()
|
||||
.transform((cv) => (isNaN(cv) ? undefined : cv))
|
||||
.transform(cv => (isNaN(cv) ? undefined : cv))
|
||||
.typeError();
|
||||
}
|
||||
|
||||
@ -250,7 +250,7 @@ const createYupSchemaAttribute = (type, validations, options) => {
|
||||
schema = yup.date();
|
||||
}
|
||||
|
||||
Object.keys(validations).forEach((validation) => {
|
||||
Object.keys(validations).forEach(validation => {
|
||||
const validationValue = validations[validation];
|
||||
|
||||
if (
|
||||
@ -269,7 +269,7 @@ const createYupSchemaAttribute = (type, validations, options) => {
|
||||
if (options.isCreatingEntry) {
|
||||
schema = schema.required(errorsTrads.required);
|
||||
} else {
|
||||
schema = schema.test('required', errorsTrads.required, (value) => {
|
||||
schema = schema.test('required', errorsTrads.required, value => {
|
||||
// Field is not touched and the user is editing the entry
|
||||
if (value === undefined && !options.isFromComponent) {
|
||||
return true;
|
||||
|
||||
@ -157,10 +157,10 @@ function Inputs({
|
||||
return disabled;
|
||||
}, [disabled, isCreatingEntry, isUserAllowedToEditField, isUserAllowedToReadField]);
|
||||
|
||||
const options = useMemo(
|
||||
() => generateOptions(fieldSchema.enum || [], isRequired),
|
||||
[fieldSchema, isRequired]
|
||||
);
|
||||
const options = useMemo(() => generateOptions(fieldSchema.enum || [], isRequired), [
|
||||
fieldSchema,
|
||||
isRequired,
|
||||
]);
|
||||
|
||||
const { label, description, placeholder, visible } = metadatas;
|
||||
|
||||
|
||||
@ -0,0 +1,31 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
/**
|
||||
* For more details about this hook see:
|
||||
* https://www.30secondsofcode.org/react/s/use-navigator-on-line
|
||||
*/
|
||||
const useNavigatorOnLine = () => {
|
||||
const onlineStatus =
|
||||
typeof navigator !== 'undefined' && typeof navigator.onLine === 'boolean'
|
||||
? navigator.onLine
|
||||
: true;
|
||||
|
||||
const [isOnline, setIsOnline] = useState(onlineStatus);
|
||||
|
||||
const setOnline = () => setIsOnline(true);
|
||||
const setOffline = () => setIsOnline(false);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('online', setOnline);
|
||||
window.addEventListener('offline', setOffline);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('online', setOnline);
|
||||
window.removeEventListener('offline', setOffline);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return isOnline;
|
||||
};
|
||||
|
||||
export default useNavigatorOnLine;
|
||||
@ -0,0 +1,48 @@
|
||||
import { renderHook, act } from '@testing-library/react-hooks';
|
||||
import useNavigatorOnLine from '../index';
|
||||
|
||||
describe('useNavigatorOnLine', () => {
|
||||
it('returns the online state', () => {
|
||||
jest.spyOn(window.navigator, 'onLine', 'get').mockReturnValue(true);
|
||||
const { result } = renderHook(() => useNavigatorOnLine());
|
||||
|
||||
expect(result.current).toEqual(true);
|
||||
});
|
||||
|
||||
it('returns the offline state', () => {
|
||||
jest.spyOn(window.navigator, 'onLine', 'get').mockReturnValue(false);
|
||||
const { result } = renderHook(() => useNavigatorOnLine());
|
||||
|
||||
expect(result.current).toEqual(false);
|
||||
});
|
||||
|
||||
it('listens for network change online', async () => {
|
||||
// Initialize an offline state
|
||||
jest.spyOn(window.navigator, 'onLine', 'get').mockReturnValue(false);
|
||||
const { result, waitForNextUpdate } = renderHook(() => useNavigatorOnLine());
|
||||
|
||||
await act(async () => {
|
||||
// Simulate a change from offline to online
|
||||
window.dispatchEvent(new window.Event('online'));
|
||||
|
||||
await waitForNextUpdate();
|
||||
});
|
||||
|
||||
expect(result.current).toEqual(true);
|
||||
});
|
||||
|
||||
it('listens for network change offline', async () => {
|
||||
// Initialize an online state
|
||||
jest.spyOn(window.navigator, 'onLine', 'get').mockReturnValue(true);
|
||||
const { result, waitForNextUpdate } = renderHook(() => useNavigatorOnLine());
|
||||
|
||||
await act(async () => {
|
||||
// Simulate a change from online to offline
|
||||
window.dispatchEvent(new window.Event('offline'));
|
||||
|
||||
await waitForNextUpdate();
|
||||
});
|
||||
|
||||
expect(result.current).toEqual(false);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,46 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { HeaderLayout } from '@strapi/design-system/Layout';
|
||||
import { LinkButton } from '@strapi/design-system/LinkButton';
|
||||
import Upload from '@strapi/icons/Upload';
|
||||
import { useTracking } from '@strapi/helper-plugin';
|
||||
|
||||
const PageHeader = ({ isOnline }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { trackUsage } = useTracking();
|
||||
|
||||
return (
|
||||
<HeaderLayout
|
||||
title={formatMessage({
|
||||
id: 'global.marketplace',
|
||||
defaultMessage: 'Marketplace',
|
||||
})}
|
||||
subtitle={formatMessage({
|
||||
id: 'admin.pages.MarketPlacePage.subtitle',
|
||||
defaultMessage: 'Get more out of Strapi',
|
||||
})}
|
||||
primaryAction={
|
||||
isOnline && (
|
||||
<LinkButton
|
||||
startIcon={<Upload />}
|
||||
variant="tertiary"
|
||||
href="https://market.strapi.io/submit-plugin"
|
||||
onClick={() => trackUsage('didSubmitPlugin')}
|
||||
>
|
||||
{formatMessage({
|
||||
id: 'admin.pages.MarketPlacePage.submit.plugin.link',
|
||||
defaultMessage: 'Submit your plugin',
|
||||
})}
|
||||
</LinkButton>
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default PageHeader;
|
||||
|
||||
PageHeader.propTypes = {
|
||||
isOnline: PropTypes.bool.isRequired,
|
||||
};
|
||||
@ -13,20 +13,23 @@ import {
|
||||
useAppInfos,
|
||||
} from '@strapi/helper-plugin';
|
||||
import { Grid, GridItem } from '@strapi/design-system/Grid';
|
||||
import { Layout, HeaderLayout, ContentLayout } from '@strapi/design-system/Layout';
|
||||
import { Layout, ContentLayout } from '@strapi/design-system/Layout';
|
||||
import { Main } from '@strapi/design-system/Main';
|
||||
import { Searchbar } from '@strapi/design-system/Searchbar';
|
||||
import { Box } from '@strapi/design-system/Box';
|
||||
import { LinkButton } from '@strapi/design-system/LinkButton';
|
||||
import { useNotifyAT } from '@strapi/design-system/LiveRegions';
|
||||
import Upload from '@strapi/icons/Upload';
|
||||
import { Typography } from '@strapi/design-system/Typography';
|
||||
import { Flex } from '@strapi/design-system/Flex';
|
||||
|
||||
import PluginCard from './components/PluginCard';
|
||||
import { EmptyPluginSearch } from './components/EmptyPluginSearch';
|
||||
import PageHeader from './components/PageHeader';
|
||||
import { fetchAppInformation } from './utils/api';
|
||||
import useFetchInstalledPlugins from '../../hooks/useFetchInstalledPlugins';
|
||||
import useFetchMarketplacePlugins from '../../hooks/useFetchMarketplacePlugins';
|
||||
import adminPermissions from '../../permissions';
|
||||
import offlineCloud from '../../assets/images/icon_offline-cloud.svg';
|
||||
import useNavigatorOnLine from '../../hooks/useNavigatorOnLine';
|
||||
import MissingPluginBanner from './components/MissingPluginBanner';
|
||||
|
||||
const matchSearch = (plugins, search) => {
|
||||
@ -49,6 +52,7 @@ const MarketPlacePage = () => {
|
||||
const toggleNotification = useNotification();
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const { autoReload: isInDevelopmentMode } = useAppInfos();
|
||||
const isOnline = useNavigatorOnLine();
|
||||
|
||||
useFocusWhenNavigate();
|
||||
|
||||
@ -117,6 +121,42 @@ const MarketPlacePage = () => {
|
||||
}
|
||||
}, [toggleNotification, isInDevelopmentMode]);
|
||||
|
||||
if (!isOnline) {
|
||||
return (
|
||||
<Layout>
|
||||
<Main>
|
||||
<PageHeader isOnline={isOnline} />
|
||||
<Flex
|
||||
width="100%"
|
||||
direction="column"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
style={{ paddingTop: '120px' }}
|
||||
>
|
||||
<Box paddingBottom={2}>
|
||||
<Typography textColor="neutral700" variant="alpha">
|
||||
{formatMessage({
|
||||
id: 'admin.pages.MarketPlacePage.offline.title',
|
||||
defaultMessage: 'You are offline',
|
||||
})}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box paddingBottom={6}>
|
||||
<Typography textColor="neutral700" variant="epsilon">
|
||||
{formatMessage({
|
||||
id: 'admin.pages.MarketPlacePage.offline.subtitle',
|
||||
defaultMessage:
|
||||
'You need to be connected to the Internet to access Strapi Market.',
|
||||
})}
|
||||
</Typography>
|
||||
</Box>
|
||||
<img src={offlineCloud} alt="offline" style={{ width: '88px', height: '88px' }} />
|
||||
</Flex>
|
||||
</Main>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
if (hasFailed) {
|
||||
return (
|
||||
<Layout>
|
||||
@ -151,29 +191,7 @@ const MarketPlacePage = () => {
|
||||
defaultMessage: 'Marketplace - Plugins',
|
||||
})}
|
||||
/>
|
||||
<HeaderLayout
|
||||
title={formatMessage({
|
||||
id: 'global.marketplace',
|
||||
defaultMessage: 'Marketplace',
|
||||
})}
|
||||
subtitle={formatMessage({
|
||||
id: 'admin.pages.MarketPlacePage.subtitle',
|
||||
defaultMessage: 'Get more out of Strapi',
|
||||
})}
|
||||
primaryAction={
|
||||
<LinkButton
|
||||
startIcon={<Upload />}
|
||||
variant="tertiary"
|
||||
href="https://market.strapi.io/submit-plugin"
|
||||
onClick={() => trackUsage('didSubmitPlugin')}
|
||||
>
|
||||
{formatMessage({
|
||||
id: 'admin.pages.MarketPlacePage.submit.plugin.link',
|
||||
defaultMessage: 'Submit your plugin',
|
||||
})}
|
||||
</LinkButton>
|
||||
}
|
||||
/>
|
||||
<PageHeader isOnline={isOnline} />
|
||||
<ContentLayout>
|
||||
<Box width="25%" paddingBottom={4}>
|
||||
<Searchbar
|
||||
|
||||
@ -11,11 +11,14 @@ import { IntlProvider } from 'react-intl';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
import { ThemeProvider, lightTheme } from '@strapi/design-system';
|
||||
import { useTracking, useAppInfos } from '@strapi/helper-plugin';
|
||||
import useNavigatorOnLine from '../../../hooks/useNavigatorOnLine';
|
||||
import MarketPlacePage from '../index';
|
||||
import server from './server';
|
||||
|
||||
const toggleNotification = jest.fn();
|
||||
|
||||
jest.mock('../../../hooks/useNavigatorOnLine', () => jest.fn(() => true));
|
||||
|
||||
jest.mock('@strapi/helper-plugin', () => ({
|
||||
...jest.requireActual('@strapi/helper-plugin'),
|
||||
useNotification: jest.fn(() => {
|
||||
@ -1664,7 +1667,7 @@ describe('Marketplace page', () => {
|
||||
expect(noResult).toBeVisible();
|
||||
});
|
||||
|
||||
it('handles production environment', async () => {
|
||||
it('handles production environment', () => {
|
||||
// Simulate production environment
|
||||
useAppInfos.mockImplementation(() => ({ autoReload: false }));
|
||||
const { queryByText } = render(App);
|
||||
@ -1678,9 +1681,25 @@ describe('Marketplace page', () => {
|
||||
},
|
||||
blockTransition: true,
|
||||
});
|
||||
|
||||
expect(toggleNotification).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Should not show install buttons
|
||||
expect(queryByText(/copy install command/i)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows an online layout', () => {
|
||||
render(App);
|
||||
const offlineText = screen.queryByText('You are offline');
|
||||
|
||||
expect(offlineText).toEqual(null);
|
||||
});
|
||||
|
||||
it('shows the offline layout', () => {
|
||||
useNavigatorOnLine.mockReturnValueOnce(false);
|
||||
render(App);
|
||||
const offlineText = screen.getByText('You are offline');
|
||||
|
||||
expect(offlineText).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@ -205,6 +205,8 @@
|
||||
"Users.components.List.empty.withFilters": "There is no users with the applied filters...",
|
||||
"Users.components.List.empty.withSearch": "There is no users corresponding to the search ({search})...",
|
||||
"admin.pages.MarketPlacePage.helmet": "Marketplace - Plugins",
|
||||
"admin.pages.MarketPlacePage.offline.title": "You are offline",
|
||||
"admin.pages.MarketPlacePage.offline.subtitle": "You need to be connected to the Internet to access Strapi Market.",
|
||||
"admin.pages.MarketPlacePage.plugin.copy": "Copy install command",
|
||||
"admin.pages.MarketPlacePage.plugin.copy.success": "Install command ready to be pasted in your terminal",
|
||||
"admin.pages.MarketPlacePage.plugin.info": "Learn more",
|
||||
|
||||
@ -1,8 +1,14 @@
|
||||
import { get } from 'lodash';
|
||||
|
||||
const getYupInnerErrors = (error) => {
|
||||
const getYupInnerErrors = error => {
|
||||
return get(error, 'inner', []).reduce((acc, curr) => {
|
||||
acc[curr.path.split('[').join('.').split(']').join('')] = {
|
||||
acc[
|
||||
curr.path
|
||||
.split('[')
|
||||
.join('.')
|
||||
.split(']')
|
||||
.join('')
|
||||
] = {
|
||||
id: curr.message,
|
||||
defaultMessage: curr.message,
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user