diff --git a/src/screens/Analytics/GlobalSearch/GlobalSearchBar.res b/src/screens/Analytics/GlobalSearch/GlobalSearchBar.res index 45620c037..6d52ae644 100644 --- a/src/screens/Analytics/GlobalSearch/GlobalSearchBar.res +++ b/src/screens/Analytics/GlobalSearch/GlobalSearchBar.res @@ -1,205 +1,9 @@ -module RenderedComponent = { - @react.component - let make = (~ele, ~searchText) => { - open LogicUtils - - listOfMatchedText(ele, searchText) - ->Array.mapWithIndex((item, i) => { - if ( - String.toLowerCase(item) == String.toLowerCase(searchText) && String.length(searchText) > 0 - ) { - Int.toString} - className="border-searched_text_border bg-yellow-searched_text font-medium text-fs-14 text-lightgray_background opacity-50"> - {item->React.string} - - } else { - Int.toString} - className="font-medium text-fs-14 text-lightgray_background opacity-50"> - {item->React.string} - - } - }) - ->React.array - } -} - -module SearchBox = { - @react.component - let make = (~openModalOnClickHandler) => { - let shortcutText = Window.Navigator.platform->String.includes("Mac") ? "Cmd + K" : "Ctrl + K" - let isMobileView = MatchMedia.useMobileChecker() - - if isMobileView { - - } else { -
-
- -

{"Search"->React.string}

-
-
{shortcutText->React.string}
-
- } - } -} - -module EmptyResult = { - @react.component - let make = (~prefix, ~searchText) => { - -
- no-result -
- {`No Results for " ${searchText} "`->React.string} -
-
-
- } -} - -module OptionsWrapper = { - open HeadlessUI - @react.component - let make = (~children) => { - - - {_ => {children}} - - - } -} - -module OptionWrapper = { - open HeadlessUI - @react.component - let make = (~index, ~value, ~children) => { - let activeClasses = isActive => { - let borderClass = isActive ? "bg-gray-100 dark:bg-jp-gray-960" : "" - `group flex items-center w-full p-2 text-sm rounded-lg ${borderClass}` - } - - Int.toString} value> - {props => { -
activeClasses}> {children}
- }} -
- } -} - -module ModalWrapper = { - @react.component - let make = (~showModal, ~setShowModal, ~children) => { - - - {children} - - - } -} - -module SearchResultsComponent = { - open GlobalSearchTypes - open LogicUtils - - @react.component - let make = (~searchResults, ~searchText, ~setShowModal) => { - React.useEffect(() => { - let onKeyPress = event => { - let keyPressed = event->ReactEvent.Keyboard.key - - if keyPressed == "Enter" { - let redirectLink = `/search?query=${searchText}` - if redirectLink->isNonEmptyString { - setShowModal(_ => false) - GlobalVars.appendDashboardPath(~url=redirectLink)->RescriptReactRouter.push - } - } - } - Window.addEventListener("keydown", onKeyPress) - Some(() => Window.removeEventListener("keydown", onKeyPress)) - }, []) - - - {searchResults - ->Array.mapWithIndex((section: resultType, index) => { - let borderClass = - index !== searchResults->Array.length - 1 ? "border-b-1 dark:border-jp-gray-960" : "" - getSectionHeader} - className={`px-3 mb-3 py-1 ${borderClass}`}> - getSectionHeader}-${index->Belt.Int.toString}`} - className="text-lightgray_background px-2 pb-1 flex justify-between"> -
- {section.section->getSectionHeader->String.toUpperCase->React.string} -
-
- {setShowModal(_ => false)}} - textStyleClass="text-xs" - searchText - /> -
-
- {section.results - ->Array.mapWithIndex((item, i) => { - let elementsArray = item.texts - - - {elementsArray - ->Array.mapWithIndex( - (item, index) => { - let elementValue = item->JSON.Decode.string->Option.getOr("") - isNonEmptyString} key={index->Int.toString}> - - = 0 && index < elementsArray->Array.length - 1}> - - {">"->React.string} - - - - }, - ) - ->React.array} - - }) - ->React.array} -
- }) - ->React.array} -
- } -} - @react.component let make = () => { open GlobalSearchTypes open GlobalSearchBarUtils - open HeadlessUI open LogicUtils + open GlobalSearchBarHelper let getURL = APIUtils.useGetURL() let prefix = useUrlPrefix() @@ -208,17 +12,22 @@ let make = () => { let (state, setState) = React.useState(_ => Idle) let (showModal, setShowModal) = React.useState(_ => false) let (searchText, setSearchText) = React.useState(_ => "") + let (activeFilter, setActiveFilter) = React.useState(_ => "") + let (localSearchText, setLocalSearchText) = React.useState(_ => "") + let (selectedOption, setSelectedOption) = React.useState(_ => ""->getDefaultOption) + let (allOptions, setAllOptions) = React.useState(_ => []) + let (categorieSuggestionResponse, setCategorieSuggestionResponse) = React.useState(_ => + Dict.make()->JSON.Encode.object + ) let (searchResults, setSearchResults) = React.useState(_ => []) let merchentDetails = HSwitchUtils.useMerchantDetailsValue() let isReconEnabled = merchentDetails.recon_status === Active let hswitchTabs = SidebarValues.useGetSidebarValues(~isReconEnabled) - let searchText = searchText->String.trim let loader = LottieFiles.useLottieJson("loader-circle.json") let {globalSearch} = HyperswitchAtom.featureFlagAtom->Recoil.useRecoilValueFromAtom let {userHasAccess} = GroupACLHooks.useUserGroupACLHook() let isShowRemoteResults = globalSearch && userHasAccess(~groupAccess=OperationsView) === Access let mixpanelEvent = MixpanelHook.useSendEvent() - let {userInfo: {merchantId}} = React.useContext(UserInfoProvider.defaultContext) let redirectOnSelect = element => { mixpanelEvent(~eventName="global_search_redirect") @@ -229,13 +38,47 @@ let make = () => { } } + React.useEffect(() => { + let onKeyPress = event => { + let keyPressed = event->ReactEvent.Keyboard.key + + if keyPressed == "Enter" { + selectedOption->redirectOnSelect + } + } + Window.addEventListener("keydown", onKeyPress) + Some(() => Window.removeEventListener("keydown", onKeyPress)) + }, []) + + let getCategoryOptions = async () => { + setState(_ => Loading) + try { + let paymentsUrl = getURL( + ~entityName=ANALYTICS_FILTERS, + ~methodType=Post, + ~id=Some("payments"), + ) + + let paymentsResponse = await fetchDetails( + paymentsUrl, + paymentsGroupByNames->getFilterBody, + Post, + ) + setCategorieSuggestionResponse(_ => paymentsResponse) + + setState(_ => Idle) + } catch { + | _ => setState(_ => Idle) + } + } + let getSearchResults = async results => { try { let url = getURL(~entityName=GLOBAL_SEARCH, ~methodType=Post) - let body = generateSearchBody(~searchText, ~merchant_id={merchantId}) + let body = searchText->generateQuery - let response = await fetchDetails(url, body, Post) + let response = await fetchDetails(url, body->JSON.Encode.object, Post) let local_results = [] results->Array.forEach((item: resultType) => { @@ -258,7 +101,6 @@ let make = () => { if results->Array.length > 0 { let defaultItem = searchText->getDefaultResult - let arr = [defaultItem]->Array.concat(results) setSearchResults(_ => arr) @@ -267,14 +109,21 @@ let make = () => { } setState(_ => Loaded) } catch { - | _ => setState(_ => Failed) + | _ => setState(_ => Loaded) } } + React.useEffect(() => { + let allOptions = searchResults->getAllOptions + setAllOptions(_ => allOptions) + setSelectedOption(_ => searchText->getDefaultOption) + None + }, [searchResults]) + React.useEffect(_ => { let results = [] - if searchText->String.length > 0 { + if searchText->String.length > 0 && activeFilter->LogicUtils.isEmptyString { setState(_ => Loading) let localResults: resultType = searchText->getLocalMatchedResults(hswitchTabs) @@ -287,7 +136,6 @@ let make = () => { } else { if results->Array.length > 0 { let defaultItem = searchText->getDefaultResult - let arr = [defaultItem]->Array.concat(results) setSearchResults(_ => arr) @@ -306,10 +154,14 @@ let make = () => { React.useEffect(_ => { setSearchText(_ => "") + setLocalSearchText(_ => "") + setActiveFilter(_ => "") None }, [showModal]) React.useEffect(() => { + getCategoryOptions()->ignore + let onKeyPress = event => { let metaKey = event->ReactEvent.Keyboard.metaKey let keyPressed = event->ReactEvent.Keyboard.key @@ -331,12 +183,19 @@ let make = () => { setShowModal(_ => true) } - let borderClass = searchText->String.length > 0 ? "border-b dark:border-jp-gray-960" : "" - let setGlobalSearchText = ReactDebounce.useDebounced(value => { setSearchText(_ => value) }, ~wait=500) + React.useEffect(() => { + setGlobalSearchText(localSearchText) + None + }, [localSearchText]) + + let setFilterText = ReactDebounce.useDebounced(value => { + setActiveFilter(_ => value) + }, ~wait=500) + let leftIcon = switch state { | Loading =>
@@ -350,58 +209,42 @@ let make = () => {
} - let modalSearchBox = - -
- {leftIcon} - { - setGlobalSearchText(event["target"]["value"]) - }} - /> -
{ - setShowModal(_ => false) - }}> - {"Esc"->React.string} - -
-
-
-
- { - element->redirectOnSelect - }}> - {_ => { - <> - {modalSearchBox} - {switch state { - | Loading => -
- -
- | _ => - if searchText->isNonEmptyString && searchResults->Array.length === 0 { - - } else { - - } - }} - +
+ + {switch getViewType(~state, ~searchResults) { + | Load => +
+ +
+ | Results => + + | FiltersSugsestions => + + | EmptyResult => }} - +
diff --git a/src/screens/Analytics/GlobalSearch/GlobalSearchBarHelper.res b/src/screens/Analytics/GlobalSearch/GlobalSearchBarHelper.res new file mode 100644 index 000000000..f71f585a7 --- /dev/null +++ b/src/screens/Analytics/GlobalSearch/GlobalSearchBarHelper.res @@ -0,0 +1,474 @@ +open LogicUtils +open GlobalSearchTypes +module RenderedComponent = { + @react.component + let make = (~ele, ~searchText) => { + listOfMatchedText(ele, searchText) + ->Array.mapWithIndex((item, i) => { + if ( + String.toLowerCase(item) == String.toLowerCase(searchText) && String.length(searchText) > 0 + ) { + Int.toString} + className="border-searched_text_border bg-yellow-searched_text font-medium text-fs-14 text-lightgray_background opacity-50"> + {item->React.string} + + } else { + Int.toString} + className="font-medium text-fs-14 text-lightgray_background opacity-50"> + {item->React.string} + + } + }) + ->React.array + } +} + +module SearchBox = { + @react.component + let make = (~openModalOnClickHandler) => { + let shortcutText = Window.Navigator.platform->String.includes("Mac") ? "Cmd + K" : "Ctrl + K" + let isMobileView = MatchMedia.useMobileChecker() + + if isMobileView { + + } else { +
+
+ +

{"Search"->React.string}

+
+
{shortcutText->React.string}
+
+ } + } +} + +module EmptyResult = { + open FramerMotion.Motion + @react.component + let make = (~prefix, ~searchText) => { +
+
+ no-result +
+ {`No Results for " ${searchText} "`->React.string} +
+
+
+ } +} + +module OptionWrapper = { + @react.component + let make = (~index, ~value, ~children, ~selectedOption, ~redirectOnSelect) => { + let activeClass = value == selectedOption ? "bg-gray-100 rounded-lg p-2 group items-center" : "" +
value->redirectOnSelect} + className={`flex ${activeClass} flex-row truncate hover:bg-gray-100 cursor-pointer hover:rounded-lg p-2 group items-center`} + key={index->Int.toString}> + {children} +
+ } +} + +module ModalWrapper = { + open FramerMotion.Motion + @react.component + let make = (~showModal, ~setShowModal, ~children) => { + +
+ {children} +
+
+ } +} + +module ShowMoreLink = { + @react.component + let make = ( + ~section: resultType, + ~cleanUpFunction=() => {()}, + ~textStyleClass="", + ~searchText, + ) => { + 10}> + { + let totalCount = section.total_results + let suffix = totalCount > 1 ? "s" : "" + let linkText = `View ${totalCount->Int.toString} result${suffix}` + + switch section.section { + | SessionizerPaymentAttempts + | SessionizerPaymentIntents + | SessionizerPaymentRefunds + | SessionizerPaymentDisputes + | PaymentAttempts + | PaymentIntents + | Refunds + | Disputes => +
{ + let link = switch section.section { + | PaymentAttempts => `payment-attempts?query=${searchText}&domain=payment_attempts` + | SessionizerPaymentAttempts => + `payment-attempts?query=${searchText}&domain=sessionizer_payment_attempts` + | PaymentIntents => `payment-intents?query=${searchText}&domain=payment_intents` + | SessionizerPaymentIntents => + `payment-intents?query=${searchText}&domain=sessionizer_payment_intents` + | Refunds => `refunds-global?query=${searchText}&domain=refunds` + | SessionizerPaymentRefunds => + `refunds-global?query=${searchText}&domain=sessionizer_refunds` + | Disputes => `dispute-global?query=${searchText}&domain=disputes` + | SessionizerPaymentDisputes => + `dispute-global?query=${searchText}&domain=sessionizer_disputes` + | Local + | Others + | Default => "" + } + GlobalVars.appendDashboardPath(~url=link)->RescriptReactRouter.push + cleanUpFunction() + }} + className={`font-medium cursor-pointer underline underline-offset-2 opacity-50 ${textStyleClass}`}> + {linkText->React.string} +
+ | Local + | Default + | Others => React.null + } + } +
+ } +} + +module SearchResultsComponent = { + open FramerMotion.Motion + @react.component + let make = (~searchResults, ~searchText, ~setShowModal, ~selectedOption, ~redirectOnSelect) => { + let borderClass = searchResults->Array.length > 0 ? "border-t dark:border-jp-gray-960" : "" + +
+ {searchResults + ->Array.mapWithIndex((section: resultType, index) => { +
getSectionHeader} ${Int.toString(index)}`} + className={`px-3 mb-3 py-1`}> +
getSectionHeader}-${index->Belt.Int.toString}`} + className="text-lightgray_background px-2 pb-1 flex justify-between "> +
+ {section.section->getSectionHeader->String.toUpperCase->React.string} +
+ {setShowModal(_ => false)}} + textStyleClass="text-xs" + searchText + /> +
+ {section.results + ->Array.mapWithIndex((item, i) => { + let elementsArray = item.texts + + {elementsArray + ->Array.mapWithIndex( + (item, index) => { + let elementValue = item->JSON.Decode.string->Option.getOr("") + isNonEmptyString} key={index->Int.toString}> + + = 0 && index < elementsArray->Array.length - 1}> + + {">"->React.string} + + + + }, + ) + ->React.array} + + }) + ->React.array} +
+ }) + ->React.array} +
+ } +} + +let sidebarScrollbarCss = ` + @supports (-webkit-appearance: none){ + .sidebar-scrollbar { + scrollbar-width: auto; + scrollbar-color: #8a8c8f; + } + + .sidebar-scrollbar::-webkit-scrollbar { + display: block; + overflow: scroll; + height: 4px; + width: 5px; + } + + .sidebar-scrollbar::-webkit-scrollbar-thumb { + background-color: #8a8c8f; + border-radius: 3px; + } + + .sidebar-scrollbar::-webkit-scrollbar-track { + display: none; + } +} + ` + +module FilterResultsComponent = { + open GlobalSearchBarUtils + open FramerMotion.Motion + @react.component + let make = ( + ~categorySuggestions: array, + ~activeFilter, + ~setActiveFilter, + ~searchText, + ~setLocalSearchText, + ) => { + let filterKey = activeFilter->String.split(":")->getValueFromArray(0, "") + + let filters = categorySuggestions->Array.filter(category => { + if !(activeFilter->isEmptyString) { + if searchText->String.charAt(searchText->String.length - 1) == ":" { + `${category.categoryType->getcategoryFromVariant}:` == `${filterKey}:` + } else { + category.categoryType + ->getcategoryFromVariant + ->String.includes(filterKey) + } + } else { + true + } + }) + + let checkFilterKey = list => { + switch list->Array.get(0) { + | Some(value) => + value.categoryType->getcategoryFromVariant === filterKey && value.options->Array.length > 0 + | _ => false + } + } + + Array.length > 0}> +
+
+ {"Suggested Filters"->String.toUpperCase->React.string} +
+
+ Array.length === 1 && filters->checkFilterKey}> +
+ + {switch filters->Array.get(0) { + | Some(value) => + value.options + ->Array.map(option => { +
{ + let saparater = + searchText->String.charAt(searchText->String.length - 1) == ":" ? "" : ":" + setLocalSearchText(_ => `${searchText}${saparater}${option}`) + setActiveFilter(_ => "") + }}> +
+ + {`${value.categoryType + ->getcategoryFromVariant + ->String.toLocaleLowerCase} : ${option}`->React.string} + +
+
+ }) + ->React.array + | _ => React.null + }} +
+
+ Array.length === 1 && filters->checkFilterKey)}> + {filters + ->Array.map(category => { +
{ + let newFilter = category.categoryType->getcategoryFromVariant + let lastString = searchText->String.charAt(searchText->String.length - 1) + if activeFilter->isNonEmptyString && lastString !== ":" { + let end = searchText->String.length - activeFilter->String.length + let newText = searchText->String.substring(~start=0, ~end) + setLocalSearchText(_ => `${newText} ${newFilter}:`) + setActiveFilter(_ => newFilter) + } else if lastString !== ":" { + setLocalSearchText(_ => `${searchText} ${newFilter}:`) + setActiveFilter(_ => newFilter) + } + }}> +
+ + {`${category.categoryType + ->getcategoryFromVariant + ->String.toLocaleLowerCase} : `->React.string} + +
+
{category.placeholder->React.string}
+
+ }) + ->React.array} +
+
+
+
+ } +} + +module ModalSearchBox = { + open FramerMotion.Motion + @react.component + let make = ( + ~leftIcon, + ~setShowModal, + ~setFilterText, + ~localSearchText, + ~setLocalSearchText, + ~allOptions, + ~selectedOption, + ~setSelectedOption, + ~redirectOnSelect, + ) => { + let (errorMessage, setErrorMessage) = React.useState(_ => "") + + let input: ReactFinalForm.fieldRenderPropsInput = { + { + name: "global_search", + onBlur: _ => (), + onChange: ev => { + let value = {ev->ReactEvent.Form.target}["value"] + setLocalSearchText(_ => value) + }, + onFocus: _ => (), + value: localSearchText->JSON.Encode.string, + checked: false, + } + } + + let handleKeyDown = e => { + open ReactEvent.Keyboard + + let index = allOptions->Array.findIndex(item => { + item == selectedOption + }) + + if e->keyCode == 40 { + let newIndex = + index == allOptions->Array.length - 1 ? 0 : Int.mod(index + 1, allOptions->Array.length) + switch allOptions->Array.get(newIndex) { + | Some(val) => setSelectedOption(_ => val) + | _ => () + } + } else if e->keyCode == 38 { + let newIndex = + index === 0 ? allOptions->Array.length - 1 : Int.mod(index - 1, allOptions->Array.length) + switch allOptions->Array.get(newIndex) { + | Some(val) => setSelectedOption(_ => val) + | _ => () + } + } else if e->keyCode == 13 { + selectedOption->redirectOnSelect + } + + if e->keyCode === 32 { + setFilterText("") + } else { + let values = localSearchText->String.split(" ") + let filter = values->getValueFromArray(values->Array.length - 1, "") + setFilterText(filter) + } + } + + let validateForm = _values => { + let errors = Dict.make() + let lastChar = localSearchText->String.charCodeAt(localSearchText->String.length - 1) + if localSearchText->GlobalSearchBarUtils.validateQuery && lastChar == 32.0 { + setErrorMessage(_ => "Multiple free-text terms found") + } else if !(localSearchText->GlobalSearchBarUtils.validateQuery) { + setErrorMessage(_ => "") + } + errors->JSON.Encode.object + } + +
JSON.Encode.object} + validate={values => values->validateForm} + onSubmit={(_, _) => Nullable.null->Promise.resolve}> + + { +
+
+ {leftIcon} +
+ +
+
{ + setShowModal(_ => false) + }}> + {"Esc"->React.string} + +
+
+ isNonEmptyString}> +
+ {errorMessage->React.string} +
+
+
+ }, + ~isRequired=false, + )} + /> +
+
+ } +} diff --git a/src/screens/Analytics/GlobalSearch/GlobalSearchBarUtils.res b/src/screens/Analytics/GlobalSearch/GlobalSearchBarUtils.res index 17ba6e384..860ebf918 100644 --- a/src/screens/Analytics/GlobalSearch/GlobalSearchBarUtils.res +++ b/src/screens/Analytics/GlobalSearch/GlobalSearchBarUtils.res @@ -1,57 +1,7 @@ -module ShowMoreLink = { - open GlobalSearchTypes - @react.component - let make = ( - ~section: resultType, - ~cleanUpFunction=() => {()}, - ~textStyleClass="", - ~searchText, - ) => { - 10}> - { - let linkText = `View ${section.total_results->Int.toString} result${section.total_results > 1 - ? "s" - : ""}` - - switch section.section { - | Local - | Default - | Others - | SessionizerPaymentAttempts - | SessionizerPaymentIntents - | SessionizerPaymentRefunds - | SessionizerPaymentDisputes => React.null - | PaymentAttempts | PaymentIntents | Refunds | Disputes => -
{ - let link = switch section.section { - | PaymentAttempts => `payment-attempts?query=${searchText}` - | PaymentIntents => `payment-intents?query=${searchText}` - | Refunds => `refunds-global?query=${searchText}` - | Disputes => `dispute-global?query=${searchText}` - | Local - | Others - | Default - | SessionizerPaymentAttempts - | SessionizerPaymentIntents - | SessionizerPaymentRefunds - | SessionizerPaymentDisputes => "" - } - GlobalVars.appendDashboardPath(~url=link)->RescriptReactRouter.push - cleanUpFunction() - }} - className={`font-medium cursor-pointer underline underline-offset-2 ${textStyleClass}`}> - {linkText->React.string} -
- } - } -
- } -} +open GlobalSearchTypes +open LogicUtils let matchInSearchOption = (searchOptions, searchText, name, link, ~sectionName) => { - open GlobalSearchTypes - open LogicUtils searchOptions ->Option.getOr([]) ->Array.filter(item => { @@ -73,8 +23,6 @@ let matchInSearchOption = (searchOptions, searchText, name, link, ~sectionName) } let getLocalMatchedResults = (searchText, tabs) => { - open LogicUtils - open GlobalSearchTypes open SidebarTypes let results = tabs->Array.reduce([], (acc, item) => { switch item { @@ -160,9 +108,6 @@ let getLocalMatchedResults = (searchText, tabs) => { } let getElements = (hits, section) => { - open GlobalSearchTypes - open LogicUtils - let getAmount = (value, amountKey, currencyKey) => `${value->getFloat(amountKey, 0.0)->Belt.Float.toString} ${value->getString(currencyKey, "")}` @@ -177,7 +122,7 @@ let getElements = (hits, section) => { } switch section { - | PaymentAttempts => + | PaymentAttempts | SessionizerPaymentAttempts => hits->Array.map(item => { let (payId, amount, status, profileId) = item->getValues @@ -186,7 +131,7 @@ let getElements = (hits, section) => { redirect_link: `/payments/${payId}/${profileId}`->JSON.Encode.string, } }) - | PaymentIntents => + | PaymentIntents | SessionizerPaymentIntents => hits->Array.map(item => { let (payId, amount, status, profileId) = item->getValues @@ -196,7 +141,7 @@ let getElements = (hits, section) => { } }) - | Refunds => + | Refunds | SessionizerPaymentRefunds => hits->Array.map(item => { let value = item->JSON.Decode.object->Option.getOr(Dict.make()) let refId = value->getString("refund_id", "") @@ -209,7 +154,7 @@ let getElements = (hits, section) => { redirect_link: `/refunds/${refId}/${profileId}`->JSON.Encode.string, } }) - | Disputes => + | Disputes | SessionizerPaymentDisputes => hits->Array.map(item => { let value = item->JSON.Decode.object->Option.getOr(Dict.make()) let disId = value->getString("dispute_id", "") @@ -224,18 +169,35 @@ let getElements = (hits, section) => { }) | Local | Others - | Default - | SessionizerPaymentAttempts - | SessionizerPaymentIntents - | SessionizerPaymentRefunds - | SessionizerPaymentDisputes => [] + | Default => [] + } +} + +let getItemFromArray = (results, key1, key2, resultsData) => { + switch (resultsData->Dict.get(key1), resultsData->Dict.get(key2)) { + | (Some(data), Some(sessionizerData)) => { + let intentsCount = data.total_results + let sessionizerCount = sessionizerData.total_results + if intentsCount > 0 && sessionizerCount > 0 { + if intentsCount >= sessionizerCount { + results->Array.push(data) + } else { + results->Array.push(sessionizerData) + } + } else if intentsCount > 0 { + results->Array.push(data) + } else { + results->Array.push(sessionizerData) + } + } + | (None, Some(sessionizerData)) => results->Array.push(sessionizerData) + | (Some(data), None) => results->Array.push(data) + | _ => () } } let getRemoteResults = json => { - open GlobalSearchTypes - open LogicUtils - let results = [] + let data = Dict.make() json ->JSON.Decode.array @@ -245,21 +207,46 @@ let getRemoteResults = json => { let section = value->getString("index", "")->getSectionVariant let hints = value->getArrayFromDict("hits", []) let total_results = value->getInt("count", hints->Array.length) + let key = value->getString("index", "") if hints->Array.length > 0 { - results->Array.push({ - section, - results: hints->getElements(section), - total_results, - }) + data->Dict.set( + key, + { + section, + results: hints->getElements(section), + total_results, + }, + ) } }) + let results = [] + + // intents + let key1 = PaymentIntents->getSectionIndex + let key2 = SessionizerPaymentIntents->getSectionIndex + getItemFromArray(results, key1, key2, data) + + // Attempts + let key1 = PaymentAttempts->getSectionIndex + let key2 = SessionizerPaymentAttempts->getSectionIndex + getItemFromArray(results, key1, key2, data) + + // Refunds + let key1 = Refunds->getSectionIndex + let key2 = SessionizerPaymentRefunds->getSectionIndex + getItemFromArray(results, key1, key2, data) + + // Disputes + let key1 = Disputes->getSectionIndex + let key2 = SessionizerPaymentDisputes->getSectionIndex + getItemFromArray(results, key1, key2, data) + results } let getDefaultResult = searchText => { - open GlobalSearchTypes { section: Default, results: [ @@ -272,14 +259,22 @@ let getDefaultResult = searchText => { } } +let getDefaultOption = searchText => { + { + texts: ["Show all results for"->JSON.Encode.string, searchText->JSON.Encode.string], + redirect_link: `/search?query=${searchText}`->JSON.Encode.string, + } +} + +let getAllOptions = (results: array) => { + results->Array.flatMap(item => item.results) +} + let parseResponse = response => { - open GlobalSearchTypes - open LogicUtils response ->getArrayFromJson([]) ->Array.map(json => { let item = json->getDictFromJsonObject - { count: item->getInt("count", 0), hits: item->getArrayFromDict("hits", []), @@ -289,7 +284,6 @@ let parseResponse = response => { } let generateSearchBody = (~searchText, ~merchant_id) => { - open LogicUtils if !(searchText->CommonAuthUtils.isValidEmail) { let filters = [ @@ -300,3 +294,227 @@ let generateSearchBody = (~searchText, ~merchant_id) => { [("query", searchText->JSON.Encode.string)]->getJsonFromArrayOfJson } } + +let categoryList = [ + Payment_Method, + Payment_Method_Type, + Currency, + Connector, + Customer_Email, + Card_Network, + Last_4, + Authentication_type, + Status, + Client_source, + Client_version, +] + +let getcategoryFromVariant = category => { + switch category { + | Payment_Method => "payment_method" + | Payment_Method_Type => "payment_method_type" + | Currency => "currency" + | Connector => "connector" + | Customer_Email => "customer_email" + | Card_Network => "card_network" + | Last_4 => "last_4" + | Date => "date" + | Authentication_type => "authentication_type" + | Status => "status" + | Client_source => "client_source" + | Client_version => "client_version" + } +} + +let getDefaultPlaceholderValue = category => { + switch category { + | Payment_Method => "payment_method:card" + | Payment_Method_Type => "payment_method_type:credit" + | Currency => "currency:USD" + | Connector => "connector:stripe" + | Customer_Email => "customer_email:abc@abc.com" + | Card_Network => "card_network:visa" + | Last_4 => "last_4:2326" + | Date => "date:today" + | Authentication_type => "authentication_type:no_three_ds" + | Status => "status:charged" + | Client_source => "client_source:Payment" + | Client_version => "client_version:0.55.0" + } +} + +let getCategoryVariantFromString = category => { + switch category { + | "payment_method" => Payment_Method + | "payment_method_type" => Payment_Method_Type + | "connector" => Connector + | "currency" => Currency + | "customer_email" => Customer_Email + | "card_network" => Card_Network + | "last_4" => Last_4 + | "authentication_type" => Authentication_type + | "status" => Status + | "client_source" => Client_source + | "client_version" => Client_version + | "date" | _ => Date + } +} + +let generatePlaceHolderValue = (category, options) => { + switch options->Array.get(0) { + | Some(value) => `${category->getcategoryFromVariant}:${value}` + | _ => category->getDefaultPlaceholderValue + } +} + +let getCategorySuggestions = json => { + let suggestions = Dict.make() + + json + ->getDictFromJsonObject + ->getArrayFromDict("queryData", []) + ->Array.forEach(item => { + let itemDict = item->getDictFromJsonObject + let key = itemDict->getString("dimension", "") + let value = + itemDict + ->getArrayFromDict("values", []) + ->Array.map(value => { + value->JSON.Decode.string->Option.getOr("") + }) + ->Array.filter(item => item->String.length > 0) + if key->isNonEmptyString && value->Array.length > 0 { + suggestions->Dict.set(key, value) + } + }) + + categoryList->Array.map(category => { + let options = suggestions->Dict.get(category->getcategoryFromVariant)->Option.getOr([]) + + { + categoryType: category, + options, + placeholder: generatePlaceHolderValue(category, options), + } + }) +} + +let paymentsGroupByNames = [ + "connector", + "payment_method", + "payment_method_type", + "currency", + "authentication_type", + "status", + "client_source", + "client_version", + "profile_id", + "card_network", + "merchant_id", +] + +let refundsGroupByNames = ["currency", "refund_status", "connector", "refund_type", "profile_id"] +let disputesGroupByNames = ["connector", "dispute_stage"] + +let getFilterBody = groupByNames => + { + let defaultDate = HSwitchRemoteFilter.getDateFilteredObject(~range=360) + let filterBodyEntity: AnalyticsUtils.filterBodyEntity = { + startTime: defaultDate.start_time, + endTime: defaultDate.end_time, + groupByNames, + source: "BATCH", + } + AnalyticsUtils.filterBody(filterBodyEntity) + }->Identity.genericTypeToJson + +let generateFilter = (queryArray: array) => { + let filter = Dict.make() + queryArray->Array.forEach(query => { + let keyValuePair = + query + ->String.split(":") + ->Array.filter(query => { + query->String.trim->isNonEmptyString + }) + + let key = keyValuePair->getValueFromArray(0, "") + let value = keyValuePair->getValueFromArray(1, "") + + switch filter->Dict.get(key) { + | Some(prevArr) => filter->Dict.set(key, prevArr->Array.concat([value])) + | _ => filter->Dict.set(key, [value]) + } + }) + + filter + ->Dict.toArray + ->Array.map(item => { + let (key, value) = item + let newValue = value->Array.map(JSON.Encode.string) + (key, newValue->JSON.Encode.array) + }) + ->Dict.fromArray +} + +let generateQuery = searchQuery => { + let filters = [] + let queryText = ref("") + + searchQuery + ->String.split(" ") + ->Array.filter(query => { + query->String.trim->isNonEmptyString + }) + ->Array.forEach(query => { + if RegExp.test(%re("/^[^:\s]+:[^:\s]+$/"), query) { + filters->Array.push(query) + } else { + queryText := query + } + }) + + let body = { + let query = if filters->Array.length > 0 { + [("filters", filters->generateFilter->JSON.Encode.object)] + } else { + [] + } + + let query = query->Array.concat([("query", queryText.contents->JSON.Encode.string)]) + + query->Dict.fromArray + } + + body +} + +let validateQuery = searchQuery => { + let freeTextCount = ref(0) + + searchQuery + ->String.split(" ") + ->Array.filter(query => { + query->String.trim->isNonEmptyString + }) + ->Array.forEach(query => { + if !RegExp.test(%re("/^[^:\s]+:[^:\s]+$/"), query) { + freeTextCount := freeTextCount.contents + 1 + } + }) + + freeTextCount.contents > 1 +} + +let getViewType = (~state, ~searchResults) => { + switch state { + | Loading => Load + | Loaded => + if searchResults->Array.length > 0 { + Results + } else { + EmptyResult + } + | Idle => FiltersSugsestions + } +} diff --git a/src/screens/Analytics/GlobalSearch/GlobalSearchTypes.res b/src/screens/Analytics/GlobalSearch/GlobalSearchTypes.res index 3657bce64..d82ad13a3 100644 --- a/src/screens/Analytics/GlobalSearch/GlobalSearchTypes.res +++ b/src/screens/Analytics/GlobalSearch/GlobalSearchTypes.res @@ -3,11 +3,11 @@ type section = | PaymentIntents | PaymentAttempts | Refunds + | Disputes | SessionizerPaymentAttempts | SessionizerPaymentIntents | SessionizerPaymentRefunds | SessionizerPaymentDisputes - | Disputes | Others | Default @@ -25,16 +25,12 @@ type resultType = { let getSectionHeader = section => { switch section { | Local => "Go To" - | PaymentIntents => "Payment Intents" - | PaymentAttempts => "Payment Attempts" - | Refunds => "Refunds" + | PaymentIntents | SessionizerPaymentIntents => "Payment Intents" + | PaymentAttempts | SessionizerPaymentAttempts => "Payment Attempts" + | Refunds | SessionizerPaymentRefunds => "Refunds" + | Disputes | SessionizerPaymentDisputes => "Disputes" | Others => "Others" - | Disputes => "Disputes" - | Default - | SessionizerPaymentAttempts - | SessionizerPaymentIntents - | SessionizerPaymentRefunds - | SessionizerPaymentDisputes => "" + | Default => "" } } @@ -52,6 +48,20 @@ let getSectionVariant = string => { } } +let getSectionIndex = string => { + switch string { + | PaymentAttempts => "payment_attempts" + | PaymentIntents => "payment_intents" + | Refunds => "refunds" + | Disputes => "disputes" + | SessionizerPaymentAttempts => "sessionizer_payment_attempts" + | SessionizerPaymentIntents => "sessionizer_payment_intents" + | SessionizerPaymentRefunds => "sessionizer_refunds" + | SessionizerPaymentDisputes => "sessionizer_disputes" + | _ => "" + } +} + type remoteResult = { count: int, hits: array, @@ -64,4 +74,30 @@ type defaultResult = { searchText: string, } -type state = Loading | Loaded | Failed | Idle +type state = Loading | Loaded | Idle + +type category = + | Payment_Method + | Payment_Method_Type + | Connector + | Customer_Email + | Card_Network + | Last_4 + | Date + | Currency + | Authentication_type + | Status + | Client_source + | Client_version + +type categoryOption = { + categoryType: category, + options: array, + placeholder: string, +} + +type viewType = + | Load + | Results + | FiltersSugsestions + | EmptyResult diff --git a/src/screens/Analytics/GlobalSearchResults/GlobalSearchTables/Disputes/DisputeTable.res b/src/screens/Analytics/GlobalSearchResults/GlobalSearchTables/Disputes/DisputeTable.res index 656e2e377..3512dc89d 100644 --- a/src/screens/Analytics/GlobalSearchResults/GlobalSearchTables/Disputes/DisputeTable.res +++ b/src/screens/Analytics/GlobalSearchResults/GlobalSearchTables/Disputes/DisputeTable.res @@ -59,6 +59,7 @@ let make = () => { let setPageDetails = Recoil.useSetRecoilState(LoadedTable.table_pageDetails) let (offset, setOffset) = React.useState(_ => pageDetail.offset) let searchText = UrlUtils.useGetFilterDictFromUrl("")->LogicUtils.getString("query", "") + let path = UrlUtils.useGetFilterDictFromUrl("")->LogicUtils.getString("domain", "") let clearPageDetails = () => { let newDict = pageDetailDict->Dict.toArray->Dict.fromArray @@ -70,12 +71,7 @@ let make = () => { setScreenState(_ => PageLoaderWrapper.Loading) try { - let (data, total) = await fetchTableData( - ~updateDetails, - ~offset, - ~query={searchText}, - ~path=domain, - ) + let (data, total) = await fetchTableData(~updateDetails, ~offset, ~query={searchText}, ~path) let arr = Array.make(~length=offset, Dict.make()) if total <= offset { diff --git a/src/screens/Analytics/GlobalSearchResults/GlobalSearchTables/PaymentAttempt/PaymentAttemptTable.res b/src/screens/Analytics/GlobalSearchResults/GlobalSearchTables/PaymentAttempt/PaymentAttemptTable.res index 317c04193..7d61f46ac 100644 --- a/src/screens/Analytics/GlobalSearchResults/GlobalSearchTables/PaymentAttempt/PaymentAttemptTable.res +++ b/src/screens/Analytics/GlobalSearchResults/GlobalSearchTables/PaymentAttempt/PaymentAttemptTable.res @@ -59,6 +59,7 @@ let make = () => { let pageDetail = pageDetailDict->Dict.get(domain)->Option.getOr(defaultValue) let (offset, setOffset) = React.useState(_ => pageDetail.offset) let searchText = UrlUtils.useGetFilterDictFromUrl("")->LogicUtils.getString("query", "") + let path = UrlUtils.useGetFilterDictFromUrl("")->LogicUtils.getString("domain", "") let clearPageDetails = () => { let newDict = pageDetailDict->Dict.toArray->Dict.fromArray @@ -70,12 +71,7 @@ let make = () => { setScreenState(_ => PageLoaderWrapper.Loading) try { - let (data, total) = await fetchTableData( - ~updateDetails, - ~offset, - ~query={searchText}, - ~path=domain, - ) + let (data, total) = await fetchTableData(~updateDetails, ~offset, ~query={searchText}, ~path) let arr = Array.make(~length=offset, Dict.make()) if total <= offset { diff --git a/src/screens/Analytics/GlobalSearchResults/GlobalSearchTables/PaymentIntent/PaymentIntentTable.res b/src/screens/Analytics/GlobalSearchResults/GlobalSearchTables/PaymentIntent/PaymentIntentTable.res index 48d3daf20..ce633c479 100644 --- a/src/screens/Analytics/GlobalSearchResults/GlobalSearchTables/PaymentIntent/PaymentIntentTable.res +++ b/src/screens/Analytics/GlobalSearchResults/GlobalSearchTables/PaymentIntent/PaymentIntentTable.res @@ -59,6 +59,7 @@ let make = () => { let pageDetail = pageDetailDict->Dict.get(domain)->Option.getOr(defaultValue) let (offset, setOffset) = React.useState(_ => pageDetail.offset) let searchText = UrlUtils.useGetFilterDictFromUrl("")->LogicUtils.getString("query", "") + let path = UrlUtils.useGetFilterDictFromUrl("")->LogicUtils.getString("domain", "") let clearPageDetails = () => { let newDict = pageDetailDict->Dict.toArray->Dict.fromArray @@ -70,12 +71,7 @@ let make = () => { setScreenState(_ => PageLoaderWrapper.Loading) try { - let (data, total) = await fetchTableData( - ~updateDetails, - ~offset, - ~query={searchText}, - ~path=domain, - ) + let (data, total) = await fetchTableData(~updateDetails, ~offset, ~query={searchText}, ~path) let arr = Array.make(~length=offset, Dict.make()) if total <= offset { diff --git a/src/screens/Analytics/GlobalSearchResults/GlobalSearchTables/Refunds/RefundsTable.res b/src/screens/Analytics/GlobalSearchResults/GlobalSearchTables/Refunds/RefundsTable.res index fdfd78a95..8aba6af61 100644 --- a/src/screens/Analytics/GlobalSearchResults/GlobalSearchTables/Refunds/RefundsTable.res +++ b/src/screens/Analytics/GlobalSearchResults/GlobalSearchTables/Refunds/RefundsTable.res @@ -59,6 +59,7 @@ let make = () => { let setPageDetails = Recoil.useSetRecoilState(LoadedTable.table_pageDetails) let (offset, setOffset) = React.useState(_ => pageDetail.offset) let searchText = UrlUtils.useGetFilterDictFromUrl("")->LogicUtils.getString("query", "") + let path = UrlUtils.useGetFilterDictFromUrl("")->LogicUtils.getString("domain", "") let clearPageDetails = () => { let newDict = pageDetailDict->Dict.toArray->Dict.fromArray @@ -70,12 +71,7 @@ let make = () => { setScreenState(_ => PageLoaderWrapper.Loading) try { - let (data, total) = await fetchTableData( - ~updateDetails, - ~offset, - ~query={searchText}, - ~path=domain, - ) + let (data, total) = await fetchTableData(~updateDetails, ~offset, ~query={searchText}, ~path) let arr = Array.make(~length=offset, Dict.make()) if total <= offset { diff --git a/src/screens/Analytics/GlobalSearchResults/GlobalSearchTables/ResultsTableUtils.res b/src/screens/Analytics/GlobalSearchResults/GlobalSearchTables/ResultsTableUtils.res index f0271bdaf..4db654b32 100644 --- a/src/screens/Analytics/GlobalSearchResults/GlobalSearchTables/ResultsTableUtils.res +++ b/src/screens/Analytics/GlobalSearchResults/GlobalSearchTables/ResultsTableUtils.res @@ -2,8 +2,6 @@ let tableBorderClass = "border-collapse border border-jp-gray-940 border-solid b let useGetData = () => { open LogicUtils - let body = Dict.make() - let {userInfo: {merchantId}} = React.useContext(UserInfoProvider.defaultContext) let getURL = APIUtils.useGetURL() async ( ~updateDetails: ( @@ -18,17 +16,9 @@ let useGetData = () => { ~query, ~path, ) => { + let body = query->GlobalSearchBarUtils.generateQuery body->Dict.set("offset", offset->Int.toFloat->JSON.Encode.float) body->Dict.set("count", 10->Int.toFloat->JSON.Encode.float) - body->Dict.set("query", query->JSON.Encode.string) - - if !(query->CommonAuthUtils.isValidEmail) { - let filters = [("customer_email", [query->JSON.Encode.string]->JSON.Encode.array)] - body->Dict.set("filters", filters->getJsonFromArrayOfJson) - body->Dict.set("query", merchantId->JSON.Encode.string) - } else { - body->Dict.set("query", query->JSON.Encode.string) - } try { let url = getURL(~entityName=GLOBAL_SEARCH, ~methodType=Post, ~id=Some(path)) diff --git a/src/screens/Analytics/GlobalSearchResults/SearchResultsPage.res b/src/screens/Analytics/GlobalSearchResults/SearchResultsPage.res index 49007092a..7b57545e0 100644 --- a/src/screens/Analytics/GlobalSearchResults/SearchResultsPage.res +++ b/src/screens/Analytics/GlobalSearchResults/SearchResultsPage.res @@ -40,16 +40,13 @@ module RenderSearchResultBody = { }) ->React.array - | PaymentIntents => - | PaymentAttempts => - | Refunds => - | Disputes => + | PaymentIntents | SessionizerPaymentIntents => + + | PaymentAttempts | SessionizerPaymentAttempts => + + | Refunds | SessionizerPaymentRefunds => + | Disputes | SessionizerPaymentDisputes => | Others | Default => "Not implemented"->React.string - | SessionizerPaymentAttempts - | SessionizerPaymentIntents - | SessionizerPaymentRefunds - | SessionizerPaymentDisputes => - ""->React.string } } } @@ -66,7 +63,7 @@ module SearchResultsComponent = {
{section.section->getSectionHeader->React.string}
- @@ -97,14 +94,15 @@ let make = () => { let query = UrlUtils.useGetFilterDictFromUrl("")->getString("query", "") let {globalSearch} = HyperswitchAtom.featureFlagAtom->Recoil.useRecoilValueFromAtom let {userHasAccess} = GroupACLHooks.useUserGroupACLHook() - let {userInfo: {merchantId}} = React.useContext(UserInfoProvider.defaultContext) let isShowRemoteResults = globalSearch && userHasAccess(~groupAccess=OperationsView) === Access + let fallBackQuery = UrlUtils.useGetFilterDictFromUrl("")->LogicUtils.getString("query", "") let getSearchResults = async results => { try { let url = getURL(~entityName=GLOBAL_SEARCH, ~methodType=Post) - let body = generateSearchBody(~searchText={query}, ~merchant_id={merchantId}) - let response = await fetchDetails(url, body, Post) + let query = searchText->isNonEmptyString ? searchText : fallBackQuery + let body = query->generateQuery + let response = await fetchDetails(url, body->JSON.Encode.object, Post) let local_results = [] results->Array.forEach((item: resultType) => { @@ -129,7 +127,7 @@ let make = () => { setState(_ => Loaded) } catch { - | _ => setState(_ => Failed) + | _ => setState(_ => Loaded) } } @@ -165,7 +163,7 @@ let make = () => { } None - }, (query, url.search)) + }, [query, url.search])
@@ -176,7 +174,7 @@ let make = () => {
| _ => if searchResults->Array.length === 0 { - + } else { } diff --git a/src/screens/Analytics/GlobalSearchResults/SearchResultsPageUtils.res b/src/screens/Analytics/GlobalSearchResults/SearchResultsPageUtils.res index dee7b46be..3c4bcbf65 100644 --- a/src/screens/Analytics/GlobalSearchResults/SearchResultsPageUtils.res +++ b/src/screens/Analytics/GlobalSearchResults/SearchResultsPageUtils.res @@ -10,6 +10,8 @@ let getSearchresults = (result: GlobalSearchTypes.defaultResult) => { }) } + let data = Dict.make() + result.remote_results->Array.forEach(value => { let remoteResults = value.hits->Array.map(item => { { @@ -19,13 +21,37 @@ let getSearchresults = (result: GlobalSearchTypes.defaultResult) => { }) if remoteResults->Array.length > 0 { - results->Array.push({ - section: value.index->getSectionVariant, - results: remoteResults, - total_results: value.count, - }) + data->Dict.set( + value.index, + { + section: value.index->getSectionVariant, + results: remoteResults, + total_results: value.count, + }, + ) } }) + open GlobalSearchBarUtils + // intents + let key1 = PaymentIntents->getSectionIndex + let key2 = SessionizerPaymentIntents->getSectionIndex + getItemFromArray(results, key1, key2, data) + + // Attempts + let key1 = PaymentAttempts->getSectionIndex + let key2 = SessionizerPaymentAttempts->getSectionIndex + getItemFromArray(results, key1, key2, data) + + // Refunds + let key1 = Refunds->getSectionIndex + let key2 = SessionizerPaymentRefunds->getSectionIndex + getItemFromArray(results, key1, key2, data) + + // Disputes + let key1 = Disputes->getSectionIndex + let key2 = SessionizerPaymentDisputes->getSectionIndex + getItemFromArray(results, key1, key2, data) + (results, result.searchText) }