From 01b9c867e3e843b03fc3cff0b60e5ac7f874b9ba Mon Sep 17 00:00:00 2001 From: fatadel Date: Fri, 24 Jul 2026 10:49:18 +0200 Subject: [PATCH] Improve discoverability of downloading a local profile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Downloading a profile was only reachable from inside the single "Upload Local Profile" menu, so users looking to save a profile to disk had no way to discover it from the toolbar. Replace that single menu with two buttons, "Download…" and "Share…", each with its own icon. Both open the same options panel; only the heading and the action button differ, so Download saves the profile to a file and Share uploads it for a shareable link. Closes #3620 --- locales/en-US/app.ftl | 15 ++- src/components/app/MenuButtons/Publish.css | 46 ++++++--- src/components/app/MenuButtons/Publish.tsx | 94 ++++++++++++------ src/components/app/MenuButtons/index.tsx | 75 ++++++++------ src/test/components/MenuButtons.test.tsx | 23 +++-- .../__snapshots__/MenuButtons.test.tsx.snap | 99 +++++++++---------- 6 files changed, 214 insertions(+), 138 deletions(-) diff --git a/locales/en-US/app.ftl b/locales/en-US/app.ftl index 3697baf81e..60903eef64 100644 --- a/locales/en-US/app.ftl +++ b/locales/en-US/app.ftl @@ -576,11 +576,14 @@ MenuButtons--index--metaInfo-button = MenuButtons--index--full-view = Full View MenuButtons--index--cancel-upload = Cancel Upload -MenuButtons--index--share-upload = - .label = Upload Local Profile +MenuButtons--index--download = + .label = Download… -MenuButtons--index--share-re-upload = - .label = Re-upload +MenuButtons--index--share = + .label = Share… + +MenuButtons--index--reshare = + .label = Re-share… MenuButtons--index--share-error-uploading = .label = Error uploading @@ -733,9 +736,11 @@ MenuButtons--publish--renderCheckbox-label-preference = Include preference value MenuButtons--publish--renderCheckbox-label-private-browsing = Include the data from private browsing windows MenuButtons--publish--renderCheckbox-label-private-browsing-warning-image = .title = This profile contains private browsing data -MenuButtons--publish--reupload-performance-profile = Re-upload Performance Profile MenuButtons--publish--share-performance-profile = Share Performance Profile +MenuButtons--publish--reshare-performance-profile = Re-share Performance Profile +MenuButtons--publish--download-performance-profile = Download Profile MenuButtons--publish--info-description = Upload your profile and make it accessible to anyone with the link. +MenuButtons--publish--download-info-description = Save this profile as a file on your computer. MenuButtons--publish--info-description-default = By default, your personal data is removed. MenuButtons--publish--info-description-firefox-nightly2 = This profile is from { -firefox-nightly-brand-name }, so by default most information is included. MenuButtons--publish--include-additional-data = Include additional data that may be identifiable diff --git a/src/components/app/MenuButtons/Publish.css b/src/components/app/MenuButtons/Publish.css index 60429e8b08..a57662c280 100644 --- a/src/components/app/MenuButtons/Publish.css +++ b/src/components/app/MenuButtons/Publish.css @@ -17,6 +17,11 @@ background-image: var(--internal-uploading-icon); } +.menuButtonsDownloadButton::before { + background-image: url(../../../../res/img/svg/download.svg); + background-size: auto 12px; +} + .menuButtonsShareButtonError { --internal-error-foreground-color: white; --internal-error-background-color: var(--red-60); @@ -52,12 +57,20 @@ --internal-upload-bar-inner-background-color: var(--blue-50); } +.publishPanelPanel .arrowPanelContent { + padding: 12px; +} + :root.dark-mode { .menuButtonsShareButtonButton { --internal-share-icon: url(../../../../res/img/svg/share-light-12.svg); --internal-uploading-icon: url(../../../../res/img/svg/sharing-animated-dark-12.svg); } + .menuButtonsDownloadButton::before { + background-image: url(../../../../res/img/svg/download-light.svg); + } + .menuButtonsShareButtonError { --internal-error-foreground-color: var(--grey-20); } @@ -74,23 +87,28 @@ .publishPanelContent { position: relative; - - /* This aligns all content, except the big icon. */ - padding-left: 70px; } .publishPanelTitle { - /* "60px" This is the value to put the background image at the right location. - * This background image is 44x44, so this puts it 16px left of the text. */ - padding-left: 60px; - margin: 0 0 0 -60px; - background: var(--internal-info-icon) left center no-repeat; - line-height: 44px; /* This is the height of the background image */ + display: flex; + align-items: center; + margin: 0 0 6px; + font-size: 15px; + font-weight: 600; + gap: 8px; + line-height: 1.3; +} + +.publishPanelTitle::before { + width: 18px; + height: 18px; + flex: none; + background: var(--internal-info-icon) center / contain no-repeat; + content: ''; } .publishPanelInfoDescription { - flex: 1; - margin-bottom: 1em; + margin: 0 0 12px; line-height: 1.5; } @@ -110,7 +128,7 @@ .publishPanelButtons { display: flex; justify-content: right; - margin-top: 20px; + margin-top: 16px; } .publishPanelButton { @@ -154,6 +172,10 @@ background: var(--internal-download-icon) center center no-repeat; } +.publishPanelButtonsDownloadPrimary .publishPanelButtonsSvgDownload { + background-image: url(../../../../res/img/svg/download-light.svg); +} + .menuButtonsDownloadSize { display: inline-block; margin: 0 4px; diff --git a/src/components/app/MenuButtons/Publish.tsx b/src/components/app/MenuButtons/Publish.tsx index b464465a33..32977146d3 100644 --- a/src/components/app/MenuButtons/Publish.tsx +++ b/src/components/app/MenuButtons/Publish.tsx @@ -50,6 +50,7 @@ import './Publish.css'; import { Localized } from '@fluent/react'; type OwnProps = { + readonly mode: 'download' | 'share'; readonly isRepublish?: boolean; }; @@ -127,30 +128,50 @@ class PublishPanelImpl extends React.PureComponent { downloadFileName, shouldSanitizeByDefault, isRepublish, + mode, } = this.props; + const isDownload = mode === 'download'; + + let title; + if (isDownload) { + title = ( + + Download Profile + + ); + } else if (isRepublish) { + title = ( + + Re-share Performance Profile + + ); + } else { + title = ( + + Share Performance Profile + + ); + } + return (
-

