Skip to content

Commit

Permalink
[NEW] Marketplace new app details page (#24711)
Browse files Browse the repository at this point in the history
* feat: ✨ Implement tab navigation on app details page

I've implemented the visual part of the new tab navigation layout for the app details page. E.G.: a tab component with 3 tab items(Details, Logs and Settings) on the app details page. Will be implementing the logical part of this next.

* refactor: ♻️ Change settings page style

I've changed the settings page max-width so that it fits better with figma.

* feat: ✨ Implement tab navigation on app details page(Logic)

I've implemented the necessary logic for the app details page tabs component to change the page content based on the selected tab. Also, I've created an AppDetailsPageHeader to improve readability in the AppDetailsPage component.

* Updated package-lock with new package.json

* fix: 🐛 Fix logs not showing bug

Fixed a typo on the AppDetailsPageHeader on which the AppMenu component was receiving a unexistent prop. Also, implemented a helper function to improve readability of the Tabs.item onClick function.

Wrong prop being sent to the AppMenu component which caused the logs not to be shown correctly

* fix: 🐛 Install necessary type dependencies

Executed a meteor npm install to update the installed package.json dependencies and solve type errors that were breaking the build tests.

* refactor: ♻️ Solve reviews

Solved the problems pointed out in the reviews on GitHub. Changed some variable names for readability, changed some conditional blocks for brevity, and added some extra measures to avoid null exceptions.

-should prefix on

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

* feat: ✨ Change visible logic for app details tab navigation

Changed the way the enabled tab flags work instead of just disabling the logs and settings tabs when an app is not installed it will not render the tabs at all.

* refactor: ♻️ Componentize app details header

Created a standalone component for the app header on the app details page called AppDetailsHeader.tsx in order to improve readability and code organization.

* feat: 🚧 Create first part of new header style

Some changes on the app header layout, added placeholder description, changed "metadata" layout, created placeholder chip for bundles and changed title size. Will be implementing the rest of the header soon.

* feat: 🚧 Create second part of new app details header style

Created the logic for the bundle's chips, integrated the app description to the header, and implemented the date formating to track the last time an app update happened

* feat: 🚧 Create third part of new app details header

Implemented the new Button + Data badge price display, implemented new install button titles based on app purchase type,  implemented new +* information in the price section of tier-based subscription apps, implemented new enabled/disabled statuses based on the data badge component, and made so that the data badge doesn't show on the AppsTable using the isAppDetailsPage flag.

* feat: ✨ Create fourth and last part of new app details page header

Implemented a tooltip component for the Bundle Chip using the PositionAnimated and Tooltip fuselage components together with the onMouseEnter and onMouseLeave react events.

* fix: 🐛 Fix non-translated strings on app details header

Added some non-translated strings to the i18n dictionary and removed a console.log.

1. Non-translated strings on the code;

* fix: 🐛 Solving more translation problems

Added the dynamic translation string to the last updated metadata

1. Non translated string on the metadata section of the app details header

* refactor: ♻️ Componentize app details header bundle chips

Created a standalone component for the bundle chips of the app details header and solved a little typo in the i18n entry for the chips text.

1. Typo in i18n dictionary

* fix: 🐛 Re-add missing translation entries to the i18n dictionary

Re-added missing entries to the i18n dictionary that were deleted during merge procedures.

* feat: ✨ Change logs from page to tab (#24952)

Changed the place of the app logs from the page to the new logs tab. Also changed some styles of the logs accordions toChanged the place of the app logs from the page to the new logs tab. Also changed some styles of the logs accordions to fit better with the new container. fit better with the new container.

* feat: ✨ Change app details page header design (#24997)

Changed the design for the page header of the app details page from a title on the left with a save and back button on the right to an back arrow icon on the left side of the title with the save button still on the right. Also changed the title of the page from App details to Back.

* Eslint

* [NEW] Description tab marketplace (#25153)

* fix: 🐛 Fix missing entry on i18n dictionary

Added a missing entry to the i18n dictionary that was lost on a branch merge.

1. Missing translation entry

* feat: ✨ Implement markdown parser on the app details page description

Implemented the MarkdownText.tsx component on the app details page description. Also changed the App class type so that it would bring the detailedDescription object and changed some of the app details page content layout to better follow the proposed design.

* Eslint

* refactor: ♻️ Change MarkdownText use to detailedDescription.rendered

Changed the method of rendering markdown on the app details page description, now it uses the dangerouslysetinnerhtml prop of the description box component.

* refactor: ♻️ Refactoring some code for reviews

Changed some booleans orders and removed an unnecessary eslint exception

* fix: 🐛 Fix missing entries to i18n dictionary

Added some missing entries to the i18n dictionary that were removed during a conflict fix.

* fix: 🐛 Re-add removed i18n dictionaries

Re add some entries that were removed on merge

* chore: fix ts

* [NEW] Carousel component app details (#25338)

* feat: 🚧 Create visual part of new slider component anchor

Implemented the visual part of the new slider anchor, will be integrating it with the API soon.

* feat: 🚧 Finish probable visual part of app details carousel

Finished what probably will be the visual part of the new app details page carousel, with mocked data, since the screenshots endpoint doesn't exist yet. Also created a preamptive function for fetching the screenshots for when the endpoint is ready for usage. Also tried a first iteration of arrow navigation on the carousel, will be finishing it soon.

* Eslint

* feat: ✨ Finish new carousel ui

Finished the new app details page carousel ui, now it counts with a hover transition on the screenshots preview image, auto passing of screenshots also on the preview, keyboard arrow navigation, and closing with the ESC key on the slider. Besides that, I've refactored some code for maintainability.

* Eslint

* feat: ✨ Integrate carousel with /screenshots endpoint

Integrated the new carousel component with the newly exposed apps/${appId}/screenshots endpoint and removed the mock data. Now only the apps with registered screenshots will show them on the component. If the app doesn't have screenshots, it shows nothing where the carousel should be.

* refactor: ♻️ Solve reviews

Refactored the way rendering works for the screenshot list inside of the ScreenshotCarousel for maintainability's sake.

* chore: fix merge

* chore: fix lint

* fix: rest externalComponents

* fix: 🐛 Solve useEndpoint typos

Solved some typos on some useEndpoint endpoints, refactored the way the screenshots are received and deleted some useless code.

Co-authored-by: dougfabris <devfabris@gmail.com>

* fix: reviews

Refactored the AppDetailsPage tab selection logic, changed some routing logic to work with it, changed lastUpdated on AppDetailsHeader to not have undefined error.

Co-authored-by: Guilherme Gazzo <guilhermegazzo@gmail.com>
Co-authored-by: dougfabris <devfabris@gmail.com>
  • Loading branch information
3 people committed May 24, 2022
1 parent 4b4e7b2 commit 7692409
Show file tree
Hide file tree
Showing 30 changed files with 703 additions and 355 deletions.
50 changes: 50 additions & 0 deletions apps/meteor/client/views/admin/apps/AppDetailsHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Box } from '@rocket.chat/fuselage';
import { useTranslation } from '@rocket.chat/ui-contexts';
import { formatDistanceStrict } from 'date-fns';
import React, { ReactElement } from 'react';

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

const AppDetailsHeader = ({ app }: { app: App }): ReactElement => {
const t = useTranslation();
const { iconFileData, name, author, version, iconFileContent, installed, modifiedAt, bundledIn, description } = app;
const lastUpdated = modifiedAt && 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;
78 changes: 55 additions & 23 deletions apps/meteor/client/views/admin/apps/AppDetailsPage.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { ISetting } from '@rocket.chat/apps-engine/definition/settings';
import { Button, ButtonGroup, Icon, Box, Throbber } from '@rocket.chat/fuselage';
import { useRoute, useCurrentRoute, useTranslation } from '@rocket.chat/ui-contexts';
import { Button, ButtonGroup, Box, Throbber, Tabs } from '@rocket.chat/fuselage';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import { useTranslation, useCurrentRoute, useRoute, useRouteParameter } from '@rocket.chat/ui-contexts';
import React, { useState, useCallback, useRef, FC } from 'react';

import { ISettingsPayload } from '../../../../app/apps/client/@types/IOrchestrator';
import { ISettings, ISettingsPayload } from '../../../../app/apps/client/@types/IOrchestrator';
import { Apps } from '../../../../app/apps/client/orchestrator';
import Page from '../../../components/Page';
import APIsDisplay from './APIsDisplay';
import AppDetailsHeader from './AppDetailsHeader';
import AppDetailsPageContent from './AppDetailsPageContent';
import AppLogsPage from './AppLogsPage';
import LoadingDetails from './LoadingDetails';
import SettingsDisplay from './SettingsDisplay';
import { handleAPIError } from './helpers';
Expand All @@ -18,59 +21,88 @@ const AppDetailsPage: FC<{ id: string }> = function AppDetailsPage({ id }) {

const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
const [isSaving, setIsSaving] = useState(false);

const settingsRef = useRef<Record<string, ISetting['value']>>({});
const appData = useAppInfo(id);

const data = useAppInfo(id);
const [, urlParams] = useCurrentRoute();
const appsRoute = useRoute('admin-apps');
const tab = useRouteParameter('tab');

const [currentRouteName] = useCurrentRoute();
if (!currentRouteName) {
throw new Error('No current route name');
}
const router = useRoute(currentRouteName);
const handleReturn = (): void => router.push({});

const { settings, apis } = { settings: {}, apis: [], ...data };
const router = useRoute(currentRouteName);
const handleReturn = useMutableCallback((): void => router.push({}));

const showSettings = Object.values(settings).length;
const showApis = apis.length;
const { installed, settings, apis } = appData || {};
const showApis = apis?.length;

const saveAppSettings = useCallback(async () => {
const { current } = settingsRef;
setIsSaving(true);
try {
await Apps.setAppSettings(
id,
(Object.values(settings) as ISetting[]).map((value) => ({ ...value, value: current?.[value.id] })) as unknown as ISettingsPayload,
(Object.values(settings || {}) as ISetting[]).map((value) => ({
...value,
value: current?.[value.id],
})) as unknown as ISettingsPayload,
);
} catch (e) {
handleAPIError(e);
}
setIsSaving(false);
}, [id, settings]);

const handleTabClick = (tab: 'details' | 'logs' | 'settings'): void => {
appsRoute.replace({ ...urlParams, tab });
};

return (
<Page flexDirection='column'>
<Page.Header title={t('App_Details')}>
<Page.Header title={t('App_Info')} onClickBack={handleReturn}>
<ButtonGroup>
<Button primary disabled={!hasUnsavedChanges || isSaving} onClick={saveAppSettings}>
{!isSaving && t('Save_changes')}
{isSaving && <Throbber inheritColor />}
</Button>
<Button onClick={handleReturn}>
<Icon name='back' />
{t('Back')}
</Button>
</ButtonGroup>
</Page.Header>
<Page.ScrollableContentWithShadow>
<Box maxWidth='x600' w='full' alignSelf='center'>
{!data && <LoadingDetails />}
{data && (
<Page.ScrollableContentWithShadow padding='x24'>
<Box w='full' alignSelf='center'>
{!appData && <LoadingDetails />}
{appData && (
<>
<AppDetailsPageContent app={data} />
{!!showApis && <APIsDisplay apis={apis} />}
{!!showSettings && (
<SettingsDisplay settings={settings} setHasUnsavedChanges={setHasUnsavedChanges} settingsRef={settingsRef} />
<AppDetailsHeader app={appData} />

<Tabs mis='-x24' mb='x36'>
<Tabs.Item onClick={(): void => handleTabClick('details')} selected={!tab || tab === 'details'}>
{t('Details')}
</Tabs.Item>
{Boolean(installed) && (
<Tabs.Item onClick={(): void => handleTabClick('logs')} selected={tab === 'logs'}>
{t('Logs')}
</Tabs.Item>
)}
{Boolean(installed && settings && Object.values(settings).length) && (
<Tabs.Item onClick={(): void => handleTabClick('settings')} selected={tab === 'settings'}>
{t('Settings')}
</Tabs.Item>
)}
</Tabs>

{Boolean(!tab || tab === 'details') && <AppDetailsPageContent app={appData} />}
{Boolean((!tab || tab === 'details') && !!showApis) && <APIsDisplay apis={apis || []} />}
{tab === 'logs' && <AppLogsPage id={id} />}
{Boolean(tab === 'settings' && settings && Object.values(settings).length) && (
<SettingsDisplay
settings={settings || ({} as ISettings)}
setHasUnsavedChanges={setHasUnsavedChanges}
settingsRef={settingsRef}
/>
)}
</>
)}
Expand Down
160 changes: 54 additions & 106 deletions apps/meteor/client/views/admin/apps/AppDetailsPageContent.tsx
Original file line number Diff line number Diff line change
@@ -1,62 +1,31 @@
/* eslint-disable @typescript-eslint/no-use-before-define */
import { Box, Callout, Chip, Divider, Margins } from '@rocket.chat/fuselage';
import { Box, Callout, Chip, Margins } from '@rocket.chat/fuselage';
import { TranslationKey, useTranslation } from '@rocket.chat/ui-contexts';
import React, { FC } from 'react';

import ExternalLink from '../../../components/ExternalLink';
import AppAvatar from '../../../components/avatar/AppAvatar';
import AppMenu from './AppMenu';
import AppStatus from './AppStatus';
import PriceDisplay from './PriceDisplay';
import { App } from './types';
import ScreenshotCarouselAnchor from './components/ScreenshotCarouselAnchor';
import { AppInfo } from './definitions/AppInfo';

type AppDetailsPageContentProps = {
app: App;
app: AppInfo;
};

const AppDetailsPageContent: FC<AppDetailsPageContentProps> = ({ app }) => {
const t = useTranslation();

const {
iconFileData = '',
name,
author: { name: authorName, homepage, support },
author: { homepage, support },
detailedDescription,
description,
categories = [],
version,
price,
purchaseType,
pricingPlans,
iconFileContent,
installed,
bundledIn,
screenshots,
} = app;

return (
<>
<Box display='flex' flexDirection='row' mbe='x20' w='full'>
<AppAvatar size='x124' mie='x20' iconFileContent={iconFileContent} iconFileData={iconFileData} />
<Box display='flex' flexDirection='column' justifyContent='space-between' flexGrow={1}>
<Box fontScale='h2'>{name}</Box>
<Box display='flex' flexDirection='row' color='hint' alignItems='center'>
<Box fontScale='p2m' mie='x4'>
{t('By_author', { author: authorName })}
</Box>
|<Box mis='x4'>{t('Version_version', { version })}</Box>
</Box>
<Box display='flex' flexDirection='row' alignItems='center' justifyContent='space-between'>
<Box flexGrow={1} display='flex' flexDirection='row' alignItems='center' marginInline='neg-x8'>
<AppStatus app={app} marginInline='x8' />
{!installed && (
<PriceDisplay purchaseType={purchaseType} pricingPlans={pricingPlans} price={price} showType={false} marginInline='x8' />
)}
</Box>
{installed && <AppMenu app={app} />}
</Box>
</Box>
</Box>
<Divider />
const t = useTranslation();

const isMarkdown = detailedDescription && Object.keys(detailedDescription).length !== 0 && detailedDescription.rendered;
const isCarouselVisible = screenshots && Boolean(screenshots.length);

return (
<Box maxWidth='x640' w='full' marginInline='auto'>
{app.licenseValidation && (
<>
{Object.entries(app.licenseValidation.warnings).map(([key]) => (
Expand All @@ -74,77 +43,56 @@ const AppDetailsPageContent: FC<AppDetailsPageContentProps> = ({ app }) => {
)}

<Box display='flex' flexDirection='column'>
<Margins block='x12'>
<Box fontScale='h4'>{t('Categories')}</Box>
<Box display='flex' flexDirection='row'>
{categories?.map((current) => (
<Chip key={current} textTransform='uppercase' mie='x8'>
<Box color='hint'>{current}</Box>
</Chip>
))}
<Margins block='x17'>
{isCarouselVisible && <ScreenshotCarouselAnchor screenshots={screenshots} />}

<Box is='section'>
<Box fontScale='h4' mbe='x8'>
{t('Description')}
</Box>
<Box
display='flex'
flexDirection='row'
mbe='neg-x16'
dangerouslySetInnerHTML={{ __html: isMarkdown ? detailedDescription.rendered : description }}
/>
</Box>

<Box fontScale='h4'>{t('Contact')}</Box>
<Box display='flex' flexDirection='row' flexGrow={1} justifyContent='space-around' flexWrap='wrap'>
<Box display='flex' flexDirection='column' mie='x12' flexGrow={1}>
<Box fontScale='h4' color='hint'>
{t('Author_Site')}
</Box>
<ExternalLink to={homepage} />
<Box is='section'>
<Box fontScale='h4' mbe='x8'>
{t('Categories')}
</Box>
<Box display='flex' flexDirection='column' flexGrow={1}>
<Box fontScale='h4' color='hint'>
{t('Support')}
</Box>
<ExternalLink to={support} />
<Box display='flex' flexDirection='row'>
{categories?.map((current) => (
<Chip key={current} textTransform='uppercase' mie='x8'>
<Box color='hint'>{current}</Box>
</Chip>
))}
</Box>
</Box>

<Box fontScale='h4'>{t('Details')}</Box>
<Box display='flex' flexDirection='row'>
{description}
<Box is='section'>
<Box fontScale='h4' mbe='x8'>
{t('Contact')}
</Box>
<Box display='flex' flexDirection='row' flexGrow={1} justifyContent='space-around' flexWrap='wrap'>
<Box display='flex' flexDirection='column' mie='x12' flexGrow={1}>
<Box fontScale='h4' color='hint'>
{t('Author_Site')}
</Box>
<ExternalLink to={homepage} />
</Box>
<Box display='flex' flexDirection='column' flexGrow={1}>
<Box fontScale='h4' color='hint'>
{t('Support')}
</Box>
<ExternalLink to={support} />
</Box>
</Box>
</Box>
</Margins>
</Box>
{bundledIn && (
<>
<Divider />
<Box display='flex' flexDirection='column'>
<Margins block='x12'>
<Box fontScale='h4'>{t('Bundles')}</Box>
{bundledIn.map((bundle) => (
<Box key={bundle.bundleId} display='flex' flexDirection='row' alignItems='center'>
<Box
width='x80'
height='x80'
display='flex'
flexDirection='row'
justifyContent='space-around'
flexWrap='wrap'
flexShrink={0}
>
{bundle.apps.map((app) => (
<AppAvatar
size='x36'
key={app.latest.name}
iconFileContent={app.latest.iconFileContent}
iconFileData={app.latest.iconFileData}
/>
))}
</Box>
<Box display='flex' flexDirection='column' mis='x12'>
<Box fontScale='p2m'>{bundle.bundleName}</Box>
{bundle.apps.map((app) => (
<Box key={app.latest.name}>{app.latest.name},</Box>
))}
</Box>
</Box>
))}
</Margins>
</Box>
</>
)}
</>
</Box>
);
};

Expand Down
Loading

0 comments on commit 7692409

Please sign in to comment.