diff --git a/examples/getstarted/.gitignore b/examples/getstarted/.gitignore index 42f3fcdbd2..78f32f6e55 100644 --- a/examples/getstarted/.gitignore +++ b/examples/getstarted/.gitignore @@ -110,6 +110,7 @@ testApp license.txt exports *.cache +dist build documentation .strapi-updater.json diff --git a/examples/kitchensink-ts/.gitignore b/examples/kitchensink-ts/.gitignore index 8e339ee395..2ed5eafd63 100644 --- a/examples/kitchensink-ts/.gitignore +++ b/examples/kitchensink-ts/.gitignore @@ -110,5 +110,6 @@ coverage license.txt exports *.cache +dist build .strapi-updater.json diff --git a/examples/kitchensink/.gitignore b/examples/kitchensink/.gitignore index 8e339ee395..2ed5eafd63 100644 --- a/examples/kitchensink/.gitignore +++ b/examples/kitchensink/.gitignore @@ -110,5 +110,6 @@ coverage license.txt exports *.cache +dist build .strapi-updater.json diff --git a/packages/cli/create-strapi-app/create-strapi-app.js b/packages/cli/create-strapi-app/create-strapi-app.js index 50188fb8b4..7b4a7af926 100644 --- a/packages/cli/create-strapi-app/create-strapi-app.js +++ b/packages/cli/create-strapi-app/create-strapi-app.js @@ -62,6 +62,15 @@ async function initProject(projectName, program) { await checkInstallPath(resolve(projectName)); } + const programFlags = program.options + .reduce((acc, { short, long }) => [...acc, short, long], []) + .filter(Boolean); + + if (program.template && programFlags.includes(program.template)) { + console.error(`${program.template} is not a valid template`); + process.exit(1); + } + const hasDatabaseOptions = databaseOptions.some((opt) => program[opt]); if (program.quickstart && hasDatabaseOptions) { diff --git a/packages/cli/create-strapi-starter/resources/gitignore b/packages/cli/create-strapi-starter/resources/gitignore index 8e339ee395..2ed5eafd63 100644 --- a/packages/cli/create-strapi-starter/resources/gitignore +++ b/packages/cli/create-strapi-starter/resources/gitignore @@ -110,5 +110,6 @@ coverage license.txt exports *.cache +dist build .strapi-updater.json diff --git a/packages/core/admin/admin/src/pages/MarketplacePage/components/NpmPackageCard/CardButton.js b/packages/core/admin/admin/src/pages/MarketplacePage/components/NpmPackageCard/CardButton.js new file mode 100644 index 0000000000..dbe992eeeb --- /dev/null +++ b/packages/core/admin/admin/src/pages/MarketplacePage/components/NpmPackageCard/CardButton.js @@ -0,0 +1,110 @@ +import React from 'react'; +import semver from 'semver'; +import PropTypes from 'prop-types'; +import { useIntl } from 'react-intl'; +import { Tooltip } from '@strapi/design-system/Tooltip'; +import { Button } from '@strapi/design-system/Button'; +import { Box } from '@strapi/design-system/Box'; +import Duplicate from '@strapi/icons/Duplicate'; + +const TooltipButton = ({ description, installMessage, disabled, handleCopy, pluginName }) => ( + + + + + +); + +const CardButton = ({ strapiPeerDepVersion, strapiAppVersion, handleCopy, pluginName }) => { + const { formatMessage } = useIntl(); + const versionRange = semver.validRange(strapiPeerDepVersion); + const isCompatible = semver.satisfies(strapiAppVersion, versionRange); + + const installMessage = formatMessage({ + id: 'admin.pages.MarketPlacePage.plugin.copy', + defaultMessage: 'Copy install command', + }); + + // Only plugins receive a strapiAppVersion + if (strapiAppVersion) { + if (!versionRange) { + return ( + + ); + } + + if (!isCompatible) { + return ( + + ); + } + } + + return ( + + ); +}; + +TooltipButton.defaultProps = { + disabled: false, + handleCopy: null, +}; + +TooltipButton.propTypes = { + description: PropTypes.string.isRequired, + installMessage: PropTypes.string.isRequired, + disabled: PropTypes.bool, + handleCopy: PropTypes.func, + pluginName: PropTypes.string.isRequired, +}; + +CardButton.defaultProps = { + strapiAppVersion: null, + strapiPeerDepVersion: null, +}; + +CardButton.propTypes = { + strapiAppVersion: PropTypes.string, + strapiPeerDepVersion: PropTypes.string, + handleCopy: PropTypes.func.isRequired, + pluginName: PropTypes.string.isRequired, +}; + +export default CardButton; diff --git a/packages/core/admin/admin/src/pages/MarketplacePage/components/NpmPackageCard/InstallPluginButton.js b/packages/core/admin/admin/src/pages/MarketplacePage/components/NpmPackageCard/InstallPluginButton.js index 21be26758e..0dac88435a 100644 --- a/packages/core/admin/admin/src/pages/MarketplacePage/components/NpmPackageCard/InstallPluginButton.js +++ b/packages/core/admin/admin/src/pages/MarketplacePage/components/NpmPackageCard/InstallPluginButton.js @@ -1,20 +1,34 @@ import React from 'react'; import PropTypes from 'prop-types'; import { useIntl } from 'react-intl'; -import { CopyToClipboard } from 'react-copy-to-clipboard'; import { useNotification, useTracking } from '@strapi/helper-plugin'; import { Box } from '@strapi/design-system/Box'; import { Icon } from '@strapi/design-system/Icon'; import { Typography } from '@strapi/design-system/Typography'; import Check from '@strapi/icons/Check'; -import Duplicate from '@strapi/icons/Duplicate'; -import { Button } from '@strapi/design-system/Button'; +import CardButton from './CardButton'; -const InstallPluginButton = ({ isInstalled, isInDevelopmentMode, commandToCopy }) => { +const InstallPluginButton = ({ + isInstalled, + isInDevelopmentMode, + commandToCopy, + strapiAppVersion, + strapiPeerDepVersion, + pluginName, +}) => { const toggleNotification = useNotification(); const { formatMessage } = useIntl(); const { trackUsage } = useTracking(); + const handleCopy = () => { + navigator.clipboard.writeText(commandToCopy); + trackUsage('willInstallPlugin'); + toggleNotification({ + type: 'success', + message: { id: 'admin.pages.MarketPlacePage.plugin.copy.success' }, + }); + }; + // Already installed if (isInstalled) { return ( @@ -33,23 +47,12 @@ const InstallPluginButton = ({ isInstalled, isInDevelopmentMode, commandToCopy } // In development, show install button if (isInDevelopmentMode) { return ( - { - trackUsage('willInstallPlugin'); - toggleNotification({ - type: 'success', - message: { id: 'admin.pages.MarketPlacePage.plugin.copy.success' }, - }); - }} - text={commandToCopy} - > - - + ); } @@ -57,10 +60,18 @@ const InstallPluginButton = ({ isInstalled, isInDevelopmentMode, commandToCopy } return null; }; +InstallPluginButton.defaultProps = { + strapiAppVersion: null, + strapiPeerDepVersion: null, +}; + InstallPluginButton.propTypes = { isInstalled: PropTypes.bool.isRequired, isInDevelopmentMode: PropTypes.bool.isRequired, commandToCopy: PropTypes.string.isRequired, + strapiAppVersion: PropTypes.string, + strapiPeerDepVersion: PropTypes.string, + pluginName: PropTypes.string.isRequired, }; export default InstallPluginButton; diff --git a/packages/core/admin/admin/src/pages/MarketplacePage/components/NpmPackageCard/index.js b/packages/core/admin/admin/src/pages/MarketplacePage/components/NpmPackageCard/index.js index 980a7b9c26..451f635b97 100644 --- a/packages/core/admin/admin/src/pages/MarketplacePage/components/NpmPackageCard/index.js +++ b/packages/core/admin/admin/src/pages/MarketplacePage/components/NpmPackageCard/index.js @@ -32,6 +32,7 @@ const NpmPackageCard = ({ useYarn, isInDevelopmentMode, npmPackageType, + strapiAppVersion, }) => { const { attributes } = npmPackage; const { formatMessage } = useIntl(); @@ -139,6 +140,9 @@ const NpmPackageCard = ({ isInstalled={isInstalled} isInDevelopmentMode={isInDevelopmentMode} commandToCopy={commandToCopy} + strapiAppVersion={strapiAppVersion} + strapiPeerDepVersion={attributes.strapiVersion} + pluginName={attributes.name} /> @@ -147,6 +151,7 @@ const NpmPackageCard = ({ NpmPackageCard.defaultProps = { isInDevelopmentMode: false, + strapiAppVersion: null, }; NpmPackageCard.propTypes = { @@ -164,12 +169,14 @@ NpmPackageCard.propTypes = { validated: PropTypes.bool.isRequired, madeByStrapi: PropTypes.bool.isRequired, strapiCompatibility: PropTypes.oneOf(['v3', 'v4']), + strapiVersion: PropTypes.string, }).isRequired, }).isRequired, isInstalled: PropTypes.bool.isRequired, useYarn: PropTypes.bool.isRequired, isInDevelopmentMode: PropTypes.bool, npmPackageType: PropTypes.string.isRequired, + strapiAppVersion: PropTypes.string, }; export default NpmPackageCard; diff --git a/packages/core/admin/admin/src/pages/MarketplacePage/components/NpmPackagesGrid/index.js b/packages/core/admin/admin/src/pages/MarketplacePage/components/NpmPackagesGrid/index.js index 5bd80fc983..3e9f655770 100644 --- a/packages/core/admin/admin/src/pages/MarketplacePage/components/NpmPackagesGrid/index.js +++ b/packages/core/admin/admin/src/pages/MarketplacePage/components/NpmPackagesGrid/index.js @@ -9,6 +9,7 @@ const NpmPackagesGrid = ({ useYarn, isInDevelopmentMode, npmPackageType, + strapiAppVersion, }) => { // Check if an individual package is in the dependencies const isAlreadyInstalled = useCallback( @@ -26,6 +27,7 @@ const NpmPackagesGrid = ({ useYarn={useYarn} isInDevelopmentMode={isInDevelopmentMode} npmPackageType={npmPackageType} + strapiAppVersion={strapiAppVersion} /> ))} @@ -35,6 +37,7 @@ const NpmPackagesGrid = ({ NpmPackagesGrid.defaultProps = { installedPackageNames: [], + strapiAppVersion: null, }; NpmPackagesGrid.propTypes = { @@ -43,6 +46,7 @@ NpmPackagesGrid.propTypes = { useYarn: PropTypes.bool.isRequired, isInDevelopmentMode: PropTypes.bool.isRequired, npmPackageType: PropTypes.string.isRequired, + strapiAppVersion: PropTypes.string, }; export default NpmPackagesGrid; diff --git a/packages/core/admin/admin/src/pages/MarketplacePage/index.js b/packages/core/admin/admin/src/pages/MarketplacePage/index.js index 1d5552fda4..133a9a1ba5 100644 --- a/packages/core/admin/admin/src/pages/MarketplacePage/index.js +++ b/packages/core/admin/admin/src/pages/MarketplacePage/index.js @@ -50,7 +50,7 @@ const MarketPlacePage = () => { const toggleNotification = useNotification(); const [searchQuery, setSearchQuery] = useState(''); const [npmPackageType, setNpmPackageType] = useState('plugin'); - const { autoReload: isInDevelopmentMode, dependencies, useYarn } = useAppInfos(); + const { autoReload: isInDevelopmentMode, dependencies, useYarn, strapiVersion } = useAppInfos(); const isOnline = useNavigatorOnLine(); useFocusWhenNavigate(); @@ -247,6 +247,7 @@ const MarketPlacePage = () => { useYarn={useYarn} isInDevelopmentMode={isInDevelopmentMode} npmPackageType="plugin" + strapiAppVersion={strapiVersion} /> )} 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..6cecbbbfe5 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 @@ -1223,38 +1223,46 @@ exports[`Marketplace page renders and matches the plugin tab snapshot 1`] = ` - - - Copy install command - - + @@ -1340,38 +1348,47 @@ exports[`Marketplace page renders and matches the plugin tab snapshot 1`] = ` - - - Copy install command - - + @@ -1410,7 +1427,7 @@ exports[`Marketplace page renders and matches the plugin tab snapshot 1`] = ` Documentation
@@ -1579,38 +1596,47 @@ exports[`Marketplace page renders and matches the plugin tab snapshot 1`] = `
- - - Copy install command - - +
@@ -2724,7 +2750,7 @@ exports[`Marketplace page renders and matches the provider tab snapshot 1`] = ` Amazon Ses
@@ -2856,7 +2882,7 @@ exports[`Marketplace page renders and matches the provider tab snapshot 1`] = ` AWS S3
@@ -2988,7 +3014,7 @@ exports[`Marketplace page renders and matches the provider tab snapshot 1`] = ` Cloudinary
@@ -3110,7 +3136,7 @@ exports[`Marketplace page renders and matches the provider tab snapshot 1`] = ` Local Upload
@@ -3242,7 +3268,7 @@ exports[`Marketplace page renders and matches the provider tab snapshot 1`] = ` Mailgun
@@ -3374,7 +3400,7 @@ exports[`Marketplace page renders and matches the provider tab snapshot 1`] = ` Nodemailer
@@ -3506,7 +3532,7 @@ exports[`Marketplace page renders and matches the provider tab snapshot 1`] = ` Rackspace
@@ -3638,7 +3664,7 @@ exports[`Marketplace page renders and matches the provider tab snapshot 1`] = ` SendGrid
@@ -3770,7 +3796,7 @@ exports[`Marketplace page renders and matches the provider tab snapshot 1`] = ` Sendmail
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..a12c8df85b 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 @@ -8,6 +8,7 @@ import { screen, getByText, queryByText, + getByRole, } from '@testing-library/react'; import { IntlProvider } from 'react-intl'; import { QueryClient, QueryClientProvider } from 'react-query'; @@ -35,6 +36,7 @@ jest.mock('@strapi/helper-plugin', () => ({ '@strapi/plugin-documentation': '4.2.0', '@strapi/provider-upload-cloudinary': '4.2.0', }, + strapiVersion: '4.1.0', useYarn: true, })), })); @@ -215,7 +217,7 @@ describe('Marketplace page', () => { 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 +237,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 }); @@ -255,4 +257,38 @@ describe('Marketplace page', () => { const notInstalledText = queryByText(notInstalledCard, /copy install command/i); expect(notInstalledText).toBeVisible(); }); + + it('disables the button and shows compatibility tooltip message when version provided', async () => { + const { getByTestId } = render(App); + const alreadyInstalledCard = screen + .getAllByTestId('npm-package-card') + .find((div) => div.innerHTML.includes('Transformer')); + const button = getByRole(alreadyInstalledCard, 'button', { name: /copy install command/i }); + const tooltip = getByTestId(`tooltip-Transformer`); + fireEvent.mouseOver(button); + await waitFor(() => { + expect(tooltip).toBeVisible(); + }); + expect(button).toBeDisabled(); + expect(tooltip).toBeInTheDocument(); + expect(tooltip).toHaveTextContent('Update your Strapi version: "4.1.0" to: "4.0.7"'); + }); + + it('shows compatibility tooltip message when no version provided', async () => { + const { getByTestId } = render(App); + const alreadyInstalledCard = screen + .getAllByTestId('npm-package-card') + .find((div) => div.innerHTML.includes('Config Sync')); + const button = getByRole(alreadyInstalledCard, 'button', { name: /copy install command/i }); + const tooltip = getByTestId(`tooltip-Config Sync`); + fireEvent.mouseOver(button); + await waitFor(() => { + expect(tooltip).toBeVisible(); + }); + expect(button).not.toBeDisabled(); + expect(tooltip).toBeInTheDocument(); + expect(tooltip).toHaveTextContent( + 'Unable to verify compatibility with your Strapi version: "4.1.0"' + ); + }); }); 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..6e87ef6eb7 100644 --- a/packages/core/admin/admin/src/pages/MarketplacePage/tests/server.js +++ b/packages/core/admin/admin/src/pages/MarketplacePage/tests/server.js @@ -48,6 +48,7 @@ const handlers = [ validated: false, madeByStrapi: false, strapiCompatibility: 'v3', + strapiVersion: '^4.0.0', }, }, { @@ -221,6 +222,7 @@ const handlers = [ validated: false, madeByStrapi: false, strapiCompatibility: 'v4', + strapiVersion: '4.x.x', }, }, { @@ -291,6 +293,7 @@ const handlers = [ validated: true, madeByStrapi: false, strapiCompatibility: 'v4', + strapiVersion: 'Contact developer', }, }, { @@ -362,6 +365,7 @@ const handlers = [ validated: false, madeByStrapi: false, strapiCompatibility: 'v4', + strapiVersion: '^3.4.2', }, }, { @@ -404,6 +408,7 @@ const handlers = [ validated: true, madeByStrapi: true, strapiCompatibility: 'v4', + strapiVersion: '^4.0.7', }, }, { @@ -446,6 +451,7 @@ const handlers = [ validated: true, madeByStrapi: false, strapiCompatibility: 'v3', + strapiVersion: '^4.3.0', }, }, { @@ -488,6 +494,7 @@ const handlers = [ validated: false, madeByStrapi: false, strapiCompatibility: 'v4', + strapiVersion: '4.0.7', }, }, ], diff --git a/packages/core/admin/admin/src/translations/en.json b/packages/core/admin/admin/src/translations/en.json index 52320dfbe3..128dff946d 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", @@ -270,6 +270,8 @@ "admin.pages.MarketPlacePage.plugin.installed": "Installed", "admin.pages.MarketPlacePage.plugin.tooltip.madeByStrapi": "Made by Strapi", "admin.pages.MarketPlacePage.plugin.tooltip.verified": "Plugin verified by Strapi", + "admin.pages.MarketPlacePage.plugin.version": "Update your Strapi version: \"{strapiAppVersion}\" to: \"{versionRange}\"", + "admin.pages.MarketPlacePage.plugin.version.null": "Unable to verify compatibility with your Strapi version: \"{strapiAppVersion}\"", "admin.pages.MarketPlacePage.providers": "Providers", "admin.pages.MarketPlacePage.search.clear": "Clear the search", "admin.pages.MarketPlacePage.search.empty": "No result for \"{target}\"", diff --git a/packages/core/admin/package.json b/packages/core/admin/package.json index 42c3018c03..616e1ac22d 100644 --- a/packages/core/admin/package.json +++ b/packages/core/admin/package.json @@ -164,4 +164,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/generators/app/lib/resources/dot-files/common/gitignore b/packages/generators/app/lib/resources/dot-files/common/gitignore index 8e339ee395..2ed5eafd63 100644 --- a/packages/generators/app/lib/resources/dot-files/common/gitignore +++ b/packages/generators/app/lib/resources/dot-files/common/gitignore @@ -110,5 +110,6 @@ coverage license.txt exports *.cache +dist build .strapi-updater.json