Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[NEW] Create marketplace app details page new header #24880

56 changes: 56 additions & 0 deletions client/views/admin/apps/AppDetailsHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { Box } from '@rocket.chat/fuselage';
import { formatDistanceStrict } from 'date-fns';
import React, { FC } from 'react';

import AppAvatar from '../../../components/avatar/AppAvatar';
import { useTranslation } from '../../../contexts/TranslationContext';
import AppMenu from './AppMenu';
import AppStatus from './AppStatus';
import BundleChips from './BundleChips';
import { App } from './types';

type AppDetailsPageHeaderProps = {
app: App;
};

const AppDetailsHeader: FC<AppDetailsPageHeaderProps> = ({ app }) => {
const t = useTranslation();

const { iconFileData = '', name, author, version, iconFileContent, installed, modifiedAt, bundledIn, description } = app;

const lastUpdated = formatDistanceStrict(new Date(modifiedAt), new Date(), { addSuffix: false });

return (
<Box display='flex' flexDirection='row' mbe='x20' w='full'>
<AppAvatar size='x124' mie='x20' iconFileContent={iconFileContent} iconFileData={iconFileData} />
<Box display='flex' flexDirection='column'>
<Box display='flex' flexDirection='row' alignItems='center' mbe='x8'>
<Box fontScale='h1' mie='x8'>
{name}
</Box>
{bundledIn && Boolean(bundledIn.length) && <BundleChips bundledIn={bundledIn} />}
</Box>
<Box mbe='x16'>{description}</Box>
<Box display='flex' flexDirection='row' alignItems='center' mbe='x16'>
<Box display='flex' flexDirection='row' alignItems='center'>
<AppStatus app={app} installed={installed} isAppDetailsPage={true} mie='x8' />
</Box>
{installed && <AppMenu app={app} />}
</Box>
<Box display='flex' flexDirection='row' color='hint' alignItems='center'>
<Box fontScale='p2m' mie='x16'>
{t('By_author', { author: author?.name })}
</Box>
| <Box mi='x16'>{t('Version_version', { version })}</Box> |{' '}
<Box mis='x16'>
{t('Marketplace_app_last_updated', {
lastUpdated,
})}
</Box>
</Box>
</Box>
</Box>
);
};

export default AppDetailsHeader;
32 changes: 15 additions & 17 deletions client/views/admin/apps/AppDetailsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import Page from '../../../components/Page';
import { useRoute, useCurrentRoute } from '../../../contexts/RouterContext';
import { useTranslation } from '../../../contexts/TranslationContext';
import APIsDisplay from './APIsDisplay';
import AppDetailsHeader from './AppDetailsHeader';
import AppDetailsPageContent from './AppDetailsPageContent';
import AppDetailsPageHeader from './AppDetailsPageHeader';
import LoadingDetails from './LoadingDetails';
import SettingsDisplay from './SettingsDisplay';
import { handleAPIError } from './helpers';
Expand Down Expand Up @@ -39,6 +39,7 @@ const AppDetailsPage: FC<{ id: string }> = function AppDetailsPage({ id }) {
const { settings, apis } = { settings: {}, apis: [], ...data };

const showApis = apis.length;
const { installed } = data || {};

const saveAppSettings = useCallback(async () => {
const { current } = settingsRef;
Expand Down Expand Up @@ -82,17 +83,14 @@ const AppDetailsPage: FC<{ id: string }> = function AppDetailsPage({ id }) {

const { isSelectedDetails, isSelectedLogs, isSelectedSettings } = selectedTab;

const shouldRunEventFunction = (flag: boolean, callback: MouseEventHandler<HTMLElement>): MouseEventHandler<HTMLElement> | undefined => {
if (flag) return callback;
};

const isSettingsTabSelected = Boolean(settings && Object.values(settings).length && isSelectedSettings);
const isSettingsTabEnabled = Boolean(settings && Object.values(settings).length);
const isSettingsTabEnabled = Boolean(settings && Object.values(settings).length && installed);

const isDetailsTabSelected = isSelectedDetails;
const areApisVisible = Boolean(isSelectedDetails && !!showApis);

const isLogsTabSelected = isSelectedLogs;
const isLogsTabEnabled = Boolean(installed);

return (
<Page flexDirection='column'>
Expand All @@ -113,22 +111,22 @@ const AppDetailsPage: FC<{ id: string }> = function AppDetailsPage({ id }) {
{!data && <LoadingDetails />}
{data && (
<>
<AppDetailsPageHeader app={data} />
<AppDetailsHeader app={data} />

<Tabs mis='-x24' mb='x36'>
<Tabs.Item onClick={selectTab} selected={isSelectedDetails}>
{t('Details')}
</Tabs.Item>
<Tabs.Item onClick={selectTab} selected={isSelectedLogs}>
{t('Logs')}
</Tabs.Item>
<Tabs.Item
onClick={shouldRunEventFunction(isSettingsTabEnabled, selectTab)}
selected={isSelectedSettings}
disabled={!isSettingsTabEnabled}
>
{t('Settings')}
</Tabs.Item>
{isLogsTabEnabled && (
<Tabs.Item onClick={selectTab} selected={isSelectedLogs}>
{t('Logs')}
</Tabs.Item>
)}
{isSettingsTabEnabled && (
<Tabs.Item onClick={selectTab} selected={isSelectedSettings} disabled={!isSettingsTabEnabled}>
{t('Settings')}
</Tabs.Item>
)}
</Tabs>

{isDetailsTabSelected && <AppDetailsPageContent app={data} />}
Expand Down
46 changes: 0 additions & 46 deletions client/views/admin/apps/AppDetailsPageHeader.tsx

This file was deleted.

2 changes: 1 addition & 1 deletion client/views/admin/apps/AppRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ const AppRow: FC<AppRowProps> = ({ medium, ...props }) => {
)}
<Table.Cell withTruncatedText>
<Box display='flex' flexDirection='row' alignItems='center' marginInline='neg-x8' onClick={preventClickPropagation}>
<AppStatus app={props} showStatus={isStatusVisible} marginInline='x8' />
<AppStatus app={props} showStatus={isStatusVisible} isAppDetailsPage={false} marginInline='x8' />
{installed && <AppMenu app={props} invisible={!isStatusVisible} marginInline='x8' />}
</Box>
</Table.Cell>
Expand Down
48 changes: 37 additions & 11 deletions client/views/admin/apps/AppStatus.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Box, Button, Icon, Throbber } from '@rocket.chat/fuselage';
import { useSafely } from '@rocket.chat/fuselage-hooks';
import colors from '@rocket.chat/fuselage-tokens/colors.json';
import React, { useCallback, useState, memo } from 'react';

import { Apps } from '../../../../app/apps/client/orchestrator';
Expand All @@ -9,6 +10,7 @@ import { useTranslation } from '../../../contexts/TranslationContext';
import AppPermissionsReviewModal from './AppPermissionsReviewModal';
import CloudLoginModal from './CloudLoginModal';
import IframeModal from './IframeModal';
import PriceDisplay from './PriceDisplay';
import { appButtonProps, appStatusSpanProps, handleAPIError, warnStatusChange, handleInstallError } from './helpers';

const installApp = async ({ id, name, version, permissionsGranted }) => {
Expand All @@ -33,12 +35,14 @@ const actions = {
},
};

const AppStatus = ({ app, showStatus = true, ...props }) => {
const AppStatus = ({ app, showStatus = true, isAppDetailsPage, installed = false, ...props }) => {
const t = useTranslation();
const [loading, setLoading] = useSafely(useState());
const [isAppPurchased, setPurchased] = useSafely(useState(app?.isPurchased));
const setModal = useSetModal();

const { price, purchaseType, pricingPlans } = app;

const button = appButtonProps(app || {});
const status = !button && appStatusSpanProps(app);

Expand Down Expand Up @@ -104,22 +108,44 @@ const AppStatus = ({ app, showStatus = true, ...props }) => {
showAppPermissionsReviewModal();
};

const AppStatusStyle = {
bg: status.label === 'Disabled' ? colors.y100 : colors.b100,
color: status.label === 'Disabled' ? colors.y800 : 'primary-500',
};

return (
<Box {...props}>
{button && (
<Button primary disabled={loading} invisible={!showStatus && !loading} minHeight='x40' onClick={handleClick}>
{loading ? (
<Throbber inheritColor />
) : (
<>
{button.icon && <Icon name={button.icon} />}
{t(button.label)}
</>
<Box
bg={colors.b100}
display='flex'
flexDirection='row'
alignItems='center'
justifyContent='center'
borderRadius='x2'
invisible={!showStatus && !loading}
>
<Button primary disabled={loading} minHeight='x40' onClick={handleClick}>
{loading ? (
<Throbber inheritColor />
) : (
<>
{button.icon && <Icon name={button.icon} />}
{t(button.label)}
</>
)}
</Button>
{isAppDetailsPage && (
<Box pi='x14' color='primary-500'>
{!installed && (
<PriceDisplay purchaseType={purchaseType} pricingPlans={pricingPlans} price={price} showType={false} marginInline='x8' />
)}
</Box>
)}
</Button>
</Box>
)}
{status && (
<Box color={status.label === 'Disabled' ? 'warning' : 'hint'} display='flex' alignItems='center'>
<Box display='flex' alignItems='center' pi='x14' pb='x8' bg={AppStatusStyle.bg} color={AppStatusStyle.color}>
<Icon size='x20' name={status.icon} mie='x4' />
{t(status.label)}
</Box>
Expand Down
63 changes: 63 additions & 0 deletions client/views/admin/apps/BundleChips.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { Box, Icon, PositionAnimated, AnimatedVisibility, Tooltip } from '@rocket.chat/fuselage';
import React, { RefObject, useRef, useState, ReactElement } from 'react';

import { useTranslation } from '../../../contexts/TranslationContext';
import { App } from './types';

type BundleChipsProps = {
bundledIn: {
bundleId: string;
bundleName: string;
apps: App[];
}[];
};

const BundleChips = ({ bundledIn }: BundleChipsProps): ReactElement => {
const t = useTranslation();

const bundleRef = useRef<Element>();
const [isHovered, setIsHovered] = useState(false);

return (
<>
{bundledIn.map((bundle) => (
<>
<Box
display='flex'
flexDirection='row'
alignItems='center'
justifyContent='center'
backgroundColor='disabled'
pi='x4'
height='x20'
borderRadius='x2'
ref={bundleRef}
onMouseEnter={(): void => setIsHovered(true)}
onMouseLeave={(): void => setIsHovered(false)}
>
<Icon name='bag' size='x20' />
<Box fontWeight='700' fontSize='x12' color='info'>
{t('bundle_chip_title', {
bundleName: bundle.bundleName,
})}
</Box>
</Box>
<PositionAnimated
anchor={bundleRef as RefObject<Element>}
placement='top-middle'
margin={8}
visible={isHovered ? AnimatedVisibility.VISIBLE : AnimatedVisibility.HIDDEN}
>
<Tooltip>
{t('this_app_is_included_with_subscription', {
bundleName: bundle.bundleName,
})}
</Tooltip>
</PositionAnimated>
</>
))}
</>
);
};

export default BundleChips;
2 changes: 1 addition & 1 deletion client/views/admin/apps/MarketplaceRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ const MarketplaceRow: FC<MarketplaceRowProps> = ({ medium, large, ...props }) =>
)}
<Table.Cell withTruncatedText>
<Box display='flex' flexDirection='row' alignItems='center' marginInline='neg-x8' onClick={preventClickPropagation}>
<AppStatus app={props} showStatus={isStatusVisible} marginInline='x8' />
<AppStatus app={props} showStatus={isStatusVisible} isAppDetailsPage={false} marginInline='x8' />
{installed && <AppMenu app={props} invisible={!isStatusVisible} marginInline='x8' />}
</Box>
</Table.Cell>
Expand Down
5 changes: 2 additions & 3 deletions client/views/admin/apps/PriceDisplay.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const formatPriceAndPurchaseType = (purchaseType, pricingPlans, price) => {
if (!pricingPlans || !Array.isArray(pricingPlans) || pricingPlans.length === 0) {
return { type, price: '-' };
}

return { type, price: formatPricingPlan(pricingPlans[0]) };
}

Expand All @@ -34,9 +35,7 @@ function PriceDisplay({ purchaseType, pricingPlans, price, showType = true, ...p
{t(type)}
</Box>
)}
<Box color='hint' withTruncatedText>
{!showType && type === 'Free' ? t(type) : formattedPrice}
</Box>
<Box withTruncatedText>{!showType && type === 'Free' ? t(type) : formattedPrice}</Box>
</Box>
);
}
Expand Down
Loading