- {isRepublish ? ( - - Re-upload Performance Profile +

{title}

+

+ {isDownload ? ( + + Save this profile as a file on your computer. ) : ( - - Share Performance Profile + + Upload your profile and make it accessible to anyone with the + link. - )} -

-

- - Upload your profile and make it accessible to anyone with the - link. - {' '} + )}{' '} {shouldSanitizeByDefault ? ( By default, your personal data is removed. @@ -222,20 +243,24 @@ class PublishPanelImpl extends React.PureComponent {

) : null}
- - + {isDownload ? ( + + ) : ( + + )}
@@ -334,7 +359,10 @@ class PublishPanelImpl extends React.PureComponent { } override render() { - const { uploadPhase } = this.props; + const { uploadPhase, mode } = this.props; + if (mode === 'download') { + return this._renderPublishPanel(); + } switch (uploadPhase) { case 'error': return this._renderErrorPanel(); @@ -383,6 +411,7 @@ export const PublishPanel = explicitConnect< type DownloadButtonProps = { readonly sanitizedProfileEncodingState: SanitizedProfileEncodingState; readonly downloadFileName: string; + readonly primary?: boolean; }; /** @@ -390,9 +419,12 @@ type DownloadButtonProps = { */ class DownloadButton extends React.PureComponent { override render() { - const { sanitizedProfileEncodingState, downloadFileName } = this.props; - const className = - 'photon-button publishPanelButton publishPanelButtonsDownload'; + const { sanitizedProfileEncodingState, downloadFileName, primary } = + this.props; + const className = classNames( + 'photon-button publishPanelButton publishPanelButtonsDownload', + { 'photon-button-primary publishPanelButtonsDownloadPrimary': primary } + ); switch (sanitizedProfileEncodingState.phase) { case 'DONE': { diff --git a/src/components/app/MenuButtons/index.tsx b/src/components/app/MenuButtons/index.tsx index 92c75be4e6..fb40eae489 100644 --- a/src/components/app/MenuButtons/index.tsx +++ b/src/components/app/MenuButtons/index.tsx @@ -249,49 +249,60 @@ class MenuButtonsImpl extends React.PureComponent { const isUploading = uploadPhase === 'uploading' || uploadPhase === 'compressing'; - - if (isUploading) { - return ( - - ); - } - const uploadedStatus = this._getUploadedStatus(dataSource, profileUrl); const isRepublish = uploadedStatus === 'uploaded'; const isError = uploadPhase === 'error'; - let labelL10nId = 'MenuButtons--index--share-upload'; + let shareLabelId = 'MenuButtons--index--share'; if (isRepublish) { - labelL10nId = 'MenuButtons--index--share-re-upload'; + shareLabelId = 'MenuButtons--index--reshare'; } - if (isError) { - labelL10nId = 'MenuButtons--index--share-error-uploading'; + shareLabelId = 'MenuButtons--index--share-error-uploading'; } return ( - - + + } - )} - panelClassName="publishPanelPanel" - // The value for the label following will be replaced - label="" - panelContent={} - /> - + /> + + {isUploading ? ( + + ) : ( + + + } + /> + + )} + ); } diff --git a/src/test/components/MenuButtons.test.tsx b/src/test/components/MenuButtons.test.tsx index 64b7570e9f..f4126ec2cf 100644 --- a/src/test/components/MenuButtons.test.tsx +++ b/src/test/components/MenuButtons.test.tsx @@ -210,9 +210,9 @@ describe('app/MenuButtons', function () { const setupResult = setup(storeWithProfile(profile)); const getPublishButton = () => - screen.getByText(/^(Re-upload|Upload Local Profile)$/); + screen.getByText(/^(Share…|Error uploading)$/); const findPublishButton = () => - screen.findByText(/^(Re-upload|Upload Local Profile)$/); + screen.findByText(/^(Share…|Error uploading)$/); const getCancelButton = () => screen.getByText('Cancel Upload'); const getPanelForm = () => ensureExists( @@ -234,10 +234,16 @@ describe('app/MenuButtons', function () { name: /Include the data from other tabs/, }); const getPanel = () => screen.getByTestId('PublishPanel-container'); + // The Share button opens the panel with the Upload CTA and the submit form. const openPublishPanel = async () => { fireFullClick(getPublishButton()); await screen.findByText(/^(Share|Re-upload) Performance Profile$/); }; + // The Download button opens the same panel but with the Download CTA. + const openDownloadPanel = async () => { + fireFullClick(screen.getByText('Download…')); + await screen.findByText('Download Profile'); + }; return { ...setupResult, @@ -251,6 +257,7 @@ describe('app/MenuButtons', function () { getPrivateBrowsingCheckbox, getRemoveOtherTabsCheckbox, openPublishPanel, + openDownloadPanel, resolveUpload, rejectUpload, }; @@ -286,27 +293,27 @@ describe('app/MenuButtons', function () { it('matches the snapshot for the opened panel for a nightly profile', async () => { const { profile } = createSimpleProfile('nightly'); - const { getPanel, openPublishPanel } = setupForPublish(profile); - await openPublishPanel(); + const { getPanel, openDownloadPanel } = setupForPublish(profile); + await openDownloadPanel(); await screen.findByRole('link', { name: /Download/ }); expect(getPanel()).toMatchSnapshot(); }); it('matches the snapshot for the opened panel for a release profile', async () => { const { profile } = createSimpleProfile('release'); - const { getPanel, openPublishPanel } = setupForPublish(profile); - await openPublishPanel(); + const { getPanel, openDownloadPanel } = setupForPublish(profile); + await openDownloadPanel(); await screen.findByRole('link', { name: /Download/ }); expect(getPanel()).toMatchSnapshot(); }); it('matches the snapshot for the menu buttons and the opened panel for an already uploaded profile', async () => { const { profile } = createSimpleProfile(); - const { getPanel, container, navigateToHash, openPublishPanel } = + const { getPanel, container, navigateToHash, openDownloadPanel } = setupForPublish(profile); navigateToHash('VALID_HASH'); expect(container).toMatchSnapshot(); - await openPublishPanel(); + await openDownloadPanel(); await screen.findByRole('link', { name: /Download/ }); expect(getPanel()).toMatchSnapshot(); }); diff --git a/src/test/components/__snapshots__/MenuButtons.test.tsx.snap b/src/test/components/__snapshots__/MenuButtons.test.tsx.snap index 2135be3fd1..746e3426c2 100644 --- a/src/test/components/__snapshots__/MenuButtons.test.tsx.snap +++ b/src/test/components/__snapshots__/MenuButtons.test.tsx.snap @@ -1806,6 +1806,17 @@ exports[`app/MenuButtons can publish and revert 1`] = ` Profile Info +
+ +
@@ -1814,7 +1825,7 @@ exports[`app/MenuButtons can publish and revert 1`] = ` class="buttonWithPanelButton menuButtonsButton menuButtonsShareButtonButton menuButtonsButton-hasIcon" type="button" > - Re-upload + Re-share…
matches the snapshot for a compression error class="publishPanelContent photon-body-10" >

Share Performance Profile

@@ -1954,13 +1965,6 @@ exports[`app/MenuButtons matches the snapshot for a compression error
-
+
+ +
@@ -2025,7 +2040,7 @@ exports[`app/MenuButtons matches the snapshot for the closed state 1`] class="buttonWithPanelButton menuButtonsButton menuButtonsShareButtonButton menuButtonsButton-hasIcon" type="button" > - Upload Local Profile + Share…
matches the snapshot for the menu buttons and Profile Info
+
+ +
@@ -2062,7 +2088,7 @@ exports[`app/MenuButtons matches the snapshot for the menu buttons and class="buttonWithPanelButton menuButtonsButton menuButtonsShareButtonButton menuButtonsButton-hasIcon" type="button" > - Re-upload + Re-share…
matches the snapshot for the menu buttons and class="publishPanelContent photon-body-10" >

- Re-upload Performance Profile + Download Profile

- Upload your profile and make it accessible to anyone with the link. + Save this profile as a file on your computer. By default, your personal data is removed.

@@ -2171,7 +2197,7 @@ exports[`app/MenuButtons matches the snapshot for the menu buttons and class="publishPanelButtons" > @@ -2188,15 +2214,6 @@ exports[`app/MenuButtons matches the snapshot for the menu buttons and ) -
@@ -2210,14 +2227,14 @@ exports[`app/MenuButtons matches the snapshot for the opened panel for class="publishPanelContent photon-body-10" >

- Share Performance Profile + Download Profile

- Upload your profile and make it accessible to anyone with the link. + Save this profile as a file on your computer. This profile is from ⁨Firefox Nightly⁩, so by default most information is included.

@@ -2289,7 +2306,7 @@ exports[`app/MenuButtons matches the snapshot for the opened panel for class="publishPanelButtons" > @@ -2306,15 +2323,6 @@ exports[`app/MenuButtons matches the snapshot for the opened panel for ) - @@ -2328,14 +2336,14 @@ exports[`app/MenuButtons matches the snapshot for the opened panel for class="publishPanelContent photon-body-10" >

- Share Performance Profile + Download Profile

- Upload your profile and make it accessible to anyone with the link. + Save this profile as a file on your computer. By default, your personal data is removed.

@@ -2402,7 +2410,7 @@ exports[`app/MenuButtons matches the snapshot for the opened panel for class="publishPanelButtons" > @@ -2419,15 +2427,6 @@ exports[`app/MenuButtons matches the snapshot for the opened panel for ) -