diff --git a/CHANGELOG.md b/CHANGELOG.md index 010160dc06..61d22f2f55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,14 @@ ## Unreleased +### New features + +- [#2351: Build the basic plugin details page](https://github.com/alphagov/govuk-prototype-kit/pull/2351) + ## 13.16.0 ### New features - +- - [#2384: Add format items filter to core filters](https://github.com/alphagov/govuk-prototype-kit/pull/2384) - [#2382: Make any npm module a plugin via a proxy plugin config](https://github.com/alphagov/govuk-prototype-kit/pull/2382) diff --git a/cypress/e2e/plugins/plugin-utils.js b/cypress/e2e/plugins/plugin-utils.js index 8a8416ab16..0ab6d16406 100644 --- a/cypress/e2e/plugins/plugin-utils.js +++ b/cypress/e2e/plugins/plugin-utils.js @@ -42,22 +42,26 @@ function provePluginTemplatesUninstalled (plugin) { } function initiatePluginAction (action, plugin, pluginName, options = {}) { - if (action === 'install') { - cy.visit(managePluginsPagePath) - } else { - cy.visit(manageInstalledPluginsPagePath) - } + cy.visit(managePluginsPagePath) if (pluginName) { cy.get(`[data-plugin-package-name="${plugin}"]`) .scrollIntoView() - .find('h4') + .find('a') .contains(pluginName) } cy.get(`[data-plugin-package-name="${plugin}"]`) .scrollIntoView() - .find('button') + .find('a') + .click() + + if (action === 'update') { + cy.get('a') + .contains('Latest version:').click() + } + + cy.get('button') .contains(capitalize(action)) .click() @@ -73,7 +77,7 @@ function provePluginInstalled (plugin, pluginName) { if (pluginName) { cy.get(`[data-plugin-package-name="${plugin}"]`) .scrollIntoView() - .find('h4') + .find('a') .contains(pluginName) } @@ -91,7 +95,7 @@ function provePluginUninstalled (plugin, pluginName) { if (pluginName) { cy.get(`[data-plugin-package-name="${plugin}"]`) .scrollIntoView() - .find('h4') + .find('a') .contains(pluginName) } @@ -108,7 +112,13 @@ function provePluginUpdated (plugin, pluginName) { provePluginInstalled(plugin, pluginName) cy.get(`[data-plugin-package-name="${plugin}"]`) .scrollIntoView() - .find('button') + .find('a') + .click() + + cy.get('a') + .contains('Latest version:').should('not.exist') + + cy.get('button') .contains(capitalize('update')).should('not.exist') } @@ -117,10 +127,13 @@ function provePluginInstalledOldVersion (plugin, pluginName) { if (pluginName) { cy.get(`[data-plugin-package-name="${plugin}"]`) .scrollIntoView() - .find('h4') + .find('a') .contains(pluginName) } + cy.get(`[data-plugin-package-name="${plugin}"] strong.govuk-tag`) + .contains('Update available') + cy.get('#installed-plugins-link').click() cy.get(`[data-plugin-package-name="${plugin}"]`) diff --git a/lib/assets/sass/manage-prototype.scss b/lib/assets/sass/manage-prototype.scss index 977587e8a3..855e33017f 100644 --- a/lib/assets/sass/manage-prototype.scss +++ b/lib/assets/sass/manage-prototype.scss @@ -440,6 +440,15 @@ body .govuk-prototype-kit-manage-prototype-govuk-tag { padding-left: 0 !important; } +.govuk-prototype-kit-manage-prototype-plugin-heading { + margin-bottom: 0; +} + +.govuk-prototype-kit-manage-prototype-plugin-sub-heading { + color: #505a5f; + margin-bottom: 15px; +} + .govuk-prototype-kit-manage-prototype-plugin-list-plugin-list { border-top: 1px solid #b1b4b6; margin-bottom: 2em; @@ -524,6 +533,9 @@ body .govuk-prototype-kit-manage-prototype-govuk-tag { @media(min-width: 40.0525em) { .govuk-prototype-kit-manage-prototype-plugin-list-plugin-list__item-buttons { + .govuk-button-group { + margin-bottom: 0; + } .govuk-button, .govuk-link--no-visited-state { margin: 0 15px 0 0; } @@ -574,3 +586,10 @@ body .govuk-prototype-kit-manage-prototype-govuk-tag { } } } + +.govuk-prototype-kit-manage-prototype-plugin-details-links { + padding-top: 20px; + margin-bottom: 20px; + border-top: 2px solid #b1b4b6; + border-bottom: 2px solid #b1b4b6; +} diff --git a/lib/manage-prototype-handlers.js b/lib/manage-prototype-handlers.js index bfd51f90fd..0126235bb4 100644 --- a/lib/manage-prototype-handlers.js +++ b/lib/manage-prototype-handlers.js @@ -404,7 +404,7 @@ function getTemplatesPostInstallHandler (req, res) { })) } -function buildPluginData (pluginData) { +function buildPluginData (pluginData, isLatest) { if (pluginData === undefined) { return } @@ -417,22 +417,25 @@ function buildPluginData (pluginData) { installedVersion, required, localVersion, - pluginConfig = {} + pluginConfig = {}, + latestPluginConfig } = pluginData + const meta = isLatest ? latestPluginConfig?.meta || pluginConfig?.meta : pluginConfig?.meta const preparedPackageNameForDisplay = plugins.preparePackageNameForDisplay(packageName) return { ...preparedPackageNameForDisplay, - ...pluginConfig.meta, + ...meta, packageName, latestVersion, installedLocally, - installLink: `${contextPath}/plugins/install?package=${encodeURIComponent(packageName)}`, + installLink: installedVersion ? undefined : `${contextPath}/plugins/install?package=${encodeURIComponent(packageName)}`, installCommand: `npm install ${packageName}`, updateLink: updateAvailable ? `${contextPath}/plugins/update?package=${encodeURIComponent(packageName)}` : undefined, updateCommand: latestVersion && `npm install ${packageName}@${latestVersion}`, uninstallLink: installed && !required ? `${contextPath}/plugins/uninstall?package=${encodeURIComponent(packageName)}${installedLocally ? `&version=${encodeURIComponent(localVersion)}` : ''}` : undefined, uninstallCommand: `npm uninstall ${packageName}`, - installedVersion + installedVersion, + inThisPlugin: getInThisPluginDetails(isLatest ? latestPluginConfig || pluginConfig : pluginConfig) } } @@ -457,7 +460,7 @@ async function prepareForPluginPage (isInstalledPage, search) { status: isInstalledPage ? 'installed' : 'search', plugins: plugins.map(buildPluginData), found: plugins.length, - updates: installedPlugins.filter(plugin => plugin.updateAvailable).length + updates: installedPlugins.filter(plugin => !plugin.installedLocally && plugin.updateAvailable).length } } @@ -589,6 +592,24 @@ async function getPluginForRequest (req) { return chosenPlugin } +function getInThisPluginDetails (pluginConfig) { + const { nunjucksMacros = [], templates = [] } = pluginConfig || {} + const list = [] + if (nunjucksMacros?.length) { + list.push({ + title: 'Components', + items: nunjucksMacros.map(({ macroName }) => macroName) + }) + } + if (templates?.length) { + list.push({ + title: 'Templates', + items: templates.map(({ name }) => name) + }) + } + return list +} + function modeIsComplete (mode, { installedVersion, latestVersion, version, installedLocally }) { switch (mode) { case 'update': @@ -749,6 +770,24 @@ async function postPluginsModeHandler (req, res) { res.json({ status }) } +async function getPluginDetailsHandler (req, res) { + const packageName = req.query.package + const isLatest = req.route.path.split('/').pop() === 'latest' + const latestLink = isLatest ? '' : req.originalUrl.replace('?', '/latest?') + const installedLink = isLatest ? req.originalUrl.replace('/latest?', '?') : '' + const plugin = await lookupPackageInfo(packageName) + const { name, scope, installedVersion, latestVersion, ...pluginData } = buildPluginData(plugin, isLatest) + const viewData = { + ...pluginData, + installedVersion, + latestVersion, + latestLink, + installedLink, + plugin: { name, scope, version: latestLink ? installedVersion || latestVersion : latestVersion } + } + res.send(nunjucksManagementEnv.render(getManagementView('plugin-details.njk'), viewData)) +} + module.exports = { contextPath, setKitRestarted, @@ -772,5 +811,6 @@ module.exports = { getPluginsModeHandler, postPluginsStatusHandler, postPluginsModeMiddleware, - postPluginsModeHandler + postPluginsModeHandler, + getPluginDetailsHandler } diff --git a/lib/manage-prototype-handlers.test.js b/lib/manage-prototype-handlers.test.js index a0d0b57385..a532ba11f0 100644 --- a/lib/manage-prototype-handlers.test.js +++ b/lib/manage-prototype-handlers.test.js @@ -40,6 +40,7 @@ const { postTemplatesInstallHandler, getTemplatesPostInstallHandler, getPluginsHandler, + getPluginDetailsHandler, postPluginsStatusHandler, postPluginsModeMiddleware, getPluginsModeHandler, @@ -453,7 +454,8 @@ describe('manage-prototype-handlers', () => { name: pluginDisplayName.name, packageName, uninstallCommand: `npm uninstall ${packageName}`, - updateCommand: `npm install ${packageName}@${latestVersion}` + updateCommand: `npm install ${packageName}@${latestVersion}`, + inThisPlugin: [] } beforeEach(() => { @@ -521,6 +523,32 @@ describe('manage-prototype-handlers', () => { expect(res.redirect).toHaveBeenCalledWith(fullPath + '?search=' + search) }) + describe('getPluginDetailsHandler', () => { + it('plugins installed', async () => { + fse.readJsonSync.mockReturnValue(undefined) + req.route.path = 'plugins-installed' + await getPluginDetailsHandler(req, res) + expect(mockNunjucksRender).toHaveBeenCalledWith( + 'views/manage-prototype/plugin-details.njk', + expect.objectContaining({ + packageName: 'test-package', + installLink: '/manage-prototype/plugins/install?package=test-package', + installCommand: 'npm install test-package', + updateCommand: 'npm install test-package@2.0.0', + uninstallCommand: 'npm uninstall test-package', + inThisPlugin: [], + latestVersion: '2.0.0', + latestLink: '/current-url', + installedLink: '', + plugin: { + name: 'Test Package', + version: '2.0.0' + } + }) + ) + }) + }) + it('getPluginsModeHandler', async () => { req.params.mode = 'install' req.query.package = packageName diff --git a/lib/manage-prototype-routes.js b/lib/manage-prototype-routes.js index 42c824c03e..5841933aee 100644 --- a/lib/manage-prototype-routes.js +++ b/lib/manage-prototype-routes.js @@ -24,7 +24,8 @@ const { postPluginsModeHandler, postPluginsStatusHandler, pluginCacheMiddleware, - postPluginsHandler + postPluginsHandler, + getPluginDetailsHandler } = require('./manage-prototype-handlers') const { packageDir, projectDir } = require('./utils/paths') const { govukFrontendPaths } = require('./govukFrontendPaths') @@ -79,6 +80,9 @@ router.post('/plugins/:mode', postPluginsModeMiddleware) router.post('/plugins/:mode', csrfProtection, postPluginsModeHandler) +router.get('/plugin-details/latest', getPluginDetailsHandler) +router.get('/plugin-details', getPluginDetailsHandler) + // Find GOV.UK Frontend (via internal package, project fallback) router.use('/dependencies/govuk-frontend', express.static( govukFrontendPaths([packageDir, projectDir]).baseDir) diff --git a/lib/nunjucks/views/manage-prototype/index.njk b/lib/nunjucks/views/manage-prototype/index.njk index ece33f1f15..5ff43596d9 100644 --- a/lib/nunjucks/views/manage-prototype/index.njk +++ b/lib/nunjucks/views/manage-prototype/index.njk @@ -13,7 +13,7 @@
{% if kitUpdateAvailable %}- + New version available: {{ latestAvailableKit }}
diff --git a/lib/nunjucks/views/manage-prototype/plugin-details.njk b/lib/nunjucks/views/manage-prototype/plugin-details.njk new file mode 100644 index 0000000000..c9cad76965 --- /dev/null +++ b/lib/nunjucks/views/manage-prototype/plugin-details.njk @@ -0,0 +1,89 @@ +{% extends "views/manage-prototype/layout.njk" %} +{% from "govuk/components/button/macro.njk" import govukButton %} + +{% block beforeContent %} + {{ super() }} + Back to plugins +{% endblock %} + +{% block content %} + +{% endblock %} diff --git a/lib/nunjucks/views/manage-prototype/plugin-header.njk b/lib/nunjucks/views/manage-prototype/plugin-header.njk new file mode 100644 index 0000000000..89466e5701 --- /dev/null +++ b/lib/nunjucks/views/manage-prototype/plugin-header.njk @@ -0,0 +1,9 @@ +v{{ plugin.version }}
+ {% if plugin.scope %} +By {{ plugin.scope }}
+ {% endif %} +