UNPKG

@finos/legend-application-marketplace

Version:
260 lines 22.8 kB
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; /** * 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, useMemo, useRef, useState } from 'react'; import { Dialog, DialogTitle, DialogContent, DialogActions, Button, Typography, Box, IconButton, TextField, InputAdornment, Select, MenuItem, FormControl, InputLabel, Pagination, Divider, CircularProgress, } from '@mui/material'; import { CloseIcon, CheckCircleIcon, ArrowRightIcon, WarningIcon, SearchIcon, ArrowUpIcon, ArrowDownIcon, } from '@finos/legend-art'; import { TerminalItemType, RecommendationSource, SortOrder, } from '@finos/legend-server-marketplace'; import { RecommendedItemsCard } from './RecommendedItemsCard.js'; import { useLegendMarketplaceBaseStore } from '../../application/providers/LegendMarketplaceFrameworkProvider.js'; import { assertErrorThrown, LogEvent } from '@finos/legend-shared'; import { LEGEND_MARKETPLACE_APP_EVENT } from '../../__lib__/LegendMarketplaceAppEvent.js'; import { flowResult } from 'mobx'; import { toastManager } from '../Toast/CartToast.js'; const MAX_DISPLAY_ITEMS_COUNT = 10; const ITEMS_PER_PAGE_LIST = [10, 15, 25, 50]; const SERVER_SEARCH_PAGE_SIZE = 300; const ListHeader = (props) => (_jsxs(Box, { className: "recommended-addons-modal__list-header", children: [_jsx(Typography, { variant: "subtitle2", className: "recommended-addons-modal__header-name", children: props.headerName }), _jsx(Typography, { variant: "subtitle2", className: "recommended-addons-modal__header-provider", children: "Provider" }), _jsx(Typography, { variant: "subtitle2", className: "recommended-addons-modal__header-price", children: "Price (monthly)" }), _jsx(Typography, { variant: "subtitle2", className: "recommended-addons-modal__header-action", children: "Action" })] })); export const RecommendedAddOnsModal = observer((props) => { const { terminal, recommendedItems, message, showModal, setShowModal, onViewCart, onTerminalSelected, totalCount: initialTotalCount, } = props; const legendMarketplaceBaseStore = useLegendMarketplaceBaseStore(); const applicationStore = legendMarketplaceBaseStore.applicationStore; const cartUser = legendMarketplaceBaseStore.cartStore.cartUser; const [searchTerm, setSearchTerm] = useState(''); const [sortOrder, setSortOrder] = useState(); const [currentPage, setCurrentPage] = useState(1); const [itemsPerPage, setItemsPerPage] = useState(15); const [terminalSearchResults, setTerminalSearchResults] = useState(undefined); const [searchTotalCount, setSearchTotalCount] = useState(undefined); const [isSearching, setIsSearching] = useState(false); const [isAssociating, setIsAssociating] = useState(false); const [associatingItemId, setAssociatingItemId] = useState(undefined); const isTerminalAdded = terminal && terminal.terminalItemType === TerminalItemType.TERMINAL; const isAddOnAssociation = !isTerminalAdded; const headerName = isTerminalAdded ? 'Add-On Name' : 'Terminal Name'; const hasMultipleSources = useMemo(() => { const hasCartItems = recommendedItems.some((item) => item.source === RecommendationSource.CART); const hasInventoryItems = recommendedItems.some((item) => item.source === RecommendationSource.INVENTORY); const hasMarketplaceItems = recommendedItems.some((item) => item.source === RecommendationSource.MARKETPLACE); return ([hasCartItems, hasInventoryItems, hasMarketplaceItems].filter(Boolean) .length >= 2); }, [recommendedItems]); const cartSourceItems = useMemo(() => recommendedItems.filter((item) => item.source === RecommendationSource.CART), [recommendedItems]); const inventorySourceItems = useMemo(() => recommendedItems.filter((item) => item.source === RecommendationSource.INVENTORY), [recommendedItems]); const marketplaceSourceItems = useMemo(() => recommendedItems.filter((item) => item.source === RecommendationSource.MARKETPLACE), [recommendedItems]); const fetchVendorAddons = useCallback(async (query, sort, signal) => { if (!terminal || !isTerminalAdded) { return; } setIsSearching(true); try { const response = await legendMarketplaceBaseStore.marketplaceServerClient.searchVendorAddons(cartUser, terminal.providerName, { // SERVER_SEARCH_PAGE_SIZE is set high enough to cover all expected results and paginate client-side. page: 1, page_size: SERVER_SEARCH_PAGE_SIZE, search: query, ...(sort ? { sort_by_price: sort } : {}), }, signal); if (!signal?.aborted) { setTerminalSearchResults(response.marketplace_addons); setSearchTotalCount(response.total_count); } } catch (error) { assertErrorThrown(error); if (error.name === 'AbortError') { return; } applicationStore.logService.error(LogEvent.create(LEGEND_MARKETPLACE_APP_EVENT.SEARCH_VENDOR_ADDONS_FAILURE), error); setTerminalSearchResults(undefined); } finally { if (!signal?.aborted) { setIsSearching(false); } } }, [ terminal, isTerminalAdded, cartUser, legendMarketplaceBaseStore.marketplaceServerClient, applicationStore.logService, ]); const abortControllerRef = useRef(null); const triggerSearch = useCallback((query, sort) => { abortControllerRef.current?.abort(); if (!isTerminalAdded || !query.trim()) { setTerminalSearchResults(undefined); setSearchTotalCount(undefined); setIsSearching(false); return; } const controller = new AbortController(); abortControllerRef.current = controller; // eslint-disable-next-line no-void void fetchVendorAddons(query.trim(), sort, controller.signal); }, [isTerminalAdded, fetchVendorAddons]); const handleSearchAction = useCallback(() => { setCurrentPage(1); triggerSearch(searchTerm, sortOrder); }, [searchTerm, sortOrder, triggerSearch]); const filteredAndSortedItems = useMemo(() => { let items; if (isTerminalAdded && terminalSearchResults) { items = [...terminalSearchResults]; } else { items = [...recommendedItems]; if (!isTerminalAdded && searchTerm) { const search = searchTerm.toLowerCase(); items = items.filter((item) => item.productName.toLowerCase().includes(search) || item.providerName.toLowerCase().includes(search)); } } if (sortOrder && !(isTerminalAdded && terminalSearchResults)) { items.sort((a, b) => sortOrder === SortOrder.ASC ? a.price - b.price : b.price - a.price); } return items; }, [ recommendedItems, searchTerm, sortOrder, isTerminalAdded, terminalSearchResults, ]); const totalPages = Math.ceil(filteredAndSortedItems.length / itemsPerPage); const mandatoryAddOns = useMemo(() => filteredAndSortedItems .filter((i) => i.isMandatory && i.productName) .map((i) => i.productName), [filteredAndSortedItems]); const paginatedItems = useMemo(() => { const startIndex = (currentPage - 1) * itemsPerPage; const endIndex = startIndex + itemsPerPage; return filteredAndSortedItems.slice(startIndex, endIndex); }, [filteredAndSortedItems, currentPage, itemsPerPage]); const closeModal = useCallback(() => { setShowModal(false); setSearchTerm(''); setSortOrder(undefined); setCurrentPage(1); setTerminalSearchResults(undefined); setSearchTotalCount(undefined); setIsSearching(false); setIsAssociating(false); setAssociatingItemId(undefined); abortControllerRef.current?.abort(); }, [setShowModal]); const handleAssociateTerminal = useCallback((selectedTerminal) => { const associate = async () => { setIsAssociating(true); setAssociatingItemId(selectedTerminal.id); try { const cartRequest = legendMarketplaceBaseStore.cartStore.providerToCartRequest(selectedTerminal); const result = await flowResult(legendMarketplaceBaseStore.cartStore.addToCartWithAPI(cartRequest)); if (!result.success) { return; } if (result.recommendations && result.recommendations.length > 0 && onTerminalSelected) { closeModal(); onTerminalSelected(selectedTerminal, result.recommendations, result.message, result.totalCount); } else { closeModal(); } } catch (error) { assertErrorThrown(error); toastManager.error(`Failed to associate with ${selectedTerminal.productName}: ${error.message}`); } finally { setIsAssociating(false); setAssociatingItemId(undefined); } }; // eslint-disable-next-line no-void void associate(); }, [legendMarketplaceBaseStore.cartStore, onTerminalSelected, closeModal]); const handleViewCart = () => { onViewCart?.(); closeModal(); }; const handleSortChange = (event) => { const value = event.target.value; const newSortOrder = value ? value : undefined; setSortOrder(newSortOrder); setCurrentPage(1); if (isTerminalAdded && searchTerm.trim() && terminalSearchResults) { triggerSearch(searchTerm, newSortOrder); } }; const handlePageChange = (_event, page) => { setCurrentPage(page); }; const handleItemsPerPageChange = (event) => { setItemsPerPage(Number(event.target.value)); setCurrentPage(1); }; if (!showModal) { return null; } return (_jsxs(Dialog, { open: showModal, onClose: closeModal, maxWidth: "md", fullWidth: true, className: "recommended-addons-modal", children: [_jsxs(DialogTitle, { className: "recommended-addons-modal__header", children: [terminal?.terminalItemType === TerminalItemType.TERMINAL ? (_jsx(CheckCircleIcon, { className: "recommended-addons-modal__success-icon" })) : (_jsx(WarningIcon, { className: "recommended-addons-modal__warning-icon" })), _jsxs(Box, { className: "recommended-addons-modal__header-content", children: [_jsx(Typography, { variant: "h6", className: "recommended-addons-modal__title", children: terminal?.terminalItemType === TerminalItemType.TERMINAL ? 'Item Added Successfully' : 'Unable to Add Item' }), terminal && (_jsx(Typography, { variant: "body2", className: "recommended-addons-modal__subtitle", children: message }))] }), _jsx(IconButton, { onClick: closeModal, className: "recommended-addons-modal__close-btn", size: "large", children: _jsx(CloseIcon, {}) })] }), _jsxs(DialogContent, { className: "recommended-addons-modal__content", children: [mandatoryAddOns.length > 0 && (_jsxs(Box, { className: "recommended-addons-modal__alert", children: [_jsx(CheckCircleIcon, {}), _jsxs(Box, { children: [_jsx(Typography, { children: _jsxs("strong", { children: ["Mandatory Add-On", mandatoryAddOns.length > 1 ? 's' : '', ' ', "Included:"] }) }), mandatoryAddOns.length === 1 ? (_jsxs(Typography, { variant: "body2", children: [mandatoryAddOns[0], " Added To Cart"] })) : (_jsx(Box, { component: "ul", sx: { margin: '0.4rem 0 0', paddingLeft: '2rem' }, children: mandatoryAddOns.map((name) => (_jsx(Typography, { component: "li", variant: "body2", sx: { lineHeight: 1.6 }, children: name }, name))) }))] })] })), _jsxs(Box, { className: "recommended-addons-modal__content-header", children: [_jsx(Typography, { variant: "h6", className: "recommended-addons-modal__section-title", children: terminal?.terminalItemType === TerminalItemType.TERMINAL ? `Recommended Add-Ons for ${terminal.providerName}` : terminal ? `Recommended Terminals for ${terminal.providerName}` : '' }), _jsx(Typography, { variant: "body2", className: "recommended-addons-modal__section-description", children: terminal?.terminalItemType === TerminalItemType.TERMINAL ? 'Enhance your terminal with these add-ons' : 'You must order a terminal license with this add-on' })] }), recommendedItems.length === 0 ? (_jsx(Box, { className: "recommended-addons-modal__empty-state", children: _jsx(Typography, { variant: "body1", children: isTerminalAdded ? 'No recommended add-ons available for this terminal.' : 'No recommended terminals available for this add-on.' }) })) : isAddOnAssociation && hasMultipleSources ? (_jsxs(Box, { className: "recommended-addons-modal__association-content", children: [cartSourceItems.length > 0 && (_jsxs(Box, { className: "recommended-addons-modal__source-section", children: [_jsxs(Box, { className: "recommended-addons-modal__source-header", children: [_jsx(Typography, { variant: "h6", className: "recommended-addons-modal__source-title", children: "From Your Cart" }), _jsx(Typography, { variant: "body2", className: "recommended-addons-modal__source-description", children: "Select a terminal from your cart to associate" })] }), _jsxs(Box, { className: "recommended-addons-modal__list", children: [_jsx(ListHeader, { headerName: headerName }), cartSourceItems.map((item) => (_jsx(RecommendedItemsCard, { recommendedItem: item, onSelect: handleAssociateTerminal, isSelecting: isAssociating, selectedItemId: associatingItemId }, item.id)))] })] })), cartSourceItems.length > 0 && (inventorySourceItems.length > 0 || marketplaceSourceItems.length > 0) && (_jsx(Divider, { sx: { my: 2 } })), inventorySourceItems.length > 0 && (_jsxs(Box, { className: "recommended-addons-modal__source-section", children: [_jsxs(Box, { className: "recommended-addons-modal__source-header", children: [_jsx(Typography, { variant: "h6", className: "recommended-addons-modal__source-title", children: "From Your Inventory" }), _jsx(Typography, { variant: "body2", className: "recommended-addons-modal__source-description", children: "Select a terminal from your existing inventory to associate" })] }), _jsxs(Box, { className: "recommended-addons-modal__list", children: [_jsx(ListHeader, { headerName: headerName }), inventorySourceItems.map((item) => (_jsx(RecommendedItemsCard, { recommendedItem: item, onSelect: handleAssociateTerminal, isSelecting: isAssociating, selectedItemId: associatingItemId }, item.id)))] })] })), (cartSourceItems.length > 0 || inventorySourceItems.length > 0) && marketplaceSourceItems.length > 0 && _jsx(Divider, { sx: { my: 2 } }), marketplaceSourceItems.length > 0 && (_jsxs(Box, { className: "recommended-addons-modal__source-section", children: [_jsxs(Box, { className: "recommended-addons-modal__source-header", children: [_jsx(Typography, { variant: "h6", className: "recommended-addons-modal__source-title", children: "From Marketplace" }), _jsx(Typography, { variant: "body2", className: "recommended-addons-modal__source-description", children: "Explore other available terminal options from the marketplace" })] }), _jsxs(Box, { className: "recommended-addons-modal__list", children: [_jsx(ListHeader, { headerName: headerName }), marketplaceSourceItems.map((item) => (_jsx(RecommendedItemsCard, { recommendedItem: item, onSelect: handleAssociateTerminal, isSelecting: isAssociating, selectedItemId: associatingItemId }, item.id)))] })] }))] })) : (_jsxs(_Fragment, { children: [_jsxs(Box, { className: "recommended-addons-modal__filter-controls", children: [_jsx(TextField, { size: "medium", placeholder: terminal?.terminalItemType === TerminalItemType.TERMINAL ? 'Search by Add-On name...' : 'Search by Terminal name...', value: searchTerm, onChange: (e) => { setSearchTerm(e.target.value); setCurrentPage(1); if (!e.target.value.trim()) { setTerminalSearchResults(undefined); setSearchTotalCount(undefined); setIsSearching(false); abortControllerRef.current?.abort(); } }, onKeyDown: (e) => { if (e.key === 'Enter') { handleSearchAction(); } }, className: "recommended-addons-modal__search-field", slotProps: { input: { endAdornment: (_jsx(InputAdornment, { position: "end", children: _jsx(IconButton, { onClick: handleSearchAction, size: "small", edge: "end", children: _jsx(SearchIcon, {}) }) })), }, } }), _jsxs(FormControl, { size: "medium", className: "recommended-addons-modal__sort-select", sx: { minWidth: 180 }, children: [_jsx(InputLabel, { id: "recommended-addons-sort-label", sx: { fontSize: '1rem' }, children: "Sort by Price" }), _jsxs(Select, { labelId: "recommended-addons-sort-label", value: sortOrder ?? '', label: "Sort by Price", onChange: handleSortChange, sx: { fontSize: '1rem' }, children: [_jsx(MenuItem, { value: "", sx: { fontSize: '1rem' }, children: _jsx("em", { children: "None" }) }), _jsx(MenuItem, { value: SortOrder.ASC, sx: { fontSize: '1rem' }, children: _jsxs(Box, { display: "flex", alignItems: "center", children: [_jsx(ArrowUpIcon, { fontSize: "small" }), _jsx(Typography, { sx: { ml: 0.5, fontSize: '1rem' }, children: "Low to High" })] }) }), _jsx(MenuItem, { value: SortOrder.DESC, sx: { fontSize: '1rem' }, children: _jsxs(Box, { display: "flex", alignItems: "center", children: [_jsx(ArrowDownIcon, { fontSize: "small" }), _jsx(Typography, { sx: { ml: 0.5, fontSize: '1rem' }, children: "High to Low" })] }) })] })] }), filteredAndSortedItems.length > MAX_DISPLAY_ITEMS_COUNT && (_jsxs(FormControl, { size: "medium", className: "recommended-addons-modal__items-per-page-select", sx: { minWidth: 120 }, children: [_jsx(InputLabel, { id: "items-per-page-label", sx: { fontSize: '1rem' }, children: "Items per page" }), _jsx(Select, { labelId: "items-per-page-label", value: itemsPerPage, label: "Items per page", onChange: handleItemsPerPageChange, sx: { fontSize: '1rem' }, children: ITEMS_PER_PAGE_LIST.map((items) => (_jsx(MenuItem, { value: items, sx: { fontSize: '1rem' }, children: items }, items))) })] }))] }), isSearching ? (_jsxs(Box, { className: "recommended-addons-modal__empty-state", display: "flex", alignItems: "center", justifyContent: "center", children: [_jsx(CircularProgress, { size: 24, sx: { mr: 1 } }), _jsx(Typography, { variant: "body1", children: "Searching..." })] })) : filteredAndSortedItems.length === 0 ? (_jsx(Box, { className: "recommended-addons-modal__empty-state", children: _jsx(Typography, { variant: "body1", children: "No items match your search criteria." }) })) : (_jsxs(_Fragment, { children: [_jsx(Box, { className: "recommended-addons-modal__list-info", children: _jsxs(Typography, { variant: "body2", sx: { fontSize: '1.4rem', color: 'var(--color-dark-grey-300)', }, children: ["Showing ", (currentPage - 1) * itemsPerPage + 1, " -", ' ', Math.min(currentPage * itemsPerPage, filteredAndSortedItems.length), ' ', "of", ' ', (terminalSearchResults ? searchTotalCount : initialTotalCount) ?? filteredAndSortedItems.length, ' ', "items"] }) }), _jsxs(Box, { className: "recommended-addons-modal__list", children: [_jsx(ListHeader, { headerName: headerName }), paginatedItems.map((item) => (_jsx(RecommendedItemsCard, { recommendedItem: item, ...(isAddOnAssociation && { onSelect: handleAssociateTerminal, isSelecting: isAssociating, selectedItemId: associatingItemId, }) }, item.id)))] }), totalPages > 1 && (_jsx(Box, { className: "recommended-addons-modal__pagination", children: _jsx(Pagination, { count: totalPages, page: currentPage, onChange: handlePageChange, color: "primary", size: "large", showFirstButton: true, showLastButton: true }) }))] }))] }))] }), _jsxs(DialogActions, { className: "recommended-addons-modal__footer", children: [_jsx(Button, { variant: "outlined", onClick: closeModal, className: "recommended-addons-modal__close-button", children: isAddOnAssociation ? 'Cancel' : 'Close' }), onViewCart && !isAddOnAssociation && (_jsx(Button, { variant: "contained", endIcon: _jsx(ArrowRightIcon, {}), onClick: handleViewCart, className: "recommended-addons-modal__view-cart-button", children: "View Cart" }))] })] })); }); //# sourceMappingURL=RecommendedAddOnsModal.js.map