From 7099dac602f41688b1ffbd60a73a4ad54bbf6970 Mon Sep 17 00:00:00 2001 From: Ben Schumacher Date: Thu, 20 Aug 2026 09:35:36 +0200 Subject: [PATCH] [MM-70277] Improve plugin upload dropzone UX (#37569) * Improve plugin upload dropzone UX Co-authored-by: Ben Schumacher * Refine plugin upload dropzone presentation Co-authored-by: Ben Schumacher * Update plugin upload Cypress specs Co-authored-by: Ben Schumacher * Preserve plugin upload drag state over children Co-authored-by: Ben Schumacher * Show plugin manifest name in upload success message The success toast previously interpolated the raw uploaded filename (e.g. mattermost-plugin-fl3xx-v0.8.1-linux-amd64.tar.gz), which is not meaningful to admins. Use the plugin manifest name returned by the upload response instead, falling back to the filename if unavailable. * Indicate upgrade/downgrade/same-version on plugin overwrite State whether an overwritten plugin's success message represents an upgrade, downgrade, or same-version replace, comparing the previously installed manifest version with the newly uploaded one via semver. * Refine plugin upload dropzone presentation and uploading state. Use a native button with Compass upload icon, keep progress inside the dropzone without changing height, and reserve button color for hover/drag-active. Co-authored-by: Cursor * Show selected filename in the dropzone and improve upload status messaging. Keep success/error feedback aligned with the dropzone, with a green check or red alert icon beside the message. Co-authored-by: Cursor * Fix no-nested-ternary lint error in plugin upload dropzone Extract the upload dropzone title text into a helper method to satisfy eslint's no-nested-ternary rule. * Fix stylelint order/properties-order errors in plugin_management.scss * Add unit tests for formatUploadOverwriteMessage semver paths Covers the upgrade, downgrade, same-version, and fallback (missing/invalid semver) branches, which the automated test-analysis bot flagged as untested. * Add unit tests for remaining disabled-reason branches and file drop Covers renderUploadDisabledReason's isDisabled and !enable branches, and adds a fireEvent.drop test for handleUploadDrop, closing out the remaining gaps from the automated test-analysis review. --------- Co-authored-by: Cursor Agent Co-authored-by: Ben Schumacher Co-authored-by: Mattermost Build Co-authored-by: Matthew Birtch <2040554+matthewbirtch@users.noreply.github.com> Co-authored-by: Claude Opus 5 (1M context) --- .../channels/plugins/plugin_install_spec.js | 3 - .../plugins/plugin_startup_fail_spec.js | 11 +- .../channels/plugins/upgrade_spec.js | 18 +- .../plugin_management.test.tsx.snap | 603 +++++++++--------- .../plugin_management/plugin_management.scss | 132 ++++ .../plugin_management.test.tsx | 209 +++++- .../plugin_management/plugin_management.tsx | 318 ++++++--- webapp/channels/src/i18n/en.json | 15 +- 8 files changed, 901 insertions(+), 408 deletions(-) create mode 100644 webapp/channels/src/components/admin_console/plugin_management/plugin_management.scss diff --git a/e2e-tests/cypress/tests/integration/channels/plugins/plugin_install_spec.js b/e2e-tests/cypress/tests/integration/channels/plugins/plugin_install_spec.js index 445b81193308..f010f6485a64 100644 --- a/e2e-tests/cypress/tests/integration/channels/plugins/plugin_install_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/plugins/plugin_install_spec.js @@ -41,9 +41,6 @@ describe('Plugins Management', () => { cy.get('input[type=file]').attachFile({fileContent, fileName: demoPlugin.filename, mimeType}); }); - // # Upload plugin - cy.get('#uploadPlugin').scrollIntoView().should('be.visible').click().wait(TIMEOUTS.HALF_SEC); - // * Verify initial disabled state after upload cy.findByTestId(demoPlugin.id, {timeout: TIMEOUTS.FIVE_MIN}).scrollIntoView().should('be.visible').within(() => { cy.findByText('Enable').should('be.visible'); diff --git a/e2e-tests/cypress/tests/integration/channels/plugins/plugin_startup_fail_spec.js b/e2e-tests/cypress/tests/integration/channels/plugins/plugin_startup_fail_spec.js index f8b8b1c0d253..5fcdc96475ae 100644 --- a/e2e-tests/cypress/tests/integration/channels/plugins/plugin_startup_fail_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/plugins/plugin_startup_fail_spec.js @@ -38,17 +38,8 @@ describe('If plugins fail to start, they can be disabled', () => { cy.get('input[type=file]').attachFile({fileContent, fileName: gitlabPlugin.filename, mimeType}); }); - cy.get('#uploadPlugin').scrollIntoView().should('be.visible').click().wait(TIMEOUTS.HALF_SEC); - - // * Verify that the button shows correct text while uploading - cy.findByText('Uploading...', {timeout: TIMEOUTS.ONE_MIN}).should('be.visible'); - - // * Verify that the button shows correct text and is disabled after upload - cy.findByText('Upload', {timeout: TIMEOUTS.ONE_MIN}).should('be.visible'); - cy.get('#uploadPlugin').and('be.disabled'); - // # Enable GitLab plugin - cy.findByTestId(gitlabPlugin.id).scrollIntoView().should('be.visible').within(() => { + cy.findByTestId(gitlabPlugin.id, {timeout: TIMEOUTS.FIVE_MIN}).scrollIntoView().should('be.visible').within(() => { // * Verify GitLab Plugin title is shown cy.waitUntil(() => cy.get('strong').scrollIntoView().should('be.visible').then((title) => { return title[0].innerText === 'GitLab'; diff --git a/e2e-tests/cypress/tests/integration/channels/plugins/upgrade_spec.js b/e2e-tests/cypress/tests/integration/channels/plugins/upgrade_spec.js index c52b17e4e3f1..f7f83449cb0f 100644 --- a/e2e-tests/cypress/tests/integration/channels/plugins/upgrade_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/plugins/upgrade_spec.js @@ -53,15 +53,8 @@ describe('Plugin remains enabled when upgraded', () => { cy.get('input[type=file]').attachFile({fileContent, fileName: demoPluginOld.filename, mimeType}); }); - cy.get('#uploadPlugin').scrollIntoView().should('be.visible').click().wait(TIMEOUTS.HALF_SEC); - - // * Verify that the button shows correct text while uploading - cy.findByText('Uploading...', {timeout: TIMEOUTS.ONE_MIN}).should('be.visible'); - - // * Verify that the button shows correct text and is disabled after upload + // * Verify the old demo plugin is installed after selecting the file waitForServerStatus(demoPluginOld.id, demoPluginOld.version, {isInstalled: true}); - cy.findByText('Upload', {timeout: TIMEOUTS.ONE_MIN}).should('be.visible'); - cy.get('#uploadPlugin', {timeout: TIMEOUTS.ONE_MIN}).should('be.disabled'); // * Verify that the old demo plugin is successfully uploaded cy.findByText(`Successfully uploaded plugin from ${demoPluginOld.filename}`); @@ -80,7 +73,7 @@ describe('Plugin remains enabled when upgraded', () => { // * Verify older version of demo plugin cy.findByText(new RegExp(`${demoPluginOld.id} - ${demoPluginOld.version}`)).scrollIntoView().should('be.visible'); - cy.get('#uploadPlugin').scrollIntoView().should('be.visible'); + cy.findByText('Click or drop plugin bundle to upload').scrollIntoView().should('be.visible'); // # Upgrade plugin cy.fixture(demoPlugin.filename, 'binary'). @@ -89,16 +82,11 @@ describe('Plugin remains enabled when upgraded', () => { cy.get('input[type=file]').attachFile({fileContent, fileName: demoPlugin.filename, mimeType}); }); - // * Verify that the button shows correct text while uploading - cy.get('#uploadPlugin').should('be.visible').click().wait(TIMEOUTS.HALF_SEC); - // # Confirm overwrite of plugin with same name cy.get('#confirmModalButton').should('be.visible').click(); - // * Verify that the button shows correct text and is disabled after upload + // * Verify that the latest demo plugin is uploaded and remains active after overwrite waitForServerStatus(demoPlugin.id, demoPlugin.version, {isActive: true}); - cy.findByText('Upload', {timeout: TIMEOUTS.ONE_MIN}).should('be.visible'); - cy.get('#uploadPlugin', {timeout: TIMEOUTS.ONE_MIN}).should('be.disabled'); // * Verify that the latest demo plugin is successfully uploaded cy.findByText(`Successfully updated plugin from ${demoPlugin.filename}`); diff --git a/webapp/channels/src/components/admin_console/plugin_management/__snapshots__/plugin_management.test.tsx.snap b/webapp/channels/src/components/admin_console/plugin_management/__snapshots__/plugin_management.test.tsx.snap index af8f9da2f83e..d7a188a17d43 100644 --- a/webapp/channels/src/components/admin_console/plugin_management/__snapshots__/plugin_management.test.tsx.snap +++ b/webapp/channels/src/components/admin_console/plugin_management/__snapshots__/plugin_management.test.tsx.snap @@ -16,7 +16,7 @@ exports[`components/PluginManagement should match snapshot 1`] = ` Plugin Management
-
- - -