/** * 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 { observer } from 'mobx-react-lite'; import { useCallback, useEffect, useRef, useState } from 'react'; import { flowResult } from 'mobx'; import { Drawer, Box, IconButton, Typography, Divider, Select, MenuItem, FormControl, InputLabel, Button, ButtonGroup, Chip, CircularProgress, Tooltip, } from '@mui/material'; import { CloseIcon, TrashIcon, ChevronDownIcon, ChevronRightIcon, InfoCircleIcon, clsx, } from '@finos/legend-art'; import { useLegendMarketplaceBaseStore } from '../../application/providers/LegendMarketplaceFrameworkProvider.js'; import { type CartVendorGroup, CartStore, } from '../../stores/cart/CartStore.js'; import { useApplicationStore } from '@finos/legend-application'; import { type CartItem } from '@finos/legend-server-marketplace'; import { formatCardPrice, formatItemPrice, } from '../ProviderCard/orderProfileUtils.js'; // ─── Helpers ───────────────────────────────────────────────────────────────── const MANDATORY_ADDON_TOOLTIP = 'This is a mandatory add-on included with the vendor profile.'; // ─── Cart Item Card (Add-on) ───────────────────────────────────────────────── const CartAddonCard = (props: { item: CartItem; vendorGroup: CartItem[]; isLastAddon: boolean; onDelete: (item: CartItem, vendorGroup: CartItem[]) => void; disabled: boolean; }): React.ReactNode => { const { item, vendorGroup, isLastAddon, onDelete, disabled } = props; const removeButton = ( onDelete(item, vendorGroup)} className="legend-marketplace-cart-drawer__addon-card__remove-btn" disabled={disabled} aria-label={`Remove ${item.productName}`} > ); return ( {item.productName} {item.isMandatory && ( )} {item.isMandatory ? ( {removeButton} ) : ( removeButton )} {formatItemPrice(item.price)} /month ); }; // ─── Cart Summary Bar ──────────────────────────────────────────────────────── const CartSummaryBar = (props: { formattedTotal: string }): React.ReactNode => { const { formattedTotal } = props; return ( Monthly Total {formattedTotal} ); }; // ─── Vendor Group Header (Parent Card) ─────────────────────────────────────── const CartVendorGroupHeader = (props: { parentItem: CartItem | undefined; displayParent: CartVendorGroup['displayParent']; vendorGroup: CartItem[]; addons: CartItem[]; addonTotalPrice: number; addonLabel: string; isExpanded: boolean; isSynthetic: boolean; onToggle: () => void; onDelete: (item: CartItem, vendorGroup: CartItem[]) => void; onDeleteSyntheticGroup: (vendorGroup: CartItem[]) => void; disabled: boolean; }): React.ReactNode => { const { parentItem, displayParent, vendorGroup, addons, addonTotalPrice, addonLabel, isExpanded, isSynthetic, onToggle, onDelete, onDeleteSyntheticGroup, disabled, } = props; return ( {displayParent.providerName} {displayParent.productName} onDeleteSyntheticGroup(vendorGroup) : () => { if (parentItem) { onDelete(parentItem, vendorGroup); } } } className="legend-marketplace-cart-drawer__item-card__remove-btn" disabled={disabled} aria-label={ isSynthetic ? `Remove all items under ${displayParent.productName}` : `Remove ${displayParent.productName}` } > {isSynthetic ? ( Already Subscribed ) : ( {formatItemPrice(displayParent.monthlyPrice ?? 0)} /month )} {addons.length > 0 && ( { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onToggle(); } }} aria-expanded={isExpanded} aria-label={`${isExpanded ? 'Collapse' : 'Expand'} add-ons for ${displayParent.productName}`} > {isExpanded ? : } {addonLabel} {!isExpanded && ` – ${formatCardPrice(addonTotalPrice)}`} )} ); }; // ─── Main CartDrawer ───────────────────────────────────────────────────────── export const CartDrawer = observer((): React.ReactNode => { const baseStore = useLegendMarketplaceBaseStore(); const applicationStore = useApplicationStore(); const cart = baseStore.cartStore; // Track which vendor groups are expanded (all expanded by default) const [expandedVendors, setExpandedVendors] = useState>( new Set(), ); const knownVendorIdsRef = useRef>(new Set()); // Refresh cart when drawer opens useEffect(() => { if (cart.open) { flowResult(cart.refresh()).catch((error) => { baseStore.applicationStore.notificationService.notifyError( `Failed to refresh cart: ${error}`, ); }); } }, [cart, cart.open, baseStore.applicationStore]); // Initialize all vendor groups as expanded when items change useEffect(() => { const currentVendorIds = new Set(cart.vendorGroupIds); setExpandedVendors((previousExpanded) => { const nextExpanded = new Set(); for (const vpId of previousExpanded) { if (currentVendorIds.has(vpId)) { nextExpanded.add(vpId); } } for (const vpId of currentVendorIds) { if (!knownVendorIdsRef.current.has(vpId)) { nextExpanded.add(vpId); } } return nextExpanded; }); knownVendorIdsRef.current = currentVendorIds; }, [cart.vendorGroupIds]); const toggleVendor = useCallback((vpId: number) => { setExpandedVendors((prev) => { const next = new Set(prev); if (next.has(vpId)) { next.delete(vpId); } else { next.add(vpId); } return next; }); }, []); const collapseAll = useCallback(() => { setExpandedVendors(new Set()); }, []); const expandAll = useCallback(() => { setExpandedVendors(new Set(cart.vendorGroupIds)); }, [cart.vendorGroupIds]); const vendorGroups = cart.vendorGroups; const handleDeleteItem = useCallback( (item: CartItem, vendorGroup: CartItem[]) => { cart.requestDeleteItemConfirmation(item, vendorGroup); }, [cart], ); const handleDeleteSyntheticGroup = useCallback( (vendorGroup: CartItem[]) => { cart.requestDeleteGroupConfirmation(vendorGroup); }, [cart], ); const vendorGroupCount = cart.vendorGroupIds.length; return ( cart.setOpen(false)} slotProps={{ paper: { className: 'legend-marketplace-cart-drawer', sx: { width: { xs: '100vw', sm: '400px' }, maxWidth: '90vw', marginTop: 'var(--legend-marketplace-header-height)', height: 'calc(100% - var(--legend-marketplace-header-height))', }, }, }} > Cart ({cart.cartSummary.total_items}) cart.setOpen(false)} size="medium" aria-label="Close cart" className="legend-marketplace-cart-drawer__close-btn" > {/* Collapse / Expand All controls */} {!cart.loadingState.isInProgress && cart.cartSummary.total_items > 0 && vendorGroupCount > 1 && ( )} {cart.loadingState.isInProgress && ( Loading cart... )} {!cart.loadingState.isInProgress && cart.cartSummary.total_items <= 0 && ( Your cart is empty )} {!cart.loadingState.isInProgress && cart.cartSummary.total_items > 0 && ( {vendorGroups.map( ({ vpId, parentItem, displayParent, addons, groupItems, isSynthetic, addonTotalPrice, addonLabel, }) => ( toggleVendor(vpId)} onDelete={handleDeleteItem} onDeleteSyntheticGroup={handleDeleteSyntheticGroup} disabled={cart.loadingState.isInProgress} /> {expandedVendors.has(vpId) && addons.map((addon, index) => ( ))} ), )} )} {!cart.loadingState.isInProgress && cart.cartSummary.total_items > 0 && ( )} {'Please Choose a Business Reason'} * Select a Reason {cart.cartSummary.total_items > 0 && !cart.businessReason && ( Select a business reason to continue. )} ); });