Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
438f26e
refactor: :recycle: Move app list install logic to app menu
rique223 Oct 11, 2022
07bbe96
Solve app menu subscription option bug
rique223 Oct 11, 2022
3dd739e
Solve LGTM always true boolean
rique223 Oct 13, 2022
a81e7ed
Merge remote-tracking branch 'origin' into improve/marketplace-apps-b…
rique223 Oct 13, 2022
7446e19
Merge branch 'develop' into improve/marketplace-apps-buttons
rique223 Oct 14, 2022
5e1143d
Lint
rique223 Oct 15, 2022
57f9d1a
Merge branch 'improve/marketplace-apps-buttons' of github.com:RocketC…
rique223 Oct 15, 2022
845e6f3
Lint
rique223 Oct 15, 2022
1141014
Merge branch 'develop' into improve/marketplace-apps-buttons
rique223 Oct 15, 2022
f25e6e1
Merge remote-tracking branch 'origin' into improve/marketplace-apps-b…
rique223 Nov 1, 2022
a8aa818
Merge branch 'develop' into improve/marketplace-apps-buttons
rique223 Nov 1, 2022
dd93dab
Merge branch 'develop' into improve/marketplace-apps-buttons
rique223 Nov 2, 2022
04478a6
Merge remote-tracking branch 'origin' into improve/marketplace-apps-b…
rique223 Nov 7, 2022
4fca927
Fix AppReviewModal confirm/cancel prop function names
rique223 Nov 7, 2022
11c341d
refactor: :recycle: Unify duplicate of InstallApp and actions
rique223 Nov 8, 2022
03437a7
Solve untyped error in helpers.ts
rique223 Nov 8, 2022
7c5c1c7
Merge branch 'develop' into improve/marketplace-apps-buttons
kodiakhq[bot] Nov 10, 2022
1b23325
Merge branch 'develop' into improve/marketplace-apps-buttons
kodiakhq[bot] Nov 11, 2022
7c8f39c
Merge branch 'develop' into improve/marketplace-apps-buttons
kodiakhq[bot] Nov 11, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,10 @@ import { Apps } from '../../../../../../../app/apps/client/orchestrator';
import AppPermissionsReviewModal from '../../../AppPermissionsReviewModal';
import CloudLoginModal from '../../../CloudLoginModal';
import IframeModal from '../../../IframeModal';
import { appButtonProps, appStatusSpanProps, handleAPIError, warnStatusChange, handleInstallError } from '../../../helpers';
import { appButtonProps, appStatusSpanProps, handleAPIError, handleInstallError } from '../../../helpers';
import { marketplaceActions } from '../../../helpers/marketplaceActions';
import AppStatusPriceDisplay from './AppStatusPriceDisplay';

const installApp = async ({ id, name, version, permissionsGranted }) => {
try {
const { status } = await Apps.installApp(id, version, permissionsGranted);
warnStatusChange(name, status);
} catch (error) {
handleAPIError(error);
}
};

const actions = {
purchase: installApp,
install: installApp,
update: async ({ id, name, marketplaceVersion, permissionsGranted }) => {
try {
const { status } = await Apps.updateApp(id, marketplaceVersion, permissionsGranted);
warnStatusChange(name, status);
} catch (error) {
handleAPIError(error);
}
},
};

const AppStatus = ({ app, showStatus = true, isAppDetailsPage, isSubscribed, installed, ...props }) => {
const t = useTranslation();
const [loading, setLoading] = useSafely(useState());
Expand All @@ -48,7 +27,7 @@ const AppStatus = ({ app, showStatus = true, isAppDetailsPage, isSubscribed, ins
(permissionsGranted) => {
setModal(null);

actions[action]({ ...app, permissionsGranted }).then(() => {
marketplaceActions[action]({ ...app, permissionsGranted }).then(() => {
setLoading(false);
});
},
Expand Down Expand Up @@ -109,7 +88,7 @@ const AppStatus = ({ app, showStatus = true, isAppDetailsPage, isSubscribed, ins

return (
<Box {...props}>
{button && (
{button && isAppDetailsPage && (
<Box
display='flex'
flexDirection='row'
Expand Down Expand Up @@ -147,9 +126,7 @@ const AppStatus = ({ app, showStatus = true, isAppDetailsPage, isSubscribed, ins
)}
{status && (
<>
<Tag medium variant={status.label === 'Disabled' ? 'secondary-danger' : ''}>
{status.label}
</Tag>
<Tag variant={status.label === 'Disabled' ? 'secondary-danger' : ''}>{status.label}</Tag>
</>
)}
</Box>
Expand Down
161 changes: 132 additions & 29 deletions apps/meteor/client/views/admin/apps/AppMenu.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,35 @@
import { Box, Icon, Menu } from '@rocket.chat/fuselage';
import { Box, Icon, Menu, Throbber } from '@rocket.chat/fuselage';
import { useSetModal, useMethod, useEndpoint, useTranslation, useRoute, useRouteParameter } from '@rocket.chat/ui-contexts';
import React, { useMemo, useCallback } from 'react';
import React, { useMemo, useCallback, useState } from 'react';

import { Apps } from '../../../../app/apps/client/orchestrator';
import WarningModal from '../../../components/WarningModal';
import AppPermissionsReviewModal from './AppPermissionsReviewModal';
import CloudLoginModal from './CloudLoginModal';
import IframeModal from './IframeModal';
import { appEnabledStatuses, warnStatusChange, handleAPIError } from './helpers';
import { appEnabledStatuses, warnStatusChange, handleAPIError, appButtonProps, handleInstallError } from './helpers';

const installApp = async ({ id, name, version, permissionsGranted }) => {
try {
const { status } = await Apps.installApp(id, version, permissionsGranted);
warnStatusChange(name, status);
} catch (error) {
handleAPIError(error);
}
};

const actions = {
purchase: installApp,
install: installApp,
update: async ({ id, name, marketplaceVersion, permissionsGranted }) => {
try {
const { status } = await Apps.updateApp(id, marketplaceVersion, permissionsGranted);
warnStatusChange(name, status);
} catch (error) {
handleAPIError(error);
}
},
};

function AppMenu({ app, ...props }) {
const t = useTranslation();
Expand All @@ -19,9 +43,15 @@ function AppMenu({ app, ...props }) {
const syncApp = useEndpoint('POST', `/apps/${app.id}/sync`);
const uninstallApp = useEndpoint('DELETE', `/apps/${app.id}`);

const [loading, setLoading] = useState(false);

const canAppBeSubscribed = app.purchaseType === 'subscription';
const isSubscribed = app.subscriptionInfo && ['active', 'trialing'].includes(app.subscriptionInfo.status);
const isAppEnabled = appEnabledStatuses.includes(app.status);
const [isAppPurchased, setPurchased] = useState(app?.isPurchased);

const button = appButtonProps(app || {});
const action = button?.action || '';

const closeModal = useCallback(() => {
setModal(null);
Expand Down Expand Up @@ -117,19 +147,84 @@ function AppMenu({ app, ...props }) {
);
}, [closeModal, handleSubscription, isSubscribed, setModal, t, uninstallApp]);

const menuOptions = useMemo(
() => ({
...(canAppBeSubscribed && {
subscribe: {
label: (
<Box>
<Icon name='card' size='x16' marginInlineEnd='x4' />
{t('Subscription')}
</Box>
),
action: handleSubscription,
const cancelAction = useCallback(() => {
setModal(null);
setLoading(false);
}, [setModal]);

const confirmAction = useCallback(
(permissionsGranted) => {
setModal(null);

actions[action]({ ...app, permissionsGranted }).then(() => {
setLoading(false);
});
},
[setModal, action, app, setLoading],
);

const showAppPermissionsReviewModal = useCallback(() => {
if (!isAppPurchased) {
setPurchased(true);
}

if (!Array.isArray(app.permissions)) {
handleInstallError(new Error('The "permissions" property from the app manifest is invalid'));
}

return setModal(<AppPermissionsReviewModal appPermissions={app.permissions} onCancel={cancelAction} onConfirm={confirmAction} />);
}, [app.permissions, cancelAction, confirmAction, isAppPurchased, setModal, setPurchased]);

const handleAcquireApp = useCallback(async () => {
setLoading(true);

const isLoggedIn = await checkUserLoggedIn();

if (!isLoggedIn) {
setLoading(false);
setModal(<CloudLoginModal />);
return;
}

if (action === 'purchase' && !isAppPurchased) {
try {
const data = await Apps.buildExternalUrl(app.id, app.purchaseType, false);
setModal(<IframeModal url={data.url} cancel={cancelAction} confirm={showAppPermissionsReviewModal} />);
} catch (error) {
handleAPIError(error);
}
return;
}

showAppPermissionsReviewModal();
}, [action, app.id, app.purchaseType, cancelAction, checkUserLoggedIn, isAppPurchased, setModal, showAppPermissionsReviewModal]);

const menuOptions = useMemo(() => {
const bothAppStatusOptions = {
...(canAppBeSubscribed &&
isSubscribed && {
subscribe: {
label: (
<Box>
<Icon name='card' size='x16' marginInlineEnd='x4' />
{t('Subscription')}
</Box>
),
action: handleSubscription,
},
}),
};

const nonInstalledAppOptions = {
...(!app.installed && {
acquire: {
label: <Box>{t(button.label.replace(' ', '_'))}</Box>,
action: handleAcquireApp,
},
}),
};

const installedAppOptions = {
...(context !== 'details' &&
app.installed && {
viewLogs: {
Expand Down Expand Up @@ -177,22 +272,30 @@ function AppMenu({ app, ...props }) {
action: handleUninstall,
},
}),
}),
[
canAppBeSubscribed,
t,
handleSubscription,
context,
handleViewLogs,
app.installed,
isAppEnabled,
handleDisable,
handleEnable,
handleUninstall,
],
);
};

return {
...bothAppStatusOptions,
...nonInstalledAppOptions,
...installedAppOptions,
};
}, [
canAppBeSubscribed,
isSubscribed,
t,
handleSubscription,
app.installed,
button?.label,
handleAcquireApp,
context,
handleViewLogs,
isAppEnabled,
handleDisable,
handleEnable,
handleUninstall,
]);

return <Menu options={menuOptions} placement='bottom-start' {...props} />;
return loading ? <Throbber disabled /> : <Menu options={menuOptions} placement='bottom-start' {...props} />;
}

export default AppMenu;
4 changes: 3 additions & 1 deletion apps/meteor/client/views/admin/apps/AppsList/AppRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,9 @@ const AppRow: FC<AppRowProps> = (props) => {
</Box>
<Box display='flex' flexDirection='row' alignItems='center' justifyContent='flex-end' onClick={preventClickPropagation} width='20%'>
<AppStatus app={props} isSubscribed={isSubscribed} isAppDetailsPage={false} installed={installed} mis='x4' />
<Box minWidth='x32'>{(installed || isSubscribed) && <AppMenu app={props} mis='x4' />}</Box>
<Box minWidth='x32'>
<AppMenu app={props} mis='x4' />
</Box>
</Box>
</Box>
);
Expand Down
21 changes: 10 additions & 11 deletions apps/meteor/client/views/admin/apps/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,18 +86,17 @@ const shouldHandleErrorAsWarning = (message: string): boolean => {
return warnings.includes(message);
};

export const handleAPIError = (error: {
xhr: { responseJSON: { status: any; messages: any; error: any; payload?: any } };
message: string;
}): void => {
const message = error.xhr?.responseJSON?.error ?? error.message;

if (shouldHandleErrorAsWarning(message)) {
dispatchToastMessage({ type: 'warning', message });
return;
}
export const handleAPIError = (error: unknown): void => {
if (error instanceof Error) {
const { message } = error;

dispatchToastMessage({ type: 'error', message });
if (shouldHandleErrorAsWarning(message)) {
dispatchToastMessage({ type: 'warning', message });
return;
}

dispatchToastMessage({ type: 'error', message });
}
};

export const warnStatusChange = (appName: string, status: AppStatus): void => {
Expand Down
19 changes: 19 additions & 0 deletions apps/meteor/client/views/admin/apps/helpers/installApp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { App, AppPermission } from '@rocket.chat/core-typings';

import { Apps } from '../../../../../app/apps/client/orchestrator';
import { handleAPIError, warnStatusChange } from '../helpers';

type installAppProps = App & {
permissionsGranted: AppPermission[];
};

export const installApp = async ({ id, name, marketplaceVersion, permissionsGranted }: installAppProps): Promise<void> => {
try {
const { status } = await Apps.installApp(id, marketplaceVersion, permissionsGranted);
if (status) {
warnStatusChange(name, status);
}
} catch (error) {
handleAPIError(error);
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { installApp } from './installApp';
import { updateApp } from './updateApp';

export const marketplaceActions = {
purchase: installApp,
install: installApp,
update: updateApp,
};
19 changes: 19 additions & 0 deletions apps/meteor/client/views/admin/apps/helpers/updateApp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { App, AppPermission } from '@rocket.chat/core-typings';

import { Apps } from '../../../../../app/apps/client/orchestrator';
import { handleAPIError, warnStatusChange } from '../helpers';

type updateAppProps = App & {
permissionsGranted: AppPermission[];
};

export const updateApp = async ({ id, name, marketplaceVersion, permissionsGranted }: updateAppProps): Promise<void> => {
try {
const { status } = await Apps.updateApp(id, marketplaceVersion, permissionsGranted);
if (status) {
warnStatusChange(name, status);
}
} catch (error) {
handleAPIError(error);
}
};