From d218cf669bd655e44c0388389ca69adaf664ce22 Mon Sep 17 00:00:00 2001 From: Hugo Costa Date: Tue, 22 Nov 2022 16:21:37 -0300 Subject: [PATCH 1/4] [FIX] Upgrading fuselage package and fix quote message prepend (#27307) Co-authored-by: Guilherme Jun Grillo <48109548+guijun13@users.noreply.github.com> --- .../meteor/client/lib/utils/prependReplies.ts | 2 +- yarn.lock | 78 +++++++++---------- 2 files changed, 40 insertions(+), 40 deletions(-) diff --git a/apps/meteor/client/lib/utils/prependReplies.ts b/apps/meteor/client/lib/utils/prependReplies.ts index 7d60d996d54e..ba4ba03da1e3 100644 --- a/apps/meteor/client/lib/utils/prependReplies.ts +++ b/apps/meteor/client/lib/utils/prependReplies.ts @@ -22,5 +22,5 @@ export const prependReplies = async (msg: string, replies: IMessage[] = [], ment ); chunks.push(msg); - return chunks.join(' '); + return chunks.join('\n'); }; diff --git a/yarn.lock b/yarn.lock index ebeb6c09ca19..df92857d2820 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5527,16 +5527,16 @@ __metadata: languageName: node linkType: hard -"@rocket.chat/css-in-js@npm:~0.31.22-dev.16": - version: 0.31.22-dev.16 - resolution: "@rocket.chat/css-in-js@npm:0.31.22-dev.16" +"@rocket.chat/css-in-js@npm:~0.31.22-dev.19": + version: 0.31.22-dev.19 + resolution: "@rocket.chat/css-in-js@npm:0.31.22-dev.19" dependencies: "@emotion/hash": ^0.9.0 - "@rocket.chat/css-supports": ~0.31.22-dev.16 - "@rocket.chat/memo": ~0.31.22-dev.16 - "@rocket.chat/stylis-logical-props-middleware": ~0.31.22-dev.16 + "@rocket.chat/css-supports": ~0.31.22-dev.19 + "@rocket.chat/memo": ~0.31.22-dev.19 + "@rocket.chat/stylis-logical-props-middleware": ~0.31.22-dev.19 stylis: ~4.1.3 - checksum: 34dcac20eb01560285a4223e796f1c7c196f81b23e97247c81a6c6ce615973ab2b78562931f87ac5015c48435b1ff8c98837b0782d21a472d86424d37bbdf326 + checksum: abbb6305cc630a976534faba5d6d519e36d3eb390f4a60e686fe5f5351609f85288dcd341859d76274f11b763d5c76a79dbf99c48bf01dae8a0249dcba9d01f0 languageName: node linkType: hard @@ -5549,12 +5549,12 @@ __metadata: languageName: node linkType: hard -"@rocket.chat/css-supports@npm:~0.31.22-dev.16": - version: 0.31.22-dev.16 - resolution: "@rocket.chat/css-supports@npm:0.31.22-dev.16" +"@rocket.chat/css-supports@npm:~0.31.22-dev.19": + version: 0.31.22-dev.19 + resolution: "@rocket.chat/css-supports@npm:0.31.22-dev.19" dependencies: - "@rocket.chat/memo": ~0.31.22-dev.16 - checksum: ede36c2895743a0d0ee318e2930a12a63e1452aa97d39ea9434c1f879c5d60fcd6914bd76cc64f164a48f0db6cfa3d9a024e39163eaf49462b6c33b22b2eb677 + "@rocket.chat/memo": ~0.31.22-dev.19 + checksum: bcc1333d1e5def81e7f1ec1a9147e21f75f9db2ea8295afd04d920eb750578ccd9ce6b23033823a089dc7a42cf0cdb2dfdd6775e6bb2f11dc7dddc97318b8753 languageName: node linkType: hard @@ -5752,10 +5752,10 @@ __metadata: languageName: node linkType: hard -"@rocket.chat/fuselage-tokens@npm:~0.32.0-dev.155": - version: 0.32.0-dev.155 - resolution: "@rocket.chat/fuselage-tokens@npm:0.32.0-dev.155" - checksum: ef4732d3943f662a4ee01879c7a9843c7eb7c30260f19fe55e4e09692c3994917597c1a97652b58164efdff8261e9d1afc0155eefae407ed3d5252be6fc8cdab +"@rocket.chat/fuselage-tokens@npm:~0.32.0-dev.158": + version: 0.32.0-dev.158 + resolution: "@rocket.chat/fuselage-tokens@npm:0.32.0-dev.158" + checksum: 18db2ad59b2e118161ea9835c8d1c1bfea6e8c654024c67140c4dd5a3eb6b51a6c7ee29203f22df368e1955e32fc67604f9a22193b31eb808c1c71bb9e1624f6 languageName: node linkType: hard @@ -5824,14 +5824,14 @@ __metadata: linkType: soft "@rocket.chat/fuselage@npm:next": - version: 0.32.0-dev.205 - resolution: "@rocket.chat/fuselage@npm:0.32.0-dev.205" - dependencies: - "@rocket.chat/css-in-js": ~0.31.22-dev.16 - "@rocket.chat/css-supports": ~0.31.22-dev.16 - "@rocket.chat/fuselage-tokens": ~0.32.0-dev.155 - "@rocket.chat/memo": ~0.31.22-dev.16 - "@rocket.chat/styled": ~0.31.22-dev.16 + version: 0.32.0-dev.208 + resolution: "@rocket.chat/fuselage@npm:0.32.0-dev.208" + dependencies: + "@rocket.chat/css-in-js": ~0.31.22-dev.19 + "@rocket.chat/css-supports": ~0.31.22-dev.19 + "@rocket.chat/fuselage-tokens": ~0.32.0-dev.158 + "@rocket.chat/memo": ~0.31.22-dev.19 + "@rocket.chat/styled": ~0.31.22-dev.19 invariant: ^2.2.4 react-aria: ~3.19.0 react-keyed-flatten-children: ^1.3.0 @@ -5843,7 +5843,7 @@ __metadata: react: ^17.0.2 react-dom: ^17.0.2 react-virtuoso: 1.2.4 - checksum: bf5861ede9cdcb5eefa80f1f9d6d0c4b623b4138817e6d8de945a1b5c5eb5abe9a6129d9e6cdb9eca52424bdf45901abdc29db8d66eae26620efda94625b8505 + checksum: 5105b3a1a8abc264ef0a8f4afacd66512f86cc574f28cfcf497f59a1f1c854c391a20faa4e0cea7cf5791fe15c4112dcf4103207fc36ac360509e211533c2d18 languageName: node linkType: hard @@ -6030,10 +6030,10 @@ __metadata: languageName: node linkType: hard -"@rocket.chat/memo@npm:~0.31.22-dev.16": - version: 0.31.22-dev.16 - resolution: "@rocket.chat/memo@npm:0.31.22-dev.16" - checksum: d0b1f3f744613095072c54dffb42519f9e37244d48e26e38bac7fb98e1e29cf572213d3b9351224547bbad66707026a21adccd705439d25fc8283b5907c85da0 +"@rocket.chat/memo@npm:~0.31.22-dev.19": + version: 0.31.22-dev.19 + resolution: "@rocket.chat/memo@npm:0.31.22-dev.19" + checksum: ed19dc301362647123f5d0622f45084f40163eef76da51509cc846ca376265200b6d9b3d02fdeb8782b4c503b491f9036dd487ad555e35a076c3fb5a37c0f684 languageName: node linkType: hard @@ -6590,13 +6590,13 @@ __metadata: languageName: node linkType: hard -"@rocket.chat/styled@npm:~0.31.22-dev.16": - version: 0.31.22-dev.16 - resolution: "@rocket.chat/styled@npm:0.31.22-dev.16" +"@rocket.chat/styled@npm:~0.31.22-dev.19": + version: 0.31.22-dev.19 + resolution: "@rocket.chat/styled@npm:0.31.22-dev.19" dependencies: - "@rocket.chat/css-in-js": ~0.31.22-dev.16 + "@rocket.chat/css-in-js": ~0.31.22-dev.19 tslib: ^2.3.1 - checksum: 936d779c975d45186f89409d0217d0aa8f14db456d50c8e0c74412b68941c994587aef79e7ac0f5203d0c53fea4bc3e153f8c0ec1ae395b0b56f7203eb102db8 + checksum: b94af2306e05276a3eb13dece9bb666268ad43c55faadd07a9e4aef0292f0bbff86d0a601cd291e9d9731b980b79232ff927db2178cab9fb707b8ce129a97a88 languageName: node linkType: hard @@ -6612,15 +6612,15 @@ __metadata: languageName: node linkType: hard -"@rocket.chat/stylis-logical-props-middleware@npm:~0.31.22-dev.16": - version: 0.31.22-dev.16 - resolution: "@rocket.chat/stylis-logical-props-middleware@npm:0.31.22-dev.16" +"@rocket.chat/stylis-logical-props-middleware@npm:~0.31.22-dev.19": + version: 0.31.22-dev.19 + resolution: "@rocket.chat/stylis-logical-props-middleware@npm:0.31.22-dev.19" dependencies: - "@rocket.chat/css-supports": ~0.31.22-dev.16 + "@rocket.chat/css-supports": ~0.31.22-dev.19 tslib: ^2.3.1 peerDependencies: stylis: 4.0.10 - checksum: 4179e742aabc8214554ade05dcac3219dab8b2566bae23b2c5e870cf735e8607d788e3831157c96b48927062d886f2af328cfc7eba3f30326be46ef934172200 + checksum: 92d079dbe3d3e14468dd88929accabe596716d0a4456759cfc5393ebb6162c0a2afe48df60ae1ede4347b5ad71683519b647b1bd5e052b9eee02221082048a78 languageName: node linkType: hard From f97c6fb98b4c43b5cc6d806049ec65e640a25747 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Tue, 22 Nov 2022 16:43:53 -0300 Subject: [PATCH 2/4] Regression: Fix custom oauth undefined clientConfig (#27320) --- apps/meteor/client/providers/UserProvider.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/client/providers/UserProvider.tsx b/apps/meteor/client/providers/UserProvider.tsx index a1458342fe9e..fe52016ac326 100644 --- a/apps/meteor/client/providers/UserProvider.tsx +++ b/apps/meteor/client/providers/UserProvider.tsx @@ -104,7 +104,7 @@ const UserProvider: FC = ({ children }) => { }); }), logout, - loginWithService: ({ service, clientConfig }: T): (() => Promise) => { + loginWithService: ({ service, clientConfig = {} }: T): (() => Promise) => { const loginMethods = { 'meteor-developer': 'MeteorDeveloperAccount', }; From 1ad8097680cfb08005a92f5eb1785cfe5cc238dc Mon Sep 17 00:00:00 2001 From: Matheus Lucca do Carmo Date: Tue, 22 Nov 2022 17:27:53 -0300 Subject: [PATCH 3/4] [NEW] Incompatible Apps (#27280) Co-authored-by: rique223 Co-authored-by: dougfabris Co-authored-by: juliajforesti --- apps/meteor/app/apps/client/orchestrator.ts | 15 ++ .../app/apps/server/communication/rest.js | 17 ++ .../AppDetailsPage/AppDetailsPageHeader.tsx | 35 +++- .../tabs/AppStatus/AppStatus.js | 136 -------------- .../tabs/AppStatus/AppStatus.tsx | 171 ++++++++++++++++++ .../tabs/AppStatus/AppStatusPriceDisplay.tsx | 4 +- .../meteor/client/views/admin/apps/AppMenu.js | 71 +++++++- .../views/admin/apps/AppsList/AppRow.tsx | 3 +- .../client/views/admin/apps/BundleChips.tsx | 35 ++-- .../meteor/client/views/admin/apps/helpers.ts | 98 +++++++++- .../rocketchat-i18n/i18n/en.i18n.json | 1 + .../apps/helpers/filterAppsByFree.test.ts | 13 +- .../apps/helpers/filterAppsByPaid.test.ts | 13 +- packages/core-typings/src/Apps.ts | 5 +- packages/rest-typings/src/apps/index.ts | 4 + 15 files changed, 431 insertions(+), 190 deletions(-) delete mode 100644 apps/meteor/client/views/admin/apps/AppDetailsPage/tabs/AppStatus/AppStatus.js create mode 100644 apps/meteor/client/views/admin/apps/AppDetailsPage/tabs/AppStatus/AppStatus.tsx diff --git a/apps/meteor/app/apps/client/orchestrator.ts b/apps/meteor/app/apps/client/orchestrator.ts index 3bc2cf8bc028..6e3591de0be0 100644 --- a/apps/meteor/app/apps/client/orchestrator.ts +++ b/apps/meteor/app/apps/client/orchestrator.ts @@ -216,6 +216,21 @@ class AppClientOrchestrator { if ('url' in result) { return result; } + + throw new Error('Failed to build external url'); + } + + public async buildIncompatibleExternalUrl(appId: string, appVersion: string, action: string): Promise { + const result = await APIClient.get('/apps/incompatibleModal', { + appId, + appVersion, + action, + }); + + if ('url' in result) { + return result; + } + throw new Error('Failed to build external url'); } diff --git a/apps/meteor/app/apps/server/communication/rest.js b/apps/meteor/app/apps/server/communication/rest.js index b623952f779c..1dd088b5cd4f 100644 --- a/apps/meteor/app/apps/server/communication/rest.js +++ b/apps/meteor/app/apps/server/communication/rest.js @@ -13,6 +13,7 @@ import { formatAppInstanceForRest } from '../../lib/misc/formatAppInstanceForRes import { actionButtonsHandler } from './endpoints/actionButtonsHandler'; import { fetch } from '../../../../server/lib/http/fetch'; +const rocketChatVersion = Info.version; const appsEngineVersionForMarketplace = Info.marketplaceApiVersion.replace(/-.*/g, ''); const getDefaultHeaders = () => ({ 'X-Apps-Engine-Version': appsEngineVersionForMarketplace, @@ -65,6 +66,22 @@ export class AppsRestApi { this.api.addRoute('actionButtons', ...actionButtonsHandler(this)); + this.api.addRoute( + 'incompatibleModal', + { authRequired: true }, + { + async get() { + const baseUrl = orchestrator.getMarketplaceUrl(); + const workspaceId = settings.get('Cloud_Workspace_Id'); + const { action, appId, appVersion } = this.queryParams; + + return API.v1.success({ + url: `${baseUrl}/apps/${appId}/incompatible/${appVersion}/${action}?workspaceId=${workspaceId}&rocketChatVersion=${rocketChatVersion}`, + }); + }, + }, + ); + // WE NEED TO MOVE EACH ENDPOINT HANDLER TO IT'S OWN FILE this.api.addRoute( '', diff --git a/apps/meteor/client/views/admin/apps/AppDetailsPage/AppDetailsPageHeader.tsx b/apps/meteor/client/views/admin/apps/AppDetailsPage/AppDetailsPageHeader.tsx index 67417d167fbe..14825e5bcbbb 100644 --- a/apps/meteor/client/views/admin/apps/AppDetailsPage/AppDetailsPageHeader.tsx +++ b/apps/meteor/client/views/admin/apps/AppDetailsPage/AppDetailsPageHeader.tsx @@ -1,5 +1,5 @@ import type { App } from '@rocket.chat/core-typings'; -import { Box } from '@rocket.chat/fuselage'; +import { Box, Tag } from '@rocket.chat/fuselage'; import { useTranslation } from '@rocket.chat/ui-contexts'; import moment from 'moment'; import React, { ReactElement } from 'react'; @@ -7,12 +7,23 @@ import React, { ReactElement } from 'react'; import AppAvatar from '../../../../components/avatar/AppAvatar'; import AppMenu from '../AppMenu'; import BundleChips from '../BundleChips'; +import { appIncompatibleStatusProps } from '../helpers'; import AppStatus from './tabs/AppStatus'; +const versioni18nKey = (app: App): string => { + const { version, marketplaceVersion, marketplace } = app; + if (typeof marketplace === 'boolean') { + return marketplaceVersion; + } + + return version; +}; + const AppDetailsPageHeader = ({ app }: { app: App }): ReactElement => { const t = useTranslation(); - const { iconFileData, name, author, version, iconFileContent, installed, isSubscribed, modifiedAt, bundledIn } = app; + const { iconFileData, name, author, iconFileContent, installed, modifiedAt, bundledIn, versionIncompatible, isSubscribed } = app; const lastUpdated = modifiedAt && moment(modifiedAt).fromNow(); + const incompatibleStatus = versionIncompatible ? appIncompatibleStatusProps() : undefined; return ( @@ -25,6 +36,7 @@ const AppDetailsPageHeader = ({ app }: { app: App }): ReactElement => { {bundledIn && Boolean(bundledIn.length) && } {app?.shortDescription && {app.shortDescription}} + {(installed || isSubscribed) && } @@ -34,11 +46,26 @@ const AppDetailsPageHeader = ({ app }: { app: App }): ReactElement => { {t('By_author', { author: author?.name })} | - {t('Version_version', { version })} + + + {t('Version_version', { version: versioni18nKey(app) })} + + + {versionIncompatible && ( + + + {incompatibleStatus?.label} + + + )} + {lastUpdated && ( <> | - + {t('Marketplace_app_last_updated', { lastUpdated, })} diff --git a/apps/meteor/client/views/admin/apps/AppDetailsPage/tabs/AppStatus/AppStatus.js b/apps/meteor/client/views/admin/apps/AppDetailsPage/tabs/AppStatus/AppStatus.js deleted file mode 100644 index 7f806669b44e..000000000000 --- a/apps/meteor/client/views/admin/apps/AppDetailsPage/tabs/AppStatus/AppStatus.js +++ /dev/null @@ -1,136 +0,0 @@ -import { Box, Button, Icon, Throbber, Tag } from '@rocket.chat/fuselage'; -import { useSafely } from '@rocket.chat/fuselage-hooks'; -import { useSetModal, useMethod, useTranslation } from '@rocket.chat/ui-contexts'; -import React, { useCallback, useState, memo } from 'react'; - -import { Apps } from '../../../../../../../app/apps/client/orchestrator'; -import AppPermissionsReviewModal from '../../../AppPermissionsReviewModal'; -import CloudLoginModal from '../../../CloudLoginModal'; -import IframeModal from '../../../IframeModal'; -import { appButtonProps, appStatusSpanProps, handleAPIError, handleInstallError } from '../../../helpers'; -import { marketplaceActions } from '../../../helpers/marketplaceActions'; -import AppStatusPriceDisplay from './AppStatusPriceDisplay'; - -const AppStatus = ({ app, showStatus = true, isAppDetailsPage, installed, ...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); - - const action = button?.action || ''; - const confirmAction = useCallback( - (permissionsGranted) => { - setModal(null); - - marketplaceActions[action]({ ...app, permissionsGranted }).then(() => { - setLoading(false); - }); - }, - [setModal, action, app, setLoading], - ); - - const cancelAction = useCallback(() => { - setLoading(false); - setModal(null); - }, [setLoading, setModal]); - - const showAppPermissionsReviewModal = () => { - if (!isAppPurchased) { - setPurchased(true); - } - - if (!app.permissions || app.permissions.length === 0) { - return confirmAction(app.permissions); - } - - if (!Array.isArray(app.permissions)) { - handleInstallError(new Error('The "permissions" property from the app manifest is invalid')); - } - - return setModal(); - }; - - const checkUserLoggedIn = useMethod('cloud:checkUserLoggedIn'); - - const handleClick = async (e) => { - e.preventDefault(); - e.stopPropagation(); - - setLoading(true); - - const isLoggedIn = await checkUserLoggedIn(); - - if (!isLoggedIn) { - setLoading(false); - setModal(); - return; - } - - if (action === 'purchase' && !isAppPurchased) { - try { - const data = await Apps.buildExternalUrl(app.id, app.purchaseType, false); - setModal(); - } catch (error) { - handleAPIError(error); - } - return; - } - - showAppPermissionsReviewModal(); - }; - - const shouldShowPriceDisplay = isAppDetailsPage && button && button.action !== 'update'; - - return ( - - {button && isAppDetailsPage && ( - - - {shouldShowPriceDisplay && !installed && ( - - - - )} - - )} - {status && ( - <> - {status.label} - - )} - - ); -}; - -export default memo(AppStatus); diff --git a/apps/meteor/client/views/admin/apps/AppDetailsPage/tabs/AppStatus/AppStatus.tsx b/apps/meteor/client/views/admin/apps/AppDetailsPage/tabs/AppStatus/AppStatus.tsx new file mode 100644 index 000000000000..e478aa4a227e --- /dev/null +++ b/apps/meteor/client/views/admin/apps/AppDetailsPage/tabs/AppStatus/AppStatus.tsx @@ -0,0 +1,171 @@ +import type { App } from '@rocket.chat/core-typings'; +import { Box, Button, Icon, Throbber, Tag, Margins } from '@rocket.chat/fuselage'; +import { useSafely } from '@rocket.chat/fuselage-hooks'; +import { useSetModal, useMethod, useTranslation, TranslationKey } from '@rocket.chat/ui-contexts'; +import React, { useCallback, useState, memo, ReactElement, Fragment } from 'react'; + +import { Apps } from '../../../../../../../app/apps/client/orchestrator'; +import AppPermissionsReviewModal from '../../../AppPermissionsReviewModal'; +import CloudLoginModal from '../../../CloudLoginModal'; +import IframeModal from '../../../IframeModal'; +import { appButtonProps, appMultiStatusProps, handleAPIError, handleInstallError } from '../../../helpers'; +import { marketplaceActions } from '../../../helpers/marketplaceActions'; +import AppStatusPriceDisplay from './AppStatusPriceDisplay'; + +type AppStatusProps = { + app: App; + showStatus?: boolean; + isAppDetailsPage: boolean; + installed?: boolean; +}; + +const AppStatus = ({ app, showStatus = true, isAppDetailsPage, installed, ...props }: AppStatusProps): ReactElement => { + const t = useTranslation(); + const [loading, setLoading] = useSafely(useState(false)); + const [isAppPurchased, setPurchased] = useSafely(useState(app?.isPurchased)); + const setModal = useSetModal(); + const { price, purchaseType, pricingPlans } = app; + const button = appButtonProps(app || {}); + const statuses = appMultiStatusProps(app, isAppDetailsPage); + + if (button?.action === undefined && button?.action) { + throw new Error('action must not be null'); + } + + const action = button?.action; + const confirmAction = useCallback( + (permissionsGranted) => { + setModal(null); + + if (action === undefined) { + setLoading(false); + return; + } + + marketplaceActions[action]({ ...app, permissionsGranted }).then(() => { + setLoading(false); + }); + }, + [setModal, action, app, setLoading], + ); + + const cancelAction = useCallback(() => { + setLoading(false); + setModal(null); + }, [setLoading, setModal]); + + const showAppPermissionsReviewModal = (): void => { + if (!isAppPurchased) { + setPurchased(true); + } + + if (!app.permissions || app.permissions.length === 0) { + return confirmAction(app.permissions); + } + + if (!Array.isArray(app.permissions)) { + handleInstallError(new Error('The "permissions" property from the app manifest is invalid')); + } + + return setModal(); + }; + + const openIncompatibleModal = async (app: App, action: string, cancel: () => void): Promise => { + try { + const incompatibleData = await Apps.buildIncompatibleExternalUrl(app.id, app.marketplaceVersion, action); + setModal(); + } catch (e: any) { + handleAPIError(e); + } + }; + + const openPurchaseModal = async (app: App): Promise => { + try { + const data = await Apps.buildExternalUrl(app.id, app.purchaseType, false); + setModal(); + } catch (error) { + handleAPIError(error); + } + }; + + const checkUserLoggedIn = useMethod('cloud:checkUserLoggedIn'); + + const handleClick = async (e: React.MouseEvent): Promise => { + e.preventDefault(); + e.stopPropagation(); + + setLoading(true); + + const isLoggedIn = await checkUserLoggedIn(); + + if (!isLoggedIn) { + setLoading(false); + setModal(); + return; + } + + if (app.versionIncompatible && action !== undefined) { + openIncompatibleModal(app, action, cancelAction); + return; + } + + if (action !== undefined && action === 'purchase' && !isAppPurchased) { + openPurchaseModal(app); + return; + } + + showAppPermissionsReviewModal(); + }; + + const shouldShowPriceDisplay = isAppDetailsPage && button; + + return ( + + {button && isAppDetailsPage && ( + + + + {shouldShowPriceDisplay && !installed && ( + + + + )} + + )} + + {statuses?.map((status, index) => ( + + + {status.tooltipText ? ( + + {status.label} + + ) : ( + + {status.label} + + )} + + + ))} + + ); +}; + +export default memo(AppStatus); diff --git a/apps/meteor/client/views/admin/apps/AppDetailsPage/tabs/AppStatus/AppStatusPriceDisplay.tsx b/apps/meteor/client/views/admin/apps/AppDetailsPage/tabs/AppStatus/AppStatusPriceDisplay.tsx index fae7f7a31ee9..5645cf4856f6 100644 --- a/apps/meteor/client/views/admin/apps/AppDetailsPage/tabs/AppStatus/AppStatusPriceDisplay.tsx +++ b/apps/meteor/client/views/admin/apps/AppDetailsPage/tabs/AppStatus/AppStatusPriceDisplay.tsx @@ -1,4 +1,4 @@ -import type { AppPricingPlan } from '@rocket.chat/core-typings'; +import type { AppPricingPlan, PurchaseType } from '@rocket.chat/core-typings'; import { Box, Tag } from '@rocket.chat/fuselage'; import { TranslationKey, useTranslation } from '@rocket.chat/ui-contexts'; import React, { FC, useMemo } from 'react'; @@ -6,7 +6,7 @@ import React, { FC, useMemo } from 'react'; import { formatPriceAndPurchaseType } from '../../../helpers'; type AppStatusPriceDisplayProps = { - purchaseType: string; + purchaseType: PurchaseType; pricingPlans: AppPricingPlan[]; price: number; showType?: boolean; diff --git a/apps/meteor/client/views/admin/apps/AppMenu.js b/apps/meteor/client/views/admin/apps/AppMenu.js index 5d61b5752889..b2119307b408 100644 --- a/apps/meteor/client/views/admin/apps/AppMenu.js +++ b/apps/meteor/client/views/admin/apps/AppMenu.js @@ -20,6 +20,15 @@ import IframeModal from './IframeModal'; import { appEnabledStatuses, handleAPIError, appButtonProps, handleInstallError, warnEnableDisableApp } from './helpers'; import { marketplaceActions } from './helpers/marketplaceActions'; +const openIncompatibleModal = async (app, action, cancel, setModal) => { + try { + const incompatibleData = await Apps.buildIncompatibleExternalUrl(app.id, app.marketplaceVersion, action); + setModal(); + } catch (e) { + handleAPIError(e); + } +}; + function AppMenu({ app, isAppDetailsPage, ...props }) { const t = useTranslation(); const dispatchToastMessage = useToastMessageDispatch(); @@ -80,6 +89,7 @@ function AppMenu({ app, isAppDetailsPage, ...props }) { const closeModal = useCallback(() => { setModal(null); + setLoading(false); }, [setModal]); const handleSubscription = useCallback(async () => { @@ -88,6 +98,11 @@ function AppMenu({ app, isAppDetailsPage, ...props }) { return; } + if (app?.versionIncompatible && !isSubscribed) { + openIncompatibleModal(app, 'subscribe', closeModal, setModal); + return; + } + let data; try { data = await buildExternalUrl({ @@ -110,7 +125,7 @@ function AppMenu({ app, isAppDetailsPage, ...props }) { }; setModal(); - }, [checkUserLoggedIn, setModal, closeModal, buildExternalUrl, app.id, app.purchaseType, syncApp]); + }, [checkUserLoggedIn, app, setModal, closeModal, isSubscribed, buildExternalUrl, syncApp]); const handleAcquireApp = useCallback(async () => { setLoading(true); @@ -123,6 +138,11 @@ function AppMenu({ app, isAppDetailsPage, ...props }) { return; } + if (app?.versionIncompatible) { + openIncompatibleModal(app, 'subscribe', closeModal, setModal); + return; + } + if (action === 'purchase' && !isAppPurchased) { try { const data = await Apps.buildExternalUrl(app.id, app.purchaseType, false); @@ -134,7 +154,7 @@ function AppMenu({ app, isAppDetailsPage, ...props }) { } showAppPermissionsReviewModal(); - }, [action, app.id, app.purchaseType, cancelAction, checkUserLoggedIn, isAppPurchased, setModal, showAppPermissionsReviewModal]); + }, [action, app, closeModal, cancelAction, checkUserLoggedIn, isAppPurchased, setModal, showAppPermissionsReviewModal]); const handleViewLogs = useCallback(() => { router.push({ context, page: 'info', id: app.id, version: app.version, tab: 'logs' }); @@ -215,9 +235,38 @@ function AppMenu({ app, isAppDetailsPage, ...props }) { uninstallApp, ]); + const incompatibleIconName = useCallback( + (app, action) => { + if (!app.versionIncompatible) { + if (action === 'update') { + return 'refresh'; + } + + return 'card'; + } + + // Now we are handling an incompatible app + if (action === 'subscribe' && !isSubscribed) { + return 'warning'; + } + + if (action === 'install' || action === 'update') { + return 'warning'; + } + + return 'card'; + }, + [isSubscribed], + ); + const handleUpdate = useCallback(async () => { setLoading(true); + if (app?.versionIncompatible) { + openIncompatibleModal(app, 'update', closeModal, setModal); + return; + } + const isLoggedIn = await checkUserLoggedIn(); if (!isLoggedIn) { @@ -227,7 +276,7 @@ function AppMenu({ app, isAppDetailsPage, ...props }) { } showAppPermissionsReviewModal(); - }, [checkUserLoggedIn, setModal, showAppPermissionsReviewModal]); + }, [checkUserLoggedIn, app, closeModal, setModal, showAppPermissionsReviewModal]); const canUpdate = app.installed && app.version && app.marketplaceVersion && semver.lt(app.version, app.marketplaceVersion); @@ -238,7 +287,7 @@ function AppMenu({ app, isAppDetailsPage, ...props }) { subscribe: { label: ( - + {t('Subscription')} ), @@ -250,7 +299,12 @@ function AppMenu({ app, isAppDetailsPage, ...props }) { const nonInstalledAppOptions = { ...(!app.installed && { acquire: { - label: {t(button.label.replace(' ', '_'))}, + label: ( + + + {t(button.label.replace(' ', '_'))} + + ), action: handleAcquireApp, }, }), @@ -274,7 +328,7 @@ function AppMenu({ app, isAppDetailsPage, ...props }) { update: { label: ( - + {t('Update')} ), @@ -331,9 +385,9 @@ function AppMenu({ app, isAppDetailsPage, ...props }) { }, [ canAppBeSubscribed, isSubscribed, + app, t, handleSubscription, - app?.installed, button?.label, handleAcquireApp, context, @@ -345,9 +399,10 @@ function AppMenu({ app, isAppDetailsPage, ...props }) { handleDisable, handleEnable, handleUninstall, + incompatibleIconName, ]); - return loading ? : ; + return loading ? : ; } export default AppMenu; diff --git a/apps/meteor/client/views/admin/apps/AppsList/AppRow.tsx b/apps/meteor/client/views/admin/apps/AppsList/AppRow.tsx index a599d4955ef6..f667d3c9f707 100644 --- a/apps/meteor/client/views/admin/apps/AppsList/AppRow.tsx +++ b/apps/meteor/client/views/admin/apps/AppsList/AppRow.tsx @@ -90,9 +90,10 @@ const AppRow = (props: AppRowProps): ReactElement => { {shortDescription && {shortDescription}} + {canUpdate && } - + diff --git a/apps/meteor/client/views/admin/apps/BundleChips.tsx b/apps/meteor/client/views/admin/apps/BundleChips.tsx index 7150893f8026..cf231e16f9eb 100644 --- a/apps/meteor/client/views/admin/apps/BundleChips.tsx +++ b/apps/meteor/client/views/admin/apps/BundleChips.tsx @@ -1,6 +1,6 @@ -import { Box, PositionAnimated, AnimatedVisibility, Tooltip, Tag } from '@rocket.chat/fuselage'; +import { Tag } from '@rocket.chat/fuselage'; import { useTranslation } from '@rocket.chat/ui-contexts'; -import React, { RefObject, useRef, useState, ReactElement, Fragment } from 'react'; +import React, { ReactElement } from 'react'; import { App } from './types'; @@ -15,29 +15,18 @@ type BundleChipsProps = { const BundleChips = ({ bundledIn }: BundleChipsProps): ReactElement => { const t = useTranslation(); - const bundleRef = useRef(); - const [isHovered, setIsHovered] = useState(false); - return ( <> - {bundledIn.map((bundle) => ( - - setIsHovered(true)} onMouseLeave={(): void => setIsHovered(false)}> - {bundle.bundleName} - - } - placement='top-middle' - margin={8} - visible={isHovered ? AnimatedVisibility.VISIBLE : AnimatedVisibility.HIDDEN} - > - - {t('this_app_is_included_with_subscription', { - bundleName: bundle.bundleName, - })} - - - + {bundledIn.map((bundle, index) => ( + + {bundle.bundleName} + ))} ); diff --git a/apps/meteor/client/views/admin/apps/helpers.ts b/apps/meteor/client/views/admin/apps/helpers.ts index db3a53a1397d..6e2e5e97030d 100644 --- a/apps/meteor/client/views/admin/apps/helpers.ts +++ b/apps/meteor/client/views/admin/apps/helpers.ts @@ -1,6 +1,6 @@ import { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; import { IApiEndpointMetadata } from '@rocket.chat/apps-engine/definition/api'; -import { App, AppPricingPlan } from '@rocket.chat/core-typings'; +import { App, AppPricingPlan, PurchaseType } from '@rocket.chat/core-typings'; import semver from 'semver'; import { Utilities } from '../../../../app/apps/lib/misc/Utilities'; @@ -9,6 +9,17 @@ import { dispatchToastMessage } from '../../../lib/toast'; export const appEnabledStatuses = [AppStatus.AUTO_ENABLED, AppStatus.MANUALLY_ENABLED]; +interface ApiError { + xhr: { + responseJSON: { + error: string; + status: string; + messages: string[]; + payload?: any; + }; + }; +} + const appErroredStatuses = [ AppStatus.COMPILER_ERROR_DISABLED, AppStatus.ERROR_DISABLED, @@ -16,16 +27,19 @@ const appErroredStatuses = [ AppStatus.INVALID_LICENSE_DISABLED, ]; +type Actions = 'update' | 'install' | 'purchase'; + type appButtonResponseProps = { - action: 'update' | 'install' | 'purchase'; - icon?: 'reload'; + action: Actions; + icon?: 'reload' | 'warning'; label: 'Update' | 'Install' | 'Subscribe' | 'See Pricing' | 'Try now' | 'Buy'; }; type appStatusSpanResponseProps = { type?: 'failed' | 'warning'; icon: 'warning' | 'ban' | 'checkmark-circled' | 'check'; - label: 'Config Needed' | 'Failed' | 'Disabled' | 'Trial period' | 'Installed'; + label: 'Config Needed' | 'Failed' | 'Disabled' | 'Trial period' | 'Installed' | 'Incompatible'; + tooltipText?: string; }; type PlanType = 'Subscription' | 'Paid' | 'Free'; @@ -50,7 +64,12 @@ export const apiCurlGetter = }).split('\n'); }; -export function handleInstallError(apiError: { xhr: { responseJSON: { status: any; messages: any; error: any; payload?: any } } }): void { +export function handleInstallError(apiError: ApiError | Error): void { + if (apiError instanceof Error) { + dispatchToastMessage({ type: 'error', message: apiError.message }); + return; + } + if (!apiError.xhr || !apiError.xhr.responseJSON) { return; } @@ -141,9 +160,18 @@ export const appButtonProps = ({ subscriptionInfo, pricingPlans, isEnterpriseOnly, + versionIncompatible, }: App): appButtonResponseProps | undefined => { const canUpdate = installed && version && marketplaceVersion && semver.lt(version, marketplaceVersion); if (canUpdate) { + if (versionIncompatible) { + return { + action: 'update', + icon: 'warning', + label: 'Update', + }; + } + return { action: 'update', icon: 'reload', @@ -157,6 +185,14 @@ export const appButtonProps = ({ const canDownload = isPurchased; if (canDownload) { + if (versionIncompatible) { + return { + action: 'install', + icon: 'warning', + label: 'Install', + }; + } + return { action: 'install', label: 'Install', @@ -168,6 +204,14 @@ export const appButtonProps = ({ const cannotTry = pricingPlans.every((currentPricingPlan) => currentPricingPlan.trialDays === 0); const isTierBased = pricingPlans.every((currentPricingPlan) => currentPricingPlan.tiers && currentPricingPlan.tiers.length > 0); + if (versionIncompatible) { + return { + action: 'purchase', + label: 'Subscribe', + icon: 'warning', + }; + } + if (cannotTry || isEnterpriseOnly) { return { action: 'purchase', @@ -190,23 +234,44 @@ export const appButtonProps = ({ const canBuy = price > 0; if (canBuy) { + if (versionIncompatible) { + return { + action: 'purchase', + label: 'Buy', + icon: 'warning', + }; + } + return { action: 'purchase', label: 'Buy', }; } + if (versionIncompatible) { + return { + action: 'purchase', + label: 'Install', + icon: 'warning', + }; + } + return { action: 'purchase', label: 'Install', }; }; +export const appIncompatibleStatusProps = (): appStatusSpanResponseProps => ({ + icon: 'check', + label: 'Incompatible', + tooltipText: t('App_version_incompatible_tooltip'), +}); + export const appStatusSpanProps = ({ installed, status, subscriptionInfo }: App): appStatusSpanResponseProps | undefined => { if (!installed) { return; } - const isFailed = status && appErroredStatuses.includes(status); if (isFailed) { return { @@ -239,6 +304,21 @@ export const appStatusSpanProps = ({ installed, status, subscriptionInfo }: App) }; }; +export const appMultiStatusProps = (app: App, isAppDetailsPage: boolean): appStatusSpanResponseProps[] => { + const status = appStatusSpanProps(app); + const statuses = []; + + if (app?.versionIncompatible !== undefined && !isAppDetailsPage) { + statuses.push(appIncompatibleStatusProps()); + } + + if (status) { + statuses.push(status); + } + + return statuses; +}; + export const formatPrice = (price: number): string => `\$${price.toFixed(2)}`; export const formatPricingPlan = ({ strategy, price, tiers = [], trialDays }: AppPricingPlan): string => { @@ -260,7 +340,11 @@ export const formatPricingPlan = ({ strategy, price, tiers = [], trialDays }: Ap }); }; -export const formatPriceAndPurchaseType = (purchaseType: string, pricingPlans: AppPricingPlan[], price: number): FormattedPriceAndPlan => { +export const formatPriceAndPurchaseType = ( + purchaseType: PurchaseType, + pricingPlans: AppPricingPlan[], + price: number, +): FormattedPriceAndPlan => { if (purchaseType === 'subscription') { const type = 'Subscription'; if (!pricingPlans || !Array.isArray(pricingPlans) || pricingPlans.length === 0) { diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index 1b1d4fe0fb04..25c6255f1f82 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -5098,6 +5098,7 @@ "Verify_your_email_for_the_code_we_sent": "Verify your email for the code we sent", "Version": "Version", "Version_version": "Version __version__", + "App_version_incompatible_tooltip": "App incompatible with Rocket.Chat version", "Video_Conference_Description": "Configure conferencing calls for your workspace.", "Video_Chat_Window": "Video Chat", "Video_Conference": "Conference Call", diff --git a/apps/meteor/tests/unit/client/views/admin/apps/helpers/filterAppsByFree.test.ts b/apps/meteor/tests/unit/client/views/admin/apps/helpers/filterAppsByFree.test.ts index 4f1453d93fc3..eed2b57523ad 100644 --- a/apps/meteor/tests/unit/client/views/admin/apps/helpers/filterAppsByFree.test.ts +++ b/apps/meteor/tests/unit/client/views/admin/apps/helpers/filterAppsByFree.test.ts @@ -1,36 +1,41 @@ /* eslint-env mocha */ +import type { PurchaseType } from '@rocket.chat/core-typings'; import { expect } from 'chai'; import { filterAppsByFree } from '../../../../../../../client/views/admin/apps/helpers/filterAppsByFree'; describe('filterAppsByFree', () => { it('should return true if app purchase type is buy and price does not exist or is 0', () => { + const purchaseType: PurchaseType = 'buy'; const app = { - purchaseType: 'buy', + purchaseType, price: 0, }; const result = filterAppsByFree(app); expect(result).to.be.true; }); it('should return false if app purchase type is not buy', () => { + const purchaseType: PurchaseType = 'subscription'; const app = { - purchaseType: 'subscription', + purchaseType, price: 0, }; const result = filterAppsByFree(app); expect(result).to.be.false; }); it('should return false if app price exists and is different than 0', () => { + const purchaseType: PurchaseType = 'buy'; const app = { - purchaseType: 'buy', + purchaseType, price: 5, }; const result = filterAppsByFree(app); expect(result).to.be.false; }); it('should return false if both app purchase type is different than buy and price exists and is different than 0', () => { + const purchaseType: PurchaseType = 'subscription'; const app = { - purchaseType: 'subscription', + purchaseType, price: 5, }; const result = filterAppsByFree(app); diff --git a/apps/meteor/tests/unit/client/views/admin/apps/helpers/filterAppsByPaid.test.ts b/apps/meteor/tests/unit/client/views/admin/apps/helpers/filterAppsByPaid.test.ts index 0e5775925458..55de2b293f07 100644 --- a/apps/meteor/tests/unit/client/views/admin/apps/helpers/filterAppsByPaid.test.ts +++ b/apps/meteor/tests/unit/client/views/admin/apps/helpers/filterAppsByPaid.test.ts @@ -1,36 +1,41 @@ /* eslint-env mocha */ +import type { PurchaseType } from '@rocket.chat/core-typings'; import { expect } from 'chai'; import { filterAppsByPaid } from '../../../../../../../client/views/admin/apps/helpers/filterAppsByPaid'; describe('filterAppsByPaid', () => { it('should return true if both app purchase type is subscription and app price exists and is not 0', () => { + const purchaseType: PurchaseType = 'subscription'; const app = { - purchaseType: 'subscription', + purchaseType, price: 5, }; const result = filterAppsByPaid(app); expect(result).to.be.true; }); it('should return true if app purchase type is subscription', () => { + const purchaseType: PurchaseType = 'subscription'; const app = { - purchaseType: 'subscription', + purchaseType, price: 0, }; const result = filterAppsByPaid(app); expect(result).to.be.true; }); it('should return true if app price exists and is not 0', () => { + const purchaseType: PurchaseType = 'buy'; const app = { - purchaseType: 'buy', + purchaseType, price: 5, }; const result = filterAppsByPaid(app); expect(result).to.be.true; }); it('should return false if both app price does not exist or is 0 and app purchase type is not subscription', () => { + const purchaseType: PurchaseType = 'buy'; const app = { - purchaseType: 'buy', + purchaseType, price: 0, }; const result = filterAppsByPaid(app); diff --git a/packages/core-typings/src/Apps.ts b/packages/core-typings/src/Apps.ts index 49fe81c96318..4ee1756eb769 100644 --- a/packages/core-typings/src/Apps.ts +++ b/packages/core-typings/src/Apps.ts @@ -60,6 +60,8 @@ export type AppPermission = { required?: boolean; }; +export type PurchaseType = 'buy' | 'subscription'; + export type App = { id: string; iconFileData: string; @@ -82,8 +84,9 @@ export type App = { }; categories: string[]; version: string; + versionIncompatible?: boolean; price: number; - purchaseType: string; + purchaseType: PurchaseType; pricingPlans: AppPricingPlan[]; iconFileContent: string; installed?: boolean; diff --git a/packages/rest-typings/src/apps/index.ts b/packages/rest-typings/src/apps/index.ts index 3e28fcdb210d..adc5ee57ed22 100644 --- a/packages/rest-typings/src/apps/index.ts +++ b/packages/rest-typings/src/apps/index.ts @@ -11,6 +11,10 @@ export type AppsEndpoints = { GET: () => { externalComponents: IExternalComponent[] }; }; + '/apps/incompatibleModal': { + GET: (params: { appId: string; appVersion: string; action: string }) => { url: string }; + }; + '/apps/:id': { GET: | ((params: { marketplace?: 'true' | 'false'; version?: string; appVersion?: string; update?: 'true' | 'false' }) => { From 8b14fc6c2251c679b79e7c462783b5f1b7c48c28 Mon Sep 17 00:00:00 2001 From: Matheus Barbosa Silva <36537004+matheusbsilva137@users.noreply.github.com> Date: Tue, 22 Nov 2022 18:49:21 -0300 Subject: [PATCH 4/4] [FIX] LDAP groups to channel mapping attempts to create a new room instead of using an existing one (#27312) --- apps/meteor/app/models/server/models/Rooms.js | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/apps/meteor/app/models/server/models/Rooms.js b/apps/meteor/app/models/server/models/Rooms.js index d86fb37323c2..703cf3b28ec9 100644 --- a/apps/meteor/app/models/server/models/Rooms.js +++ b/apps/meteor/app/models/server/models/Rooms.js @@ -309,7 +309,7 @@ export class Rooms extends Base { } findOneByNonValidatedName(name, options) { - const room = this.findOneByName(name, options); + const room = this.findOneByNameOrFname(name, options); if (room) { return room; } @@ -332,6 +332,21 @@ export class Rooms extends Base { return this.findOne(query, options); } + findOneByNameOrFname(name, options) { + const query = { + $or: [ + { + name, + }, + { + fname: name, + }, + ], + }; + + return this.findOne(query, options); + } + findOneByNameAndNotId(name, rid) { const query = { _id: { $ne: rid },