/** * Copyright (c) 2025-present, Goldman Sachs * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { clsx, CheckIcon, PlusIcon, CheckCircleIcon } from '@finos/legend-art'; import { RecommendationSource, type TerminalResult, } from '@finos/legend-server-marketplace'; import { Box, Button, CircularProgress, Tooltip, Typography, } from '@mui/material'; import { flowResult } from 'mobx'; import { observer } from 'mobx-react-lite'; import { type ReactNode, useState } from 'react'; import { assertErrorThrown } from '@finos/legend-shared'; import { toastManager } from '../Toast/CartToast.js'; import { useLegendMarketplaceBaseStore } from '../../application/providers/LegendMarketplaceFrameworkProvider.js'; import { formatItemPrice } from '../ProviderCard/orderProfileUtils.js'; import { LegendMarketplaceTelemetryHelper, TERMINAL_SEARCH_LOCATION, } from '../../__lib__/LegendMarketplaceTelemetryHelper.js'; interface RecommendedItemsCardProps { recommendedItem: TerminalResult; onSelect?: (item: TerminalResult) => Promise | boolean; /** * Notifies the parent list that this item was successfully added, so * column filters (Action status) that live outside this component can stay * in sync with skip-workflow adds that don't register in cartStore. */ onItemAdded?: (itemId: number) => void; isSelecting?: boolean; selectedItemId?: number; permissionIdOverride?: number; modelOverride?: string | null; } export const RecommendedItemsCard = observer( (props: RecommendedItemsCardProps) => { const { recommendedItem, onSelect, onItemAdded, isSelecting, selectedItemId, permissionIdOverride, modelOverride, } = props; const legendMarketplaceBaseStore = useLegendMarketplaceBaseStore(); const [isAddingToCart, setIsAddingToCart] = useState(false); // Tracks a successful add within this component's lifetime so the button // transitions even when skipWorkflow=true causes the item to bypass the // normal cart and therefore not appear in isItemInCart. const [isAdded, setIsAdded] = useState(false); const inCart = legendMarketplaceBaseStore.cartStore.isItemInCart( recommendedItem.id, ); const isAssociationFlow = onSelect !== undefined; const isMarketplaceItem = recommendedItem.source === RecommendationSource.MARKETPLACE; const isCartSourceItem = recommendedItem.source === RecommendationSource.CART; const isInCartOrAdded = isAssociationFlow && isCartSourceItem ? isAdded : inCart || isAdded; const isCurrentlySelecting = isAssociationFlow && Boolean(isSelecting) && selectedItemId === recommendedItem.id; const handleAddAddonToCart = (addon: TerminalResult) => { setIsAddingToCart(true); const cartItemRequest = legendMarketplaceBaseStore.cartStore.buildAddonCartRequest(addon, { ...(permissionIdOverride === undefined ? {} : { overridePermissionId: permissionIdOverride }), ...(modelOverride === undefined ? {} : { overrideModel: modelOverride }), }); flowResult( legendMarketplaceBaseStore.cartStore.addToCartWithAPI(cartItemRequest), ) .then((result) => { if (result.success) { setIsAdded(true); LegendMarketplaceTelemetryHelper.logEvent_AddTerminalAddonToCart( legendMarketplaceBaseStore.applicationStore.telemetryService, addon.id, addon.productName, addon.providerName, addon.terminalItemType, TERMINAL_SEARCH_LOCATION.ADDONS_POPUP, legendMarketplaceBaseStore.cartStore.cartUser !== legendMarketplaceBaseStore.applicationStore.identityService .currentUser, ); onItemAdded?.(addon.id); } else if (result.message) { toastManager.warning(result.message); } }) .catch((error) => { assertErrorThrown(error); toastManager.error( `Failed to add ${addon.productName} to cart: ${error.message}`, ); }) .finally(() => { setIsAddingToCart(false); }); }; const renderAssociationButton = ( isMarketplaceRecommendation: boolean, className: string, ): ReactNode => { const isLoading = isCurrentlySelecting || isAddingToCart; return ( ); }; const renderAssociationAction = (): ReactNode => { if (recommendedItem.isOwned) { return ( Subscribed ); } if (isInCartOrAdded) { return ( In Cart ); } return renderAssociationButton( isMarketplaceItem, isMarketplaceItem ? 'recommended-addons-modal__add-btn' : 'recommended-addons-modal__select-btn', ); }; const renderNonAssociationButtonLabel = () => { if (isAddingToCart) { return ( <> Adding...   ); } if (isInCartOrAdded) { return 'Added to Cart'; } return ( <> Add to Cart   ); }; const renderAction = () => { if (isAssociationFlow) { return renderAssociationAction(); } const button = ( ); if (isInCartOrAdded) { return ( {button} ); } return button; }; return ( {recommendedItem.productName} {recommendedItem.category} {formatItemPrice(recommendedItem.price)} {renderAction()} ); }, );