@finos/legend-application-marketplace
Version:
Legend Marketplace application core
260 lines • 22.8 kB
JavaScript
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