[MM-69593] Remove StreamlinedMarketplace feature flag (#37390)

* Remove StreamlinedMarketplace feature flag definition

Co-authored-by: mattermost-code <matty-code@mattermost.com>

* Remove StreamlinedMarketplace feature flag from webapp

The streamlined marketplace UI is now the permanent behavior. Remove the
selector, the legacy tabbed/search branch of the marketplace modal, and
update the affected tests and snapshots.

Co-authored-by: mattermost-code <matty-code@mattermost.com>

* Remove obsolete StreamlinedMarketplace feature flag guard in cypress spec

Co-authored-by: mattermost-code <matty-code@mattermost.com>

* Remove unused marketplace i18n strings after flag removal

Co-authored-by: mattermost-code <matty-code@mattermost.com>

* Clamp marketplace pagination when listing shrinks

When fetchListing() or plugin-status refetches reduce the listing size,
keep the current page within the last valid page so users are not left
on an empty view with no pagination footer.

Co-authored-by: mattermost-code <matty-code@mattermost.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: mattermost-code <matty-code@mattermost.com>
This commit is contained in:
cursor[bot]
2026-07-08 16:03:57 -03:00
committed by GitHub
co-authored by mattermost-code Cursor Agent
parent 7870605fb1
commit af43bfdbb0
9 changed files with 553 additions and 1162 deletions
@@ -17,7 +17,6 @@ describe('Plugin Marketplace', () => {
before(() => {
cy.shouldNotRunOnCloudEdition();
cy.shouldHaveFeatureFlag('StreamlinedMarketplace', 'false'); // https://mattermost.atlassian.net/browse/MM-54230
cy.shouldHavePluginUploadEnabled();
cy.apiInitSetup().then(({team}) => {
@@ -799,7 +799,6 @@ const defaultServerConfig: AdminConfig = {
OnboardingTourTips: true,
EnableExportDirectDownload: false,
MoveThreadsEnabled: false,
StreamlinedMarketplace: true,
CloudDedicatedExportUI: false,
WebSocketEventScope: true,
ExperimentalAuditSettingsSystemConsoleUI: true,
-3
View File
@@ -36,8 +36,6 @@ type FeatureFlags struct {
MoveThreadsEnabled bool
StreamlinedMarketplace bool
CloudDedicatedExportUI bool
WebSocketEventScope bool
@@ -156,7 +154,6 @@ func (f *FeatureFlags) SetDefaults() {
f.OnboardingTourTips = true
f.EnableExportDirectDownload = false
f.MoveThreadsEnabled = false
f.StreamlinedMarketplace = true
f.CloudDedicatedExportUI = false
f.WebSocketEventScope = true
f.ExperimentalAuditSettingsSystemConsoleUI = true
@@ -486,7 +486,6 @@ describe('components/PluginManagement', () => {
},
},
appsFeatureFlagEnabled: false,
streamlinedMarketplaceFlagEnabled: false,
actions: {
uploadPlugin: jest.fn(),
installPluginFromUrl: jest.fn(),
@@ -72,9 +72,7 @@ describe('components/marketplace/', () => {
entities: {
general: {
firstAdminCompleteSetup: false,
config: {
FeatureFlagStreamlinedMarketplace: 'false',
},
config: {},
license: {
Cloud: 'false',
},
@@ -169,7 +167,7 @@ describe('components/marketplace/', () => {
}
});
test('hides search, shows web marketplace banner in FeatureFlags.StreamlinedMarketplace', () => {
test('hides search and shows web marketplace banner', () => {
const setState = jest.fn();
const useStateSpy = jest.spyOn(React, 'useState');
useStateSpy.mockImplementation(() => [true, setState]);
@@ -179,8 +177,6 @@ describe('components/marketplace/', () => {
sampleInstalledPlugin,
];
(mockState.entities.general.config as any).FeatureFlagStreamlinedMarketplace = 'true';
// Suppress expected errors from useState mock affecting all child components
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
try {
@@ -197,12 +193,11 @@ describe('components/marketplace/', () => {
}
});
test("doesn't show web marketplace banner in FeatureFlags.StreamlinedMarketplace for Cloud", () => {
test("doesn't show web marketplace banner for Cloud", () => {
const setState = jest.fn();
const useStateSpy = jest.spyOn(React, 'useState');
useStateSpy.mockImplementation(() => [true, setState]);
(mockState.entities.general.config as any).FeatureFlagStreamlinedMarketplace = 'true';
mockState.entities.general.license.Cloud = 'true';
// Suppress expected errors from useState mock affecting all child components
@@ -2,31 +2,25 @@
// See LICENSE.txt for license information.
import classNames from 'classnames';
import debounce from 'lodash/debounce';
import React, {useCallback, useEffect, useRef, useState} from 'react';
import type {ReactNode} from 'react';
import {Tabs, Tab} from 'react-bootstrap';
import type {SelectCallback} from 'react-bootstrap';
import {useIntl} from 'react-intl';
import {useDispatch, useSelector} from 'react-redux';
import {Link} from 'react-router-dom';
import {MagnifyIcon} from '@mattermost/compass-icons/components';
import {FooterPagination, GenericModal} from '@mattermost/components';
import {getPluginStatuses} from 'mattermost-redux/actions/admin';
import {setFirstAdminVisitMarketplaceStatus} from 'mattermost-redux/actions/general';
import {getFirstAdminVisitMarketplaceStatus, getLicense} from 'mattermost-redux/selectors/entities/general';
import {streamlinedMarketplaceEnabled} from 'mattermost-redux/selectors/entities/preferences';
import {fetchListing, filterListing} from 'actions/marketplace';
import {fetchListing} from 'actions/marketplace';
import {closeModal} from 'actions/views/modals';
import {getListing, getInstalledListing} from 'selectors/views/marketplace';
import {getListing} from 'selectors/views/marketplace';
import {isModalOpen} from 'selectors/views/modals';
import usePluginStatusesSync from 'components/common/hooks/usePluginStatusesSync';
import LoadingScreen from 'components/loading_screen';
import Input, {SIZE} from 'components/widgets/inputs/input/input';
import {ModalIdentifiers} from 'utils/constants';
import {isCloudLicense} from 'utils/license_utils';
@@ -38,13 +32,6 @@ import WebMarketplaceBanner from './web_marketplace_banner';
import './marketplace_modal.scss';
const MarketplaceTabs = {
ALL_LISTING: 'all',
INSTALLED_LISTING: 'installed',
};
const SEARCH_TIMEOUT_MILLISECONDS = 200;
const linkConsole = (msg: ReactNode[]): ReactNode => (
<Link to='/admin_console/plugins/plugin_management'>
{msg}
@@ -58,17 +45,13 @@ const MarketplaceModal = () => {
const show = useSelector((state: GlobalState) => isModalOpen(state, ModalIdentifiers.PLUGIN_MARKETPLACE));
const listing = useSelector(getListing);
const installedListing = useSelector(getInstalledListing);
// Refetch plugin statuses while the modal is open whenever the server signals a change.
const pluginStatuses = usePluginStatusesSync();
const hasFirstAdminVisitedMarketplace = useSelector(getFirstAdminVisitMarketplaceStatus);
const isStreamlinedMarketplaceEnabled = useSelector(streamlinedMarketplaceEnabled);
const license = useSelector(getLicense);
const isCloud = isCloudLicense(license);
const [tabKey, setTabKey] = useState(MarketplaceTabs.ALL_LISTING);
const [filter, setFilter] = useState('');
const [page, setPage] = useState(0);
const [hasLoaded, setHasLoaded] = useState(false);
const [loading, setLoading] = React.useState(true);
@@ -84,16 +67,6 @@ const MarketplaceModal = () => {
setLoading(false);
}, []);
const doSearch = useCallback(async () => {
const {error} = await dispatch(filterListing(filter));
if (error) {
setServerError(true);
}
}, [filter]);
const debouncedSearch = debounce(doSearch, SEARCH_TIMEOUT_MILLISECONDS);
useEffect(() => {
async function doFetch() {
await dispatch(getPluginStatuses());
@@ -115,11 +88,11 @@ const MarketplaceModal = () => {
}, [pluginStatuses]);
useEffect(() => {
if (hasLoaded) {
debouncedSearch();
setPage(0);
const maxPage = Math.max(0, Math.ceil(listing.length / ITEMS_PER_PAGE) - 1);
if (page > maxPage) {
setPage(maxPage);
}
}, [filter]);
}, [listing.length, page]);
const scrollListToTop = useCallback(() => {
if (listRef.current) {
@@ -131,20 +104,6 @@ const MarketplaceModal = () => {
dispatch(closeModal(ModalIdentifiers.PLUGIN_MARKETPLACE));
};
const handleChangeTab: SelectCallback = useCallback((tabKey) => {
setTabKey(tabKey);
setPage(0);
scrollListToTop();
}, [scrollListToTop]);
const handleOnChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
setFilter(event.target.value);
}, []);
const handleOnClear = useCallback(() => {
setFilter('');
}, []);
const handleOnNextPage = useCallback(() => {
setPage(page + 1);
scrollListToTop();
@@ -155,65 +114,35 @@ const MarketplaceModal = () => {
scrollListToTop();
}, [page, scrollListToTop]);
const handleNoResultsButtonClick = useCallback(() => {
handleChangeTab(MarketplaceTabs.ALL_LISTING);
}, [handleChangeTab]);
const getHeaderInput = useCallback(() => {
if (isStreamlinedMarketplaceEnabled) {
return null;
}
return (
<Input
id='searchMarketplaceTextbox'
name='searchMarketplaceTextbox'
containerClassName='marketplace-modal-search'
inputClassName='search_input'
type='text'
inputSize={SIZE.LARGE}
inputPrefix={<MagnifyIcon size={24}/>}
placeholder={formatMessage({id: 'marketplace_modal.search', defaultMessage: 'Search marketplace'})}
useLegend={false}
autoFocus={true}
clearable={true}
value={filter}
onChange={handleOnChange}
onClear={handleOnClear}
/>
);
}, [filter, handleOnChange, handleOnClear]);
const getFooterContent = useCallback(() => {
if (isStreamlinedMarketplaceEnabled && listing.length <= ITEMS_PER_PAGE) {
if (listing.length <= ITEMS_PER_PAGE) {
return null;
}
return (
<FooterPagination
page={page}
total={tabKey === MarketplaceTabs.ALL_LISTING ? listing.length : installedListing.length}
total={listing.length}
itemsPerPage={ITEMS_PER_PAGE}
onNextPage={handleOnNextPage}
onPreviousPage={handleOnPreviousPage}
/>
);
}, [installedListing.length, listing.length, page, handleOnNextPage, handleOnPreviousPage, tabKey, isStreamlinedMarketplaceEnabled]);
}, [listing.length, page, handleOnNextPage, handleOnPreviousPage]);
const getAppendedContent = useCallback(() => {
if (!isStreamlinedMarketplaceEnabled || isCloud) {
if (isCloud) {
return null;
}
return <WebMarketplaceBanner/>;
}, [isStreamlinedMarketplaceEnabled, isCloud]);
}, [isCloud]);
return (
<GenericModal
id='marketplace-modal'
className={classNames('marketplace-modal', {
'streamlined-marketplace': isStreamlinedMarketplaceEnabled,
'with-web-marketplace-link': isStreamlinedMarketplaceEnabled && !isCloud,
className={classNames('marketplace-modal', 'streamlined-marketplace', {
'with-web-marketplace-link': !isCloud,
})}
modalHeaderText={formatMessage({id: 'marketplace_modal.title', defaultMessage: 'App Marketplace'})}
ariaLabel={formatMessage({id: 'marketplace_modal.title', defaultMessage: 'App Marketplace'})}
@@ -229,75 +158,21 @@ const MarketplaceModal = () => {
show={show}
compassDesign={true}
bodyPadding={false}
bodyDivider={isStreamlinedMarketplaceEnabled}
bodyDivider={true}
footerDivider={true}
onExited={handleOnClose}
footerContent={getFooterContent()}
appendedContent={getAppendedContent()}
headerInput={getHeaderInput()}
>
{isStreamlinedMarketplaceEnabled ? (
<>
{loading ? (
<LoadingScreen className='loading'/>
) : (
<MarketplaceList
listRef={listRef}
listing={listing}
page={page}
filter={filter}
noResultsMessage={formatMessage({id: 'marketplace_modal.no_plugins', defaultMessage: 'No plugins found'})}
/>
)}
</>
{loading ? (
<LoadingScreen className='loading'/>
) : (
<Tabs
id='marketplaceTabs'
className='tabs'
defaultActiveKey={MarketplaceTabs.ALL_LISTING}
activeKey={tabKey}
onSelect={handleChangeTab}
unmountOnExit={true}
>
<Tab
eventKey={MarketplaceTabs.ALL_LISTING}
title={formatMessage({id: 'marketplace_modal.tabs.all_listing', defaultMessage: 'All'})}
>
{loading ? (
<LoadingScreen className='loading'/>
) : (
<MarketplaceList
listRef={listRef}
listing={listing}
page={page}
filter={filter}
noResultsMessage={formatMessage({id: 'marketplace_modal.no_plugins', defaultMessage: 'No plugins found'})}
/>
)}
</Tab>
<Tab
eventKey={MarketplaceTabs.INSTALLED_LISTING}
title={formatMessage(
{id: 'marketplace_modal.tabs.installed_listing', defaultMessage: 'Installed ({count})'},
{count: installedListing.length},
)}
>
<MarketplaceList
listRef={listRef}
listing={installedListing}
page={page}
filter={filter}
noResultsMessage={formatMessage({
id: 'marketplace_modal.no_plugins_installed',
defaultMessage: 'No plugins installed found',
})}
noResultsAction={{
label: formatMessage({id: 'marketplace_modal.install_plugins', defaultMessage: 'Install plugins'}),
onClick: handleNoResultsButtonClick,
}}
/>
</Tab>
</Tabs>
<MarketplaceList
listRef={listRef}
listing={listing}
page={page}
noResultsMessage={formatMessage({id: 'marketplace_modal.no_plugins', defaultMessage: 'No plugins found'})}
/>
)}
</GenericModal>
);
-5
View File
@@ -5795,7 +5795,6 @@
"marketplace_command.no_permission": "You do not have the appropriate permissions to access the marketplace.",
"marketplace_modal_list.no_plugins_filter": "No results for \"{filter}\"",
"marketplace_modal.app_error": "Error connecting to the marketplace server. Please check your settings in the <linkConsole>System Console</linkConsole>.",
"marketplace_modal.install_plugins": "Install plugins",
"marketplace_modal.installing": "Installing...",
"marketplace_modal.list.configure": "Configure",
"marketplace_modal.list.install": "Install",
@@ -5811,10 +5810,6 @@
"marketplace_modal.list.update_confirmation.message.warning_major_version_with_release_notes": "This update may contain breaking changes. Consult the <a>release notes</a> before upgrading.",
"marketplace_modal.list.update_confirmation.title": "Confirm Plugin Update",
"marketplace_modal.no_plugins": "No plugins found",
"marketplace_modal.no_plugins_installed": "No plugins installed found",
"marketplace_modal.search": "Search marketplace",
"marketplace_modal.tabs.all_listing": "All",
"marketplace_modal.tabs.installed_listing": "Installed ({count})",
"marketplace_modal.title": "App Marketplace",
"marketplace_modal.web_marketplace_link.desc": "Connect the tools you use most to Mattermost",
"marketplace_modal.web_marketplace_link.title": "Discover community integrations",
@@ -330,10 +330,6 @@ export function moveThreadsEnabled(state: GlobalState): boolean {
return getFeatureFlagValue(state, 'MoveThreadsEnabled') === 'true' && getLicense(state).IsLicensed === 'true';
}
export function streamlinedMarketplaceEnabled(state: GlobalState): boolean {
return getFeatureFlagValue(state, 'StreamlinedMarketplace') === 'true';
}
export function getWysiwygEditorPreference(state: GlobalState): boolean {
const featureEnabled = getFeatureFlagValue(state, 'WysiwygEditor') === 'true';
if (!featureEnabled) {