/** * Copyright (c) 2026-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 { type JSX, type SyntheticEvent, useState } from 'react'; import { Box, Button, Divider, FormControl, InputLabel, ListSubheader, MenuItem, Popover, Select, Tab, Tabs, TextField, Typography, type SelectChangeEvent, } from '@mui/material'; import { LegendUser, type UserSearchService } from '@finos/legend-shared'; import { UserSearchInput } from '@finos/legend-art'; import { OrderSearchStatus } from '@finos/legend-server-marketplace'; import { getOrderSearchStatusLabel, parseLastDaysInput, ORDER_SEARCH_DEFAULT_LAST_DAYS, ORDER_SEARCH_MAX_LAST_DAYS, ORDER_SEARCH_MIN_LAST_DAYS, } from '../../stores/orders/OrderHelpers.js'; import type { OrderSearchFormValues } from '../../stores/orders/OrderStore.js'; const OPEN_STATUS_OPTIONS = [ OrderSearchStatus.PENDING_APPROVAL, OrderSearchStatus.PENDING_FULFILLMENT, ]; const CLOSED_STATUS_OPTIONS = [ OrderSearchStatus.CANCELLED, OrderSearchStatus.COMPLETED, OrderSearchStatus.REJECTED, ]; enum SearchMode { ORDER_ID = 'ORDER_ID', OTHER_FILTERS = 'OTHER_FILTERS', } interface AdvancedOrderSearchPopoverProps { open: boolean; anchorEl: HTMLElement | null; onClose: () => void; onSearch: (filters: OrderSearchFormValues) => void; onClear: () => void; isSearching: boolean; hasActiveSearch: boolean; userSearchService: UserSearchService | undefined; } /** * Popover form (anchored to the search bar's "Advanced Search" tune icon) * used to search across the entire orders dataset via `POST /workflow/search/orders`, * rather than the current user's own open/closed orders. */ export const AdvancedOrderSearchPopover = ( props: AdvancedOrderSearchPopoverProps, ): JSX.Element => { const { open, anchorEl, onClose, onSearch, onClear, isSearching, hasActiveSearch, userSearchService, } = props; const [orderId, setOrderId] = useState(''); const [orderedBy, setOrderedBy] = useState(new LegendUser()); const [orderedFor, setOrderedFor] = useState(new LegendUser()); const [status, setStatus] = useState( OrderSearchStatus.ALL, ); const [lastDaysInput, setLastDaysInput] = useState(''); const [searchMode, setSearchMode] = useState(SearchMode.ORDER_ID); // The two tabs are mutually exclusive search modes: only the fields on the // active tab are used when searching, regardless of what was previously // entered on the other tab. const isOrderIdMode = searchMode === SearchMode.ORDER_ID; const canSearch = isOrderIdMode ? Boolean(orderId.trim()) : Boolean(orderedBy.id.trim()) || Boolean(orderedFor.id.trim()); const lastDays = parseLastDaysInput(lastDaysInput); const isLastDaysInvalid = lastDaysInput.trim() !== '' && lastDays === undefined; const handleSearchModeChange = ( _event: SyntheticEvent, value: SearchMode, ): void => { setSearchMode(value); }; const handleStatusChange = ( event: SelectChangeEvent, ): void => { setStatus(event.target.value); }; const handleSearch = (): void => { if (!canSearch || isSearching) { return; } if (isOrderIdMode) { onSearch({ orderId: orderId.trim(), orderedBy: undefined, orderedFor: undefined, status: OrderSearchStatus.ALL, lastDays: undefined, }); return; } onSearch({ orderId: undefined, orderedBy, orderedFor, status, lastDays, }); }; const handleClear = (): void => { setOrderId(''); setOrderedBy(new LegendUser()); setOrderedFor(new LegendUser()); setStatus(OrderSearchStatus.ALL); setLastDaysInput(''); setSearchMode(SearchMode.ORDER_ID); if (hasActiveSearch) { onClear(); } onClose(); }; return ( Advanced Search {isOrderIdMode ? ( setOrderId(event.target.value)} slotProps={{ inputLabel: { shrink: true } }} /> ) : ( <> Required: Fill at least one Status (Optional) { const nextValue = event.target.value; if (/^\d*$/.test(nextValue)) { setLastDaysInput(nextValue); } }} error={isLastDaysInvalid} helperText={ isLastDaysInvalid ? `Enter a value between ${ORDER_SEARCH_MIN_LAST_DAYS} and ${ORDER_SEARCH_MAX_LAST_DAYS}` : `Default ${ORDER_SEARCH_DEFAULT_LAST_DAYS}` } slotProps={{ htmlInput: { inputMode: 'numeric' }, }} /> )} ); };