From f2af7ec25d9acd67bf625947a4cf10e0d1d04d9f Mon Sep 17 00:00:00 2001 From: Gabriel Henriques Date: Wed, 20 May 2020 18:31:46 -0300 Subject: [PATCH] Wip 4 --- client/admin/apps/AppDetailsPage.js | 117 ++++++++++++++ client/admin/apps/AppMenu.js | 10 ++ client/admin/apps/AppStatus.js | 87 ++++++++++ client/admin/apps/AppsRoute.js | 11 +- client/admin/apps/CloudLoginModal.js | 29 ++++ client/admin/apps/IframeModal.js | 29 ++++ client/admin/apps/MarketplacePage.js | 29 +--- client/admin/apps/MarketplaceTable.js | 134 ++++++++-------- client/admin/apps/PriceDisplay.js | 31 ++++ client/admin/apps/helpers.js | 144 +++++++++++++++++ client/admin/apps/hooks/useAppInfo.js | 96 +++++++++++ client/admin/apps/hooks/useMarketplaceApps.js | 122 ++++++++++++++ client/admin/apps/hooks/useMenuOptions.js | 151 ++++++++++++++++++ client/admin/routes.js | 2 +- client/hooks/useResizeInlineBreakpoint.js | 9 ++ 15 files changed, 912 insertions(+), 89 deletions(-) create mode 100644 client/admin/apps/AppDetailsPage.js create mode 100644 client/admin/apps/AppMenu.js create mode 100644 client/admin/apps/AppStatus.js create mode 100644 client/admin/apps/CloudLoginModal.js create mode 100644 client/admin/apps/IframeModal.js create mode 100644 client/admin/apps/PriceDisplay.js create mode 100644 client/admin/apps/helpers.js create mode 100644 client/admin/apps/hooks/useAppInfo.js create mode 100644 client/admin/apps/hooks/useMarketplaceApps.js create mode 100644 client/admin/apps/hooks/useMenuOptions.js create mode 100644 client/hooks/useResizeInlineBreakpoint.js diff --git a/client/admin/apps/AppDetailsPage.js b/client/admin/apps/AppDetailsPage.js new file mode 100644 index 000000000000..572dd5f24059 --- /dev/null +++ b/client/admin/apps/AppDetailsPage.js @@ -0,0 +1,117 @@ +import React, { useState, useCallback } from 'react'; +import { Button, ButtonGroup, Icon, Avatar, Box, Divider, Chip, Margins } from '@rocket.chat/fuselage'; + +import Page from '../../components/basic/Page'; +import { useRoute } from '../../contexts/RouterContext'; +import { useMethod } from '../../contexts/ServerContext'; +import PriceDisplay from './PriceDisplay'; +import { AppStatus } from './AppStatus'; +import { useTranslation } from '../../contexts/TranslationContext'; +import { useAppInfo } from './hooks/useAppInfo'; +import { AppMenu } from './AppMenu'; + +const objectFit = { objectFit: 'contain' }; + +export default function AppDetailsPage({ id }) { + const t = useTranslation(); + const data = useAppInfo(id); + const [modal, setModal] = useState(null); + + const router = useRoute('admin-apps'); + const handleReturn = useCallback(() => router.push({})); + + const { + iconFileData = '', + name, + author: { name: authorName, homepage, support } = {}, + description, + categories = [], + version, + price, + purchaseType, + pricingPlans, + iconFileContent, + installed, + bundledIn, + } = data; + + const getLoggedInCloud = useMethod('cloud:checkUserLoggedIn'); + const isLoggedIn = getLoggedInCloud(); + + return <> + + + + + + + + + + + {name} + + {`${ t('By') } ${ authorName }`} + | + {`${ t('Version') } ${ version }`} + + + + + {!installed && } + + {installed && } + + + + + + + + {t('Categories')} + + {categories && categories.map((current) => {current})} + + + {t('Contact')} + + + {t('Author_Site')} + {homepage} + + + {t('Support')} + {support} + + + + {t('Details')} + {description} + + + + {bundledIn && <> + + + + {t('Bundles')} + {bundledIn.map((bundle) => + + {bundle.apps.map((app) => )} + + + {bundle.bundleName} + {bundle.apps.map((app) => {app.latest.name},)} + + )} + + + } + + {modal}; +} diff --git a/client/admin/apps/AppMenu.js b/client/admin/apps/AppMenu.js new file mode 100644 index 000000000000..745d0c0cdf3f --- /dev/null +++ b/client/admin/apps/AppMenu.js @@ -0,0 +1,10 @@ +import React from 'react'; +import { Menu } from '@rocket.chat/fuselage'; + +import { useMenuOptions } from './hooks/useMenuOptions'; + +export const AppMenu = ({ app, setModal, isLoggedIn, ...props }) => { + const menuOptions = useMenuOptions({ app, setModal, isLoggedIn }); + + return ; +}; diff --git a/client/admin/apps/AppStatus.js b/client/admin/apps/AppStatus.js new file mode 100644 index 000000000000..4ea476ce1353 --- /dev/null +++ b/client/admin/apps/AppStatus.js @@ -0,0 +1,87 @@ +import React, { useCallback, useState } from 'react'; +import { Box, Button, Icon, Throbber } from '@rocket.chat/fuselage'; + +import { useTranslation } from '../../contexts/TranslationContext'; +import { appButtonProps, appStatusSpanProps, handleAPIError, warnStatusChange } from './helpers'; +import { Apps } from '../../../app/apps/client/orchestrator'; +import { IframeModal } from './IframeModal'; +import { CloudLoginModal } from './CloudLoginModal'; + +const installApp = async ({ id, name, version }) => { + try { + const { status } = await Apps.installApp(id, version); + warnStatusChange(name, status); + } catch (error) { + handleAPIError(error); + } +}; + +const actions = { + purchase: installApp, + install: installApp, + update: async ({ id, name, version }) => { + try { + const { status } = await Apps.updateApp(id, version); + warnStatusChange(name, status); + } catch (error) { + handleAPIError(error); + } + }, +}; + +export const AppStatus = React.memo(({ app, setModal, isLoggedIn, ...props }) => { + const t = useTranslation(); + const [loading, setLoading] = useState(); + + const button = appButtonProps(app); + const status = !button && appStatusSpanProps(app); + + const { id } = app; + + const confirmAction = () => { + setModal(null); + + actions[button.action](app).then(() => { + setLoading(false); + }); + }; + + const cancelAction = () => { + setModal(null); + setLoading(false); + }; + + const openModal = async () => { + try { + const data = await Apps.buildExternalUrl(app.id, app.purchaseType, false); + + setModal(() => ); + } catch (error) { + handleAPIError(error); + } + }; + + const handleClick = useCallback((e) => { + e.preventDefault(); + e.stopPropagation(); + if (isLoggedIn) { + setLoading(true); + + button.action === 'purchase' ? openModal() : confirmAction(); + return; + } + setModal( setModal(null)} />); + }, [id, isLoggedIn, button && button.action]); + + return + {button && } + {status && + + {t(status.label)} + } + ; +}); diff --git a/client/admin/apps/AppsRoute.js b/client/admin/apps/AppsRoute.js index 1fe4b493d82b..7b5bd1cfb6bf 100644 --- a/client/admin/apps/AppsRoute.js +++ b/client/admin/apps/AppsRoute.js @@ -2,9 +2,11 @@ import React, { useState, useEffect } from 'react'; import { Box } from '@rocket.chat/fuselage'; import { Apps } from '../../../app/apps/client/orchestrator'; +import { useRouteParameter } from '../../contexts/RouterContext'; import { usePermission } from '../../contexts/AuthorizationContext'; import { useTranslation } from '../../contexts/TranslationContext'; import NotAuthorizedPage from '../NotAuthorizedPage'; +import AppDetailsPage from './AppDetailsPage'; import MarketplacePage from './MarketplacePage'; export default function AppsRoute() { @@ -13,6 +15,10 @@ export default function AppsRoute() { const canViewAppsAndMarketplace = usePermission('manage-apps'); const [isEnabled, setEnabled] = useState(false); + const context = useRouteParameter('context'); + const id = useRouteParameter('id'); + const version = useRouteParameter('version'); + useEffect(() => { (async () => setEnabled(await Apps.isEnabled()))(); }, []); @@ -25,5 +31,8 @@ export default function AppsRoute() { return {t('Apps_disabled')}; } - return ; + return <> + {!context && } + {context === 'details' && } + ; } diff --git a/client/admin/apps/CloudLoginModal.js b/client/admin/apps/CloudLoginModal.js new file mode 100644 index 000000000000..0effb2f61f88 --- /dev/null +++ b/client/admin/apps/CloudLoginModal.js @@ -0,0 +1,29 @@ + +import React from 'react'; +import { Icon, Button, ButtonGroup } from '@rocket.chat/fuselage'; + +import { useTranslation } from '../../contexts/TranslationContext'; +import { Modal } from '../../components/basic/Modal'; +import { useRoute } from '../../contexts/RouterContext'; + +export const CloudLoginModal = ({ cancel, ...props }) => { + const t = useTranslation(); + const router = useRoute('cloud'); + + return + + + {t('Apps_Marketplace_Login_Required_Title')} + + + + {t('Apps_Marketplace_Login_Required_Description')} + + + + + + + + ; +}; diff --git a/client/admin/apps/IframeModal.js b/client/admin/apps/IframeModal.js new file mode 100644 index 000000000000..bd5cce284012 --- /dev/null +++ b/client/admin/apps/IframeModal.js @@ -0,0 +1,29 @@ +import React, { useEffect } from 'react'; +import { Box } from '@rocket.chat/fuselage'; + +import { Modal } from '../../components/basic/Modal'; + +const iframeMsgListener = (confirm, cancel) => (e) => { + let data; + try { + data = JSON.parse(e.data); + } catch (e) { + return; + } + + data.result ? confirm(data) : cancel(); +}; + +export const IframeModal = ({ url, confirm, cancel, ...props }) => { + useEffect(() => { + const listener = iframeMsgListener(confirm, cancel); + window.addEventListener('message', listener); + + return () => window.removeEventListener('message', listener); + }, []); + return + +