diff --git a/apps/meteor/client/components/AdministrationList/AppsModelList.tsx b/apps/meteor/client/components/AdministrationList/AppsModelList.tsx index 3f9291fe2cc77..cb6ec49e3d360 100644 --- a/apps/meteor/client/components/AdministrationList/AppsModelList.tsx +++ b/apps/meteor/client/components/AdministrationList/AppsModelList.tsx @@ -1,6 +1,6 @@ import { OptionTitle } from '@rocket.chat/fuselage'; import { useTranslation, useRoute } from '@rocket.chat/ui-contexts'; -import React, { FC } from 'react'; +import React, { ReactElement } from 'react'; import { triggerActionButtonAction } from '../../../app/ui-message/client/ActionManager'; import { IAppAccountBoxItem } from '../../../app/ui-utils/client/lib/AccountBox'; @@ -12,9 +12,10 @@ type AppsModelListProps = { closeList: () => void; }; -const AppsModelList: FC = ({ appBoxItems, showManageApps, closeList }) => { +const AppsModelList = ({ appBoxItems, showManageApps, closeList }: AppsModelListProps): ReactElement => { const t = useTranslation(); const marketplaceRoute = useRoute('admin-marketplace'); + const page = 'list'; return ( <> @@ -26,7 +27,7 @@ const AppsModelList: FC = ({ appBoxItems, showManageApps, cl icon='store' text={t('Marketplace')} action={(): void => { - marketplaceRoute.push(); + marketplaceRoute.push({ context: 'all', page }); closeList(); }} /> @@ -34,7 +35,7 @@ const AppsModelList: FC = ({ appBoxItems, showManageApps, cl icon='cube' text={t('Installed')} action={(): void => { - marketplaceRoute.push({ context: 'installed' }); + marketplaceRoute.push({ context: 'installed', page }); closeList(); }} /> diff --git a/apps/meteor/client/views/admin/apps/AppDetailsPage/AppDetailsPage.tsx b/apps/meteor/client/views/admin/apps/AppDetailsPage/AppDetailsPage.tsx index 84f3ead9d1655..9997c0f542c89 100644 --- a/apps/meteor/client/views/admin/apps/AppDetailsPage/AppDetailsPage.tsx +++ b/apps/meteor/client/views/admin/apps/AppDetailsPage/AppDetailsPage.tsx @@ -27,18 +27,19 @@ const AppDetailsPage = ({ id }: { id: App['id'] }): ReactElement => { const settingsRef = useRef>({}); const appData = useAppInfo(id); - const [routeName, urlParams] = useCurrentRoute(); - const appsRoute = useRoute('admin-apps'); - const marketplaceRoute = useRoute('admin-marketplace'); - const tab = useRouteParameter('tab'); - const [currentRouteName] = useCurrentRoute(); if (!currentRouteName) { throw new Error('No current route name'); } - const router = useRoute(currentRouteName); - const handleReturn = useMutableCallback((): void => router.push({})); + + const [, urlParams] = useCurrentRoute(); + const tab = useRouteParameter('tab'); + const context = useRouteParameter('context'); + + const handleReturn = useMutableCallback((): void => { + context && router.push({ context, page: 'list' }); + }); const { installed, settings, privacyPolicySummary, permissions, tosLink, privacyLink, marketplace } = appData || {}; const isSecurityVisible = privacyPolicySummary || permissions || tosLink || privacyLink; @@ -61,13 +62,7 @@ const AppDetailsPage = ({ id }: { id: App['id'] }): ReactElement => { }, [id, settings]); const handleTabClick = (tab: 'details' | 'security' | 'releases' | 'settings' | 'logs'): void => { - if (routeName === 'admin-marketplace') { - marketplaceRoute.replace({ ...urlParams, tab }); - } - - if (routeName === 'admin-apps') { - appsRoute.replace({ ...urlParams, tab }); - } + router.replace({ ...urlParams, tab }); }; return ( diff --git a/apps/meteor/client/views/admin/apps/AppInstallPage.js b/apps/meteor/client/views/admin/apps/AppInstallPage.js index 43f424ebe073e..ef1c07d345c05 100644 --- a/apps/meteor/client/views/admin/apps/AppInstallPage.js +++ b/apps/meteor/client/views/admin/apps/AppInstallPage.js @@ -6,7 +6,8 @@ import { useEndpoint, useUpload, useTranslation, - useToastMessageDispatch, + useCurrentRoute, + useRouteParameter, } from '@rocket.chat/ui-contexts'; import React, { useCallback, useEffect, useState } from 'react'; @@ -16,16 +17,25 @@ import { useFileInput } from '../../../hooks/useFileInput'; import { useForm } from '../../../hooks/useForm'; import AppPermissionsReviewModal from './AppPermissionsReviewModal'; import AppUpdateModal from './AppUpdateModal'; -import { handleInstallError } from './helpers'; +import { useAppsReload } from './AppsContext'; +import { handleAPIError, handleInstallError } from './helpers'; import { getManifestFromZippedApp } from './lib/getManifestFromZippedApp'; const placeholderUrl = 'https://rocket.chat/apps/package.zip'; function AppInstallPage() { const t = useTranslation(); - const dispatchToastMessage = useToastMessageDispatch(); - const appsRoute = useRoute('admin-apps'); + const reload = useAppsReload(); + + const [currentRouteName] = useCurrentRoute(); + if (!currentRouteName) { + throw new Error('No current route name'); + } + const router = useRoute(currentRouteName); + + const context = useRouteParameter('context'); + const setModal = useSetModal(); const appId = useQueryStringParameter('id'); @@ -61,25 +71,20 @@ function AppInstallPage() { fileData.append('app', appFile, appFile.name); fileData.append('permissions', JSON.stringify(permissionsGranted)); - if (appId) { - try { + try { + if (appId) { await uploadUpdateApp(fileData); - } catch (error) { - dispatchToastMessage({ type: 'error', message: error }); - } finally { - setModal(null); - } - } else { - try { + } else { app = await uploadApp(fileData); - } catch (error) { - dispatchToastMessage({ type: 'error', message: error }); - } finally { - setModal(null); } + } catch (e) { + handleAPIError(e); } - appsRoute.push({ context: 'details', id: appId || app.app.id }); + router.push({ context: 'installed', page: 'info', id: appId || app.app.id }); + + reload(); + setModal(null); }; @@ -140,7 +145,7 @@ function AppInstallPage() { }; const handleCancel = () => { - appsRoute.push(); + router.push({ context, page: 'list' }); }; return ( diff --git a/apps/meteor/client/views/admin/apps/AppMenu.js b/apps/meteor/client/views/admin/apps/AppMenu.js index 44ee181a99c1a..7c5314c54da2c 100644 --- a/apps/meteor/client/views/admin/apps/AppMenu.js +++ b/apps/meteor/client/views/admin/apps/AppMenu.js @@ -1,5 +1,13 @@ import { Box, Icon, Menu, Throbber } from '@rocket.chat/fuselage'; -import { useSetModal, useMethod, useEndpoint, useTranslation, useRoute, useRouteParameter } from '@rocket.chat/ui-contexts'; +import { + useSetModal, + useMethod, + useEndpoint, + useTranslation, + useRoute, + useRouteParameter, + useCurrentRoute, +} from '@rocket.chat/ui-contexts'; import React, { useMemo, useCallback, useState } from 'react'; import { Apps } from '../../../../app/apps/client/orchestrator'; @@ -35,7 +43,13 @@ function AppMenu({ app, ...props }) { const t = useTranslation(); const setModal = useSetModal(); const checkUserLoggedIn = useMethod('cloud:checkUserLoggedIn'); - const appsRoute = useRoute('admin-apps'); + + const [currentRouteName] = useCurrentRoute(); + if (!currentRouteName) { + throw new Error('No current route name'); + } + const router = useRoute(currentRouteName); + const context = useRouteParameter('context'); const setAppStatus = useEndpoint('POST', `/apps/${app.id}/status`); @@ -97,8 +111,8 @@ function AppMenu({ app, ...props }) { }, [checkUserLoggedIn, setModal, closeModal, buildExternalUrl, app.id, app.purchaseType, syncApp]); const handleViewLogs = useCallback(() => { - appsRoute.push({ context: 'details', id: app.id, version: app.version, tab: 'logs' }); - }, [app.id, app.version, appsRoute]); + router.push({ context, page: 'info', id: app.id, version: app.version, tab: 'logs' }); + }, [app.id, app.version, context, router]); const handleDisable = useCallback(() => { const confirm = async () => { @@ -261,6 +275,11 @@ function AppMenu({ app, ...props }) { action: handleEnable, }, }), + ...(app.installed && { + divider: { + type: 'divider', + }, + }), ...(app.installed && { uninstall: { label: ( diff --git a/apps/meteor/client/views/admin/apps/AppsList/AppRow.tsx b/apps/meteor/client/views/admin/apps/AppsList/AppRow.tsx index aca58478dc983..1d07aeafbb884 100644 --- a/apps/meteor/client/views/admin/apps/AppsList/AppRow.tsx +++ b/apps/meteor/client/views/admin/apps/AppsList/AppRow.tsx @@ -3,8 +3,8 @@ import { css } from '@rocket.chat/css-in-js'; import { Box } from '@rocket.chat/fuselage'; import { useBreakpoints } from '@rocket.chat/fuselage-hooks'; import colors from '@rocket.chat/fuselage-tokens/colors'; -import { useRoute } from '@rocket.chat/ui-contexts'; -import React, { FC, memo, KeyboardEvent, MouseEvent } from 'react'; +import { useCurrentRoute, useRoute, useRouteParameter } from '@rocket.chat/ui-contexts'; +import React, { memo, KeyboardEvent, MouseEvent, ReactElement } from 'react'; import AppAvatar from '../../../../components/avatar/AppAvatar'; import AppStatus from '../AppDetailsPage/tabs/AppStatus/AppStatus'; @@ -13,32 +13,28 @@ import BundleChips from '../BundleChips'; type AppRowProps = App & { isMarketplace: boolean }; -// TODO: org props -const AppRow: FC = (props) => { - const { name, id, description, iconFileData, marketplaceVersion, iconFileContent, installed, isSubscribed, isMarketplace, bundledIn } = - props; +const AppRow = (props: AppRowProps): ReactElement => { + const { name, id, description, iconFileData, marketplaceVersion, iconFileContent, installed, isSubscribed, bundledIn } = props; const breakpoints = useBreakpoints(); const isDescriptionVisible = breakpoints.includes('xl'); - const appsRoute = useRoute('admin-apps'); - const marketplaceRoute = useRoute('admin-marketplace'); + const [currentRouteName] = useCurrentRoute(); + if (!currentRouteName) { + throw new Error('No current route name'); + } + const router = useRoute(currentRouteName); - const handleClick = (): void => { - if (isMarketplace) { - marketplaceRoute.push({ - context: 'details', + const context = useRouteParameter('context'); + + const handleNavigateToAppInfo = (): void => { + context && + router.push({ + context, + page: 'info', version: marketplaceVersion, id, }); - return; - } - - appsRoute.push({ - context: 'details', - version: marketplaceVersion, - id, - }); }; const handleKeyDown = (e: KeyboardEvent): void => { @@ -46,7 +42,7 @@ const AppRow: FC = (props) => { return; } - handleClick(); + handleNavigateToAppInfo(); }; const preventClickPropagation = (e: MouseEvent): void => { @@ -67,7 +63,7 @@ const AppRow: FC = (props) => { key={id} role='link' tabIndex={0} - onClick={handleClick} + onClick={handleNavigateToAppInfo} onKeyDown={handleKeyDown} display='flex' flexDirection='row' diff --git a/apps/meteor/client/views/admin/apps/AppsPage/AppsPage.tsx b/apps/meteor/client/views/admin/apps/AppsPage/AppsPage.tsx index 400ec4c3bcf15..23485b04dc87f 100644 --- a/apps/meteor/client/views/admin/apps/AppsPage/AppsPage.tsx +++ b/apps/meteor/client/views/admin/apps/AppsPage/AppsPage.tsx @@ -1,5 +1,5 @@ import { Button, ButtonGroup, Icon, Skeleton, Tabs } from '@rocket.chat/fuselage'; -import { useRoute, useSetting, useMethod, useTranslation } from '@rocket.chat/ui-contexts'; +import { useRoute, useSetting, useMethod, useTranslation, useCurrentRoute, useRouteParameter } from '@rocket.chat/ui-contexts'; import React, { useEffect, useState, ReactElement } from 'react'; import Page from '../../../../components/Page'; @@ -13,11 +13,17 @@ const AppsPage = ({ isMarketplace }: AppsPageProps): ReactElement => { const t = useTranslation(); const isDevelopmentMode = useSetting('Apps_Framework_Development_Mode'); - const marketplaceRoute = useRoute('admin-marketplace'); - const appsRoute = useRoute('admin-apps'); const cloudRoute = useRoute('cloud'); const checkUserLoggedIn = useMethod('cloud:checkUserLoggedIn'); + const [currentRouteName] = useCurrentRoute(); + if (!currentRouteName) { + throw new Error('No current route name'); + } + const router = useRoute(currentRouteName); + + const context = useRouteParameter('context'); + const [isLoggedInCloud, setIsLoggedInCloud] = useState(); useEffect(() => { @@ -32,9 +38,13 @@ const AppsPage = ({ isMarketplace }: AppsPageProps): ReactElement => { }; const handleUploadButtonClick = (): void => { - appsRoute.push({ context: 'install' }); + context && router.push({ context, page: 'install' }); }; + const handleMarketplaceTabClick = (): void => router.push({ context: 'all', page: 'list' }); + + const handleInstalledTabClick = (): void => router.push({ context: 'installed', page: 'list' }); + return ( @@ -58,21 +68,15 @@ const AppsPage = ({ isMarketplace }: AppsPageProps): ReactElement => { - marketplaceRoute.push({ context: '' })} selected={isMarketplace}> + {t('Marketplace')} - marketplaceRoute.push({ context: 'installed' })} - selected={!isMarketplace} - mbe='neg-x4' - borderWidth='0' - borderBlockWidth='x4' - > + {t('Installed')} - + ); diff --git a/apps/meteor/client/views/admin/apps/AppsPage/AppsPageContent.tsx b/apps/meteor/client/views/admin/apps/AppsPage/AppsPageContent.tsx index ea6be4c0422b4..bc34915042506 100644 --- a/apps/meteor/client/views/admin/apps/AppsPage/AppsPageContent.tsx +++ b/apps/meteor/client/views/admin/apps/AppsPage/AppsPageContent.tsx @@ -1,6 +1,6 @@ import { Pagination, Divider } from '@rocket.chat/fuselage'; import { useDebouncedState } from '@rocket.chat/fuselage-hooks'; -import { useRoute, useTranslation } from '@rocket.chat/ui-contexts'; +import { useCurrentRoute, useRoute, useRouteParameter, useTranslation } from '@rocket.chat/ui-contexts'; import React, { ReactElement, useMemo, useState } from 'react'; import { usePagination } from '../../../../components/GenericTable/hooks/usePagination'; @@ -19,14 +19,21 @@ import NoInstalledAppMatchesEmptyState from './NoInstalledAppMatchesEmptyState'; import NoInstalledAppsFoundEmptyState from './NoInstalledAppsFoundEmptyState'; import NoMarketplaceOrInstalledAppMatchesEmptyState from './NoMarketplaceOrInstalledAppMatchesEmptyState'; -const AppsPageContent = ({ isMarketplace }: { isMarketplace: boolean }): ReactElement => { +const AppsPageContent = (): ReactElement => { const t = useTranslation(); const { marketplaceApps, installedApps } = useAppsResult(); const [text, setText] = useDebouncedState('', 500); const reload = useAppsReload(); const { current, itemsPerPage, setItemsPerPage: onSetItemsPerPage, setCurrent: onSetCurrent, ...paginationProps } = usePagination(); - const marketplaceRoute = useRoute('admin-marketplace'); + const [currentRouteName] = useCurrentRoute(); + if (!currentRouteName) { + throw new Error('No current route name'); + } + const router = useRoute(currentRouteName); + + const context = useRouteParameter('context'); + const isMarketplace = context === 'all'; const [freePaidFilterStructure, setFreePaidFilterStructure] = useState({ label: t('Filter_By_Price'), @@ -85,6 +92,10 @@ const AppsPageContent = ({ isMarketplace }: { isMarketplace: boolean }): ReactEl sortFilterStructure.items.find((item) => item.checked)?.id !== 'mru' || selectedCategories.length > 0; + const handleReturn = (): void => { + router.push({ context: 'all', page: 'list' }); + }; + return ( <> marketplaceRoute.push({ context: '' })} + onButtonClick={handleReturn} /> )} - {noInstalledAppsFound && marketplaceRoute.push({ context: '' })} />} + {noInstalledAppsFound && } {appsResult.phase === AsyncStatePhase.REJECTED && } ); diff --git a/apps/meteor/client/views/admin/apps/AppsRoute.tsx b/apps/meteor/client/views/admin/apps/AppsRoute.tsx index 9480e86fd1a9f..a014a94c0cc66 100644 --- a/apps/meteor/client/views/admin/apps/AppsRoute.tsx +++ b/apps/meteor/client/views/admin/apps/AppsRoute.tsx @@ -1,5 +1,5 @@ import { useRouteParameter, useRoute, usePermission, useMethod } from '@rocket.chat/ui-contexts'; -import React, { useState, useEffect, FC } from 'react'; +import React, { useState, useEffect, ReactElement } from 'react'; import PageSkeleton from '../../../components/PageSkeleton'; import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; @@ -8,17 +8,18 @@ import AppInstallPage from './AppInstallPage'; import AppsPage from './AppsPage/AppsPage'; import AppsProvider from './AppsProvider'; -const AppsRoute: FC = () => { +const AppsRoute = (): ReactElement => { const [isLoading, setLoading] = useState(true); - const canViewAppsAndMarketplace = usePermission('manage-apps'); + const canManageApps = usePermission('manage-apps'); const isAppsEngineEnabled = useMethod('apps/is-enabled'); const appsWhatIsItRoute = useRoute('admin-apps-disabled'); + const marketplaceRoute = useRoute('admin-marketplace'); useEffect(() => { let mounted = true; const initialize = async (): Promise => { - if (!canViewAppsAndMarketplace) { + if (!canManageApps) { return; } @@ -39,15 +40,16 @@ const AppsRoute: FC = () => { return (): void => { mounted = false; }; - }, [canViewAppsAndMarketplace, isAppsEngineEnabled, appsWhatIsItRoute]); + }, [canManageApps, isAppsEngineEnabled, appsWhatIsItRoute, marketplaceRoute]); const context = useRouteParameter('context'); const isMarketplace = !context; const id = useRouteParameter('id'); + const page = useRouteParameter('page'); - if (!canViewAppsAndMarketplace) { + if (!canManageApps) { return ; } @@ -57,9 +59,9 @@ const AppsRoute: FC = () => { return ( - {((!context || context === 'installed') && ) || - (id && context === 'details' && ) || - (context === 'install' && )} + {(page === 'list' && ) || + (id && page === 'info' && ) || + (page === 'install' && )} ); }; diff --git a/apps/meteor/client/views/admin/routes.tsx b/apps/meteor/client/views/admin/routes.tsx index b00a8dfcc7b8d..dd3a08c2015d5 100644 --- a/apps/meteor/client/views/admin/routes.tsx +++ b/apps/meteor/client/views/admin/routes.tsx @@ -21,14 +21,16 @@ registerAdminRoute('/apps/what-is-it', { component: lazy(() => import('./apps/AppsWhatIsIt')), }); -registerAdminRoute('/marketplace/:context?/:id?/:version?/:tab?', { +registerAdminRoute('/marketplace/:context?/:page?/:id?/:version?/:tab?', { name: 'admin-marketplace', component: lazy(() => import('./apps/AppsRoute')), -}); - -registerAdminRoute('/apps/:context?/:id?/:version?/:tab?', { - name: 'admin-apps', - component: lazy(() => import('./apps/AppsRoute')), + triggersEnter: [ + (context, redirect): void => { + if (!context.params.context) { + redirect('/admin/marketplace/all/list'); + } + }, + ], }); registerAdminRoute('/info', { diff --git a/apps/meteor/tests/e2e/administration-menu.spec.ts b/apps/meteor/tests/e2e/administration-menu.spec.ts index 4da0bf93361fe..007846845c3de 100644 --- a/apps/meteor/tests/e2e/administration-menu.spec.ts +++ b/apps/meteor/tests/e2e/administration-menu.spec.ts @@ -35,13 +35,13 @@ test.describe.serial('administration-menu', () => { test('expect open app marketplace page', async ({ page }) => { await poHomeDiscussion.sidenav.openAdministrationByLabel('Marketplace'); - await expect(page).toHaveURL('admin/marketplace'); + await expect(page).toHaveURL('admin/marketplace/all/list'); }); test('expect open app installed page', async ({ page }) => { await poHomeDiscussion.sidenav.openAdministrationByLabel('Installed'); - await expect(page).toHaveURL('admin/marketplace/installed'); + await expect(page).toHaveURL('admin/marketplace/installed/list'); }); test.describe('user', () => { diff --git a/apps/meteor/tests/unit/client/components/AdministrationList/AppsModelList.spec.tsx b/apps/meteor/tests/unit/client/components/AdministrationList/AppsModelList.spec.tsx index 8b97900ee1a0b..a15af26cb14d4 100644 --- a/apps/meteor/tests/unit/client/components/AdministrationList/AppsModelList.spec.tsx +++ b/apps/meteor/tests/unit/client/components/AdministrationList/AppsModelList.spec.tsx @@ -67,7 +67,7 @@ describe('components/AdministrationList/AppsModelList', () => { const button = screen.getByText('Installed'); userEvent.click(button); - await waitFor(() => expect(pushRoute).to.have.been.called.with('admin-marketplace', { context: 'installed' })); + await waitFor(() => expect(pushRoute).to.have.been.called.with('admin-marketplace', { context: 'installed', page: 'list' })); await waitFor(() => expect(closeList).to.have.been.called()); }); diff --git a/yarn.lock b/yarn.lock index 700fc16cf8bb5..7e36456098cac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -23669,7 +23669,7 @@ __metadata: optional: true bin: lessc: ./bin/lessc - checksum: c9b8c0e865427112c48a9cac36f14964e130577743c29d56a6d93b5812b70846b04ccaa364acf1e8d75cee3855215ec0a2d8d9de569c80e774f10b6245f39b7d + checksum: 61568b56b5289fdcfe3d51baf3c13e7db7140022c0a37ef0ae343169f0de927a4b4f4272bc10c20101796e8ee79e934e024051321bba93b3ae071f734309bd98 languageName: node linkType: hard