diff --git a/packages/diracx-web-components/src/components/JobMonitor/JobSearchBar.tsx b/packages/diracx-web-components/src/components/JobMonitor/JobSearchBar.tsx index ac8810f7..87d805b6 100644 --- a/packages/diracx-web-components/src/components/JobMonitor/JobSearchBar.tsx +++ b/packages/diracx-web-components/src/components/JobMonitor/JobSearchBar.tsx @@ -66,20 +66,16 @@ export function JobSearchBar({ - createSuggestions( + createSuggestions={({ previousToken, previousEquation, equationIndex }) => + createSuggestions({ diracxUrl, accessToken, previousToken, previousEquation, columns, searchBody, - equationIndex, - ) + searchBodyIndex: equationIndex, + }) } allowKeyWordSearch={false} // Disable keyword search for job monitor plotTypeSelectorProps={plotTypeSelectorProps} @@ -100,20 +96,28 @@ export function JobSearchBar({ * @param searchBodyIndex The index of the search body, which is used to determine the current search context (optional). * @returns A list of suggestions based on the current tokens and data. */ -async function createSuggestions( - diracxUrl: string | null, - accessToken: string | undefined, - previousToken: SearchBarToken | undefined, - previousEquation: SearchBarTokenEquation | undefined, +async function createSuggestions({ + diracxUrl, + accessToken, + previousToken, + previousEquation, + columns, + searchBody, + searchBodyIndex, +}: { + diracxUrl: string | null; + accessToken: string | undefined; + previousToken: SearchBarToken | undefined; + previousEquation: SearchBarTokenEquation | undefined; // eslint-disable-next-line @typescript-eslint/no-explicit-any - columns: ColumnDef[], - searchBody: SearchBody, - searchBodyIndex?: number, -): Promise { + columns: ColumnDef[]; + searchBody?: SearchBody; + searchBodyIndex?: number; +}): Promise { let data: JobSummary[] = []; const search = [...(searchBody?.search || [])]; - + /** The search body index is used to determine the current search context */ const newSearchBody = { ...searchBody, search: search.slice(0, searchBodyIndex), @@ -146,15 +150,11 @@ async function createSuggestions( const type = columns.map( (column) => column.meta?.type || CategoryType.STRING, ) as CategoryType[]; - const hideSuggestion = columns.map( - (column) => column.meta?.isQuasiUnique || false, - ); return { items: items, type, nature: Array(items.length).fill(SearchBarTokenNature.CATEGORY), - hideSuggestion: hideSuggestion, }; } @@ -165,26 +165,35 @@ async function createSuggestions( items: ["minute", "hour", "day", "week", "month", "year"], nature: Array(6).fill(SearchBarTokenNature.VALUE), type: Array(6).fill(CategoryType.DATE), - hideSuggestion: Array(6).fill(previousToken.hideSuggestion), }; } - if (!previousToken.hideSuggestion) { + const hideSuggestion = columns.some( + (column) => + column.header === previousEquation.items[0].label && + column.meta?.isQuasiUnique === true, + ); + + if (!hideSuggestion) { // Load the suggestions for the selected category + + /** + * The internal name of the category is used to fetch the job summary + */ const category = fromHumanReadableText( String(previousEquation.items[0].label), columns, ); + await fetchJobSummary(category); - const items = data.map( - (item) => item[category as keyof JobSummary] as string, + const items = data.map((item) => + String(item[category as keyof JobSummary]), ); return { items: items, nature: Array(items.length).fill(SearchBarTokenNature.VALUE), - type: Array(items.length).fill(CategoryType.STRING), - hideSuggestion: Array(items.length).fill(previousToken.hideSuggestion), + type: Array(items.length).fill(previousToken.type), }; } } @@ -197,9 +206,6 @@ async function createSuggestions( items: items, type: Array(items.length).fill(CategoryType.STRING), nature: Array(items.length).fill(SearchBarTokenNature.OPERATOR), - hideSuggestion: Array(items.length).fill( - previousToken.hideSuggestion, - ), }; case CategoryType.NUMBER: items = Operators.getNumberOperators().map((op) => op.getDisplay()); @@ -207,9 +213,6 @@ async function createSuggestions( items: items, type: Array(items.length).fill(CategoryType.NUMBER), nature: Array(items.length).fill(SearchBarTokenNature.OPERATOR), - hideSuggestion: Array(items.length).fill( - previousToken.hideSuggestion, - ), }; case CategoryType.BOOLEAN: items = Operators.getBooleanOperators().map((op) => op.getDisplay()); @@ -217,9 +220,6 @@ async function createSuggestions( items: items, type: Array(items.length).fill(CategoryType.BOOLEAN), nature: Array(items.length).fill(SearchBarTokenNature.OPERATOR), - hideSuggestion: Array(items.length).fill( - previousToken.hideSuggestion, - ), }; case CategoryType.DATE: items = Operators.getDateOperators().map((op) => op.getDisplay()); @@ -227,9 +227,6 @@ async function createSuggestions( items: items, type: Array(items.length).fill(CategoryType.DATE), nature: Array(items.length).fill(SearchBarTokenNature.OPERATOR), - hideSuggestion: Array(items.length).fill( - previousToken.hideSuggestion, - ), }; case CategoryType.CUSTOM: items = Operators.getDefaultOperators().map((op) => op.getDisplay()); @@ -237,18 +234,13 @@ async function createSuggestions( items: items, nature: Array(items.length).fill(SearchBarTokenNature.OPERATOR), type: Array(items.length).fill(CategoryType.CUSTOM), - hideSuggestion: Array(items.length).fill( - previousToken.hideSuggestion, - ), }; - // We don't want suggestions for the number and in case of a custom token default: return { items: [], nature: [], type: [], - hideSuggestion: [], }; } } @@ -257,6 +249,5 @@ async function createSuggestions( items: [], nature: [], type: [], - hideSuggestion: [], }; } diff --git a/packages/diracx-web-components/src/components/shared/DataTable.tsx b/packages/diracx-web-components/src/components/shared/DataTable.tsx index b120e548..0088384d 100644 --- a/packages/diracx-web-components/src/components/shared/DataTable.tsx +++ b/packages/diracx-web-components/src/components/shared/DataTable.tsx @@ -354,7 +354,7 @@ export function DataTable>({ }, ), }), - [handleContextMenu], + [handleContextMenu, disableCheckbox], ); function getLeftOffsetForColumn(column: Column): number { diff --git a/packages/diracx-web-components/src/components/shared/SearchBar/DisplayTokenEquation.tsx b/packages/diracx-web-components/src/components/shared/SearchBar/DisplayTokenEquation.tsx index db96a27c..108d44ba 100644 --- a/packages/diracx-web-components/src/components/shared/SearchBar/DisplayTokenEquation.tsx +++ b/packages/diracx-web-components/src/components/shared/SearchBar/DisplayTokenEquation.tsx @@ -75,6 +75,7 @@ export function DisplayTokenEquation({ }} key={tokenIndex} onClick={(e) => handleClick(e, tokenIndex)} + id={`tokenid:equation-${equationIndex}-token-${tokenIndex}`} onContextMenu={(e) => { e.preventDefault(); handleRightClick(); diff --git a/packages/diracx-web-components/src/components/shared/SearchBar/SearchBar.tsx b/packages/diracx-web-components/src/components/shared/SearchBar/SearchBar.tsx index 31102730..6b6a8c8b 100644 --- a/packages/diracx-web-components/src/components/shared/SearchBar/SearchBar.tsx +++ b/packages/diracx-web-components/src/components/shared/SearchBar/SearchBar.tsx @@ -1,4 +1,4 @@ -import React, { useState, useRef, useEffect } from "react"; +import React, { useState, useRef, useEffect, useMemo } from "react"; import { Box, Menu, MenuItem, IconButton } from "@mui/material"; @@ -32,17 +32,25 @@ import { import SearchField from "./SearchField"; import { PlotTypeSelector } from "./PlotTypeSelector"; +export interface CreateSuggestionsParams { + previousToken?: SearchBarToken; + previousEquation?: SearchBarTokenEquation; + currentInput?: string; + equationIndex?: number; +} + export interface SearchBarProps { /** The filters to be applied to the search */ filters: Filter[]; /** The function to set the filters */ setFilters: React.Dispatch>; /** The data to be used for suggestions */ - createSuggestions: ( - previousToken: SearchBarToken | undefined, - previousEquation: SearchBarTokenEquation | undefined, - equationIndex?: number, - ) => Promise; + createSuggestions: ({ + previousToken, + previousEquation, + currentInput, + equationIndex, + }: CreateSuggestionsParams) => Promise; /** The function to call when the search is performed (optional) */ searchFunction?: ( equations: SearchBarTokenEquation[], @@ -81,19 +89,23 @@ export function SearchBar({ plotTypeSelectorProps, }: SearchBarProps) { const [inputValue, setInputValue] = useState(""); - const [anchorEl, setAnchorEl] = useState(null); + const [anchorEl, setAnchorEl] = useState(null); const [clickedTokenIndex, setClickedTokenIndex] = useState(null); const [focusedTokenIndex, setFocusedTokenIndex] = useState(null); - const inputRef = useRef(null); + const inputRef = useRef(null); const searchTimerRef = useRef(null); const lastSearchedEquationsRef = useRef("[]"); const lastClickedTokenIndexRef = useRef(null); const [tokenEquations, setTokenEquations] = useState< SearchBarTokenEquation[] >([]); + + const [isSuggestionsLoading, setIsSuggestionsLoading] = + useState(false); + /** A ref to store the current filters to avoid reloading the token equations */ const currentFilters = useRef(null); /** A ref to store a boolean indicating if the component is updating from search */ @@ -103,7 +115,6 @@ export function SearchBar({ items: [], nature: [], type: [], - hideSuggestion: [], }); const { previousEquation, previousToken } = getPreviousEquationAndToken( @@ -147,18 +158,65 @@ export function SearchBar({ } }, [filters, createSuggestions, currentFilters, tokenEquations.length]); - // Create a list of options based on the current tokens and data + /** + * This effect is used to check if the provided function uses the current input + */ + const usesCurrentInput = useMemo( + () => functionUsesCurrentInput(createSuggestions), + [createSuggestions], + ); + + // Load suggestions (with proper loading tracking and cancellation) useEffect(() => { - async function load() { - const result = await createSuggestions( - previousToken, - previousEquation, - focusedTokenIndex?.equationIndex, - ); - setSuggestions(result); - } - load(); - }, [previousEquation, previousToken, createSuggestions, focusedTokenIndex]); + let cancelled = false; + + const emptySuggestions: SearchBarSuggestions = { + items: [], + nature: [], + type: [], + }; + + const run = async () => { + setIsSuggestionsLoading(true); + setSuggestions(emptySuggestions); + + try { + const params: CreateSuggestionsParams = { + previousToken, + previousEquation, + equationIndex: + focusedTokenIndex?.equationIndex ?? tokenEquations.length - 1, + }; + if (usesCurrentInput && inputValue) { + params.currentInput = inputValue; + } + + const result = await createSuggestions(params); + if (!cancelled) { + setSuggestions(result); + } + } finally { + if (!cancelled) { + setIsSuggestionsLoading(false); + } + } + }; + + run(); + return () => { + cancelled = true; + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + previousEquation, + previousToken, + createSuggestions, + focusedTokenIndex, + tokenEquations.length, + // If the current input is not used, we don't want to trigger the suggestions for each letter typed + // eslint-disable-next-line react-hooks/exhaustive-deps + ...(usesCurrentInput ? [inputValue] : []), + ]); // Timer to delay the search function // This effect will trigger the searchFonction after a delay if the equations are valid @@ -182,6 +240,7 @@ export function SearchBar({ currentEquationsString !== lastSearchedEquationsRef.current; if (allEquationsValid && hasChanged) { + isUpdatingFromSearch.current = true; searchTimerRef.current = setTimeout(() => { lastSearchedEquationsRef.current = currentEquationsString; searchFunction(tokenEquations, setFilters); @@ -200,19 +259,24 @@ export function SearchBar({ inputRef.current?.focus(); }, [focusedTokenIndex]); - const handleOptionMenuOpen = ( - event: React.MouseEvent, - equationIndex: number, - tokenIndex: number, - ) => { - if ( - (tokenEquations[equationIndex].items[tokenIndex].suggestions?.items || []) - .length > 0 - ) { - setAnchorEl(event.currentTarget); - setClickedTokenIndex({ equationIndex, tokenIndex }); + // Effect to open the suggestions menu when a token is clicked + useEffect(() => { + if (clickedTokenIndex !== null) { + const { equationIndex, tokenIndex } = clickedTokenIndex; + const suggestions = + tokenEquations[equationIndex].items[tokenIndex].suggestions?.items || + []; + + if (suggestions.length > 0) { + // If there are suggestions, open the menu + setAnchorEl( + document.querySelector( + `#tokenid\\:equation-${equationIndex}-token-${tokenIndex}`, + ), + ); + } } - }; + }, [tokenEquations, clickedTokenIndex]); const handleOptionMenuClose = () => { setAnchorEl(null); @@ -223,7 +287,6 @@ export function SearchBar({ option: string, nature: SearchBarTokenNature, type: CategoryType, - hideSuggestion: boolean, ) => { if (searchTimerRef.current) { clearTimeout(searchTimerRef.current); @@ -238,7 +301,6 @@ export function SearchBar({ type: type, // Change the type nature: nature, // Change the nature label: option, - hideSuggestion, }; updatedTokens[clickedTokenIndex.equationIndex] = updatedToken; // Update the equation in the list @@ -257,6 +319,7 @@ export function SearchBar({ setTokenEquations={setTokenEquations} tokenEquations={tokenEquations} suggestions={suggestions} + suggestionsLoading={isSuggestionsLoading} focusedTokenIndex={focusedTokenIndex} setFocusedTokenIndex={setFocusedTokenIndex} allowKeyWordSearch={allowKeyWordSearch} @@ -277,11 +340,11 @@ export function SearchBar({ ); tokenEquations[clickedTokenIndex.equationIndex].items[ clickedTokenIndex.tokenIndex - ].suggestions = await createSuggestions( + ].suggestions = await createSuggestions({ previousToken, previousEquation, - clickedTokenIndex.equationIndex, - ); + equationIndex: clickedTokenIndex.equationIndex, + }); setTokenEquations([...tokenEquations]); // Update the state to trigger a re-render } @@ -300,9 +363,8 @@ export function SearchBar({ items: [], nature: [], type: [], - hideSuggestion: [], } - : { items: [], nature: [], type: [], hideSuggestion: [] }; + : { items: [], nature: [], type: [] }; return ( ({ }} data-testid="search-bar" > - + {tokenEquations.map((equation, index) => ( - handleOptionMenuOpen(e, index, tokenIndex) + handleClick={(_e, tokenIndex) => + setClickedTokenIndex({ equationIndex: index, tokenIndex }) } handleRightClick={() => setTokenEquations((prev) => [ @@ -366,7 +436,6 @@ export function SearchBar({ option, currentSuggestions.nature[idx], currentSuggestions.type[idx], - currentSuggestions.hideSuggestion[idx], ) } > @@ -410,3 +479,18 @@ export function SearchBar({ ); } + +/** + * This function is used to check if the provided function uses the current input + * + * @param func The function to check if it uses the current input + * @returns A boolean indicating whether the function uses the current input + */ +function functionUsesCurrentInput( + func: (params: CreateSuggestionsParams) => Promise, +): boolean { + const funcString = func.toString(); + return ( + funcString.includes("currentInput") || funcString.includes("inputValue") + ); +} diff --git a/packages/diracx-web-components/src/components/shared/SearchBar/SearchField.tsx b/packages/diracx-web-components/src/components/shared/SearchBar/SearchField.tsx index 65266eff..110be77f 100644 --- a/packages/diracx-web-components/src/components/shared/SearchBar/SearchField.tsx +++ b/packages/diracx-web-components/src/components/shared/SearchBar/SearchField.tsx @@ -24,18 +24,34 @@ import "dayjs/locale/en-gb"; // Import the locale for dayjs import { MyDateTimePicker } from "./DatePicker"; interface SearchFieldProps { + /** The current input value in the search field */ inputValue: string; + /** Function to update the input value */ setInputValue: React.Dispatch>; + + /** Reference to the input element */ inputRef: React.RefObject; + + /** The current token equations */ + tokenEquations: SearchBarTokenEquation[]; + /** Function to update the token equations */ setTokenEquations: React.Dispatch< React.SetStateAction >; - tokenEquations: SearchBarTokenEquation[]; + + /** The current suggestions for the search field */ suggestions: SearchBarSuggestions; + /** Boolean indicating if suggestions are loading */ + suggestionsLoading: boolean; + + /** The current focused token index */ focusedTokenIndex: EquationAndTokenIndex | null; + /** Function to update the focused token index */ setFocusedTokenIndex: React.Dispatch< React.SetStateAction >; + + /** Boolean indicating if keyword search is allowed */ allowKeyWordSearch?: boolean; } @@ -46,6 +62,7 @@ export default function SearchField({ setTokenEquations, tokenEquations, suggestions, + suggestionsLoading, focusedTokenIndex, setFocusedTokenIndex, allowKeyWordSearch = true, @@ -79,7 +96,6 @@ export default function SearchField({ label: string, nature: SearchBarTokenNature, type: CategoryType, - hideSuggestion: boolean, ) { if (!allowKeyWordSearch && nature === SearchBarTokenNature.CUSTOM) return; @@ -100,7 +116,6 @@ export default function SearchField({ nature: nature, suggestions: nature === SearchBarTokenNature.CATEGORY ? undefined : suggestions, - hideSuggestion: hideSuggestion, }; handleEquationsVerification(updatedTokens, setTokenEquations); } @@ -116,14 +131,13 @@ export default function SearchField({ type: type, nature: nature, suggestions: suggestions, - hideSuggestion: hideSuggestion, }); handleEquationsVerification([...tokenEquations], setTokenEquations); } else { // We are creating a new equation const newLastEquation: SearchBarTokenEquation = { status: - type === CategoryType.CUSTOM + nature === SearchBarTokenNature.CUSTOM ? EquationStatus.VALID : EquationStatus.WAITING, items: [ @@ -132,7 +146,6 @@ export default function SearchField({ type: type, nature: nature, suggestions: undefined, - hideSuggestion: hideSuggestion, }, ], }; @@ -249,16 +262,12 @@ export default function SearchField({ }, 0); } - // Calculate the width of the input field based on the input value length - const width = Math.min(Math.max(inputValue.length * 8 + 50, 150), 800); - const handleDateAccepted = (newValue: string | null) => { if (newValue) { handleTokenCreation( newValue, SearchBarTokenNature.VALUE, CategoryType.DATE, - false, ); } }; @@ -289,8 +298,7 @@ export default function SearchField({ sx={{ marginTop: "2px", minWidth: "180px", - width: "auto", - maxWidth: 0.9, + flexGrow: 1, }} disableClearable={true} options={suggestions.items} @@ -298,6 +306,7 @@ export default function SearchField({ onHighlightChange={(_e, option) => { optionSelectedRef.current = option !== null; }} + loading={suggestionsLoading} renderInput={(params) => ( ) => { - if (e.key === "Enter" && inputValue.trim()) { + if (e.key === "Enter" && inputValue && inputValue.trim()) { if (optionSelectedRef.current) { optionSelectedRef.current = false; return; } - const { nature, type, hideSuggestion } = getTokenMetadata( + const { nature, type } = getTokenMetadata( inputValue.trim(), suggestions, previousToken, ); // Always create token on Enter press, regardless of operator type - handleTokenCreation( - inputValue.trim(), - nature, - type, - hideSuggestion, - ); + handleTokenCreation(inputValue.trim(), nature, type); } if (e.key === "Backspace") { handleBackspaceKeyDown(); @@ -358,7 +359,7 @@ export default function SearchField({ /> )} onChange={(_e, value: string | null) => { - if (value !== null && value !== "") { + if (value && value !== "") { optionSelectedRef.current = true; // Check if previous token is "in" or "not in" operator @@ -369,24 +370,24 @@ export default function SearchField({ ) { // For "in" and "not in" operators, accumulate values with " | " separator // Don't create token immediately - wait for Enter press - if (inputValue.trim() === "") { + if (inputValue && inputValue.trim() === "") { setInputValue(value); } else { // Additional value selection - append with separator setInputValue((prev) => { - return inputRef.current?.value + " | " + prev.trim(); + return inputRef.current?.value + " | " + prev; }); } return; } // For all other operators, create token immediately - const { nature, type, hideSuggestion } = getTokenMetadata( + const { nature, type } = getTokenMetadata( value.trim(), suggestions, previousToken, ); - handleTokenCreation(value, nature, type, hideSuggestion); + handleTokenCreation(value, nature, type); } }} /> diff --git a/packages/diracx-web-components/src/components/shared/SearchBar/Utils.tsx b/packages/diracx-web-components/src/components/shared/SearchBar/Utils.tsx index c5596f6b..cae381be 100644 --- a/packages/diracx-web-components/src/components/shared/SearchBar/Utils.tsx +++ b/packages/diracx-web-components/src/components/shared/SearchBar/Utils.tsx @@ -10,6 +10,8 @@ import { Operators, } from "../../../types"; +import { CreateSuggestionsParams } from "./SearchBar"; + /** * @param tokenEquations The list of token equations to be verified. * @param setTokenEquations A function to update the state of token equations. @@ -90,9 +92,11 @@ function handleEquationVerification( case CategoryType.NUMBER: if ( - freeTextOperators.includes(tokenEquation.items[1].label as string) || - (tokenEquation.items[1].type === CategoryType.NUMBER && - !Number.isNaN(Number(tokenEquation.items[2].label))) + tokenEquation.items[1].type === CategoryType.NUMBER && + tokenEquation.items[2].type === CategoryType.NUMBER && + (typeof tokenEquation.items[2].label === "string" + ? !isNaN(Number(tokenEquation.items[2].label)) + : tokenEquation.items[2].label.every((item) => !isNaN(Number(item)))) ) tokenEquation.status = EquationStatus.VALID; else tokenEquation.status = EquationStatus.INVALID; @@ -209,7 +213,7 @@ export function getPreviousEquationAndToken( * @param value The value of the token to be checked. * @param suggestions The suggestions object containing items and their types. * @param lastToken The last token in the equation, which can be undefined - * @returns The type of the token, which can be "custom", "value", "operator", "custom_value", or a category type. + * @returns The type of the token and its nature. */ export function getTokenMetadata( value: string, @@ -218,28 +222,54 @@ export function getTokenMetadata( ): { nature: SearchBarTokenNature; type: CategoryType; - hideSuggestion: boolean; } { + // The value can be for a is in/is not in + const values = value.split(/,|\|/).map((v) => v.trim()); + + // If the value is in the suggestions list, return its metadata + // If there is no suggestions for this category, then we allow every input + // If the last token is an free text operator + const index = suggestions.items.indexOf(value); - if (index >= 0) { + const isFreeTextOperator = Operators.getFreeTextOperators() + .map((op) => op.getDisplay()) + .includes(String(lastToken?.label)); + if (index >= 0 || suggestions.items.length === 0 || isFreeTextOperator) { + return { + nature: suggestions.nature[index] ?? SearchBarTokenNature.VALUE, + type: suggestions.type[index] ?? lastToken?.type ?? CategoryType.CUSTOM, + }; + } + + // Special case for the "is in" and "is not in" operators. We need to split the value to check each part. + if ( + lastToken && + lastToken.nature === SearchBarTokenNature.OPERATOR && + (lastToken.label === Operators.IN.getDisplay() || + lastToken.label === Operators.NOT_IN.getDisplay()) + ) { + if (values.every((value) => suggestions.items.includes(value))) + return { + nature: SearchBarTokenNature.VALUE, + type: lastToken?.type || CategoryType.CUSTOM, + }; + return { - nature: suggestions.nature[index], - type: suggestions.type[index], - hideSuggestion: suggestions.hideSuggestion[index], + nature: SearchBarTokenNature.CUSTOM, + type: lastToken?.type || CategoryType.CUSTOM, }; } + if (lastToken && lastToken.nature === SearchBarTokenNature.OPERATOR) { - // If the last token is an operator, we assume the current token is a value + // If the last token is an operator, the user wrote a custom value return { - nature: SearchBarTokenNature.VALUE, - type: CategoryType.CUSTOM, - hideSuggestion: lastToken.hideSuggestion, + nature: SearchBarTokenNature.CUSTOM, + type: lastToken?.type || CategoryType.CUSTOM, }; } return { nature: SearchBarTokenNature.CUSTOM, - type: CategoryType.CUSTOM, - hideSuggestion: true, + type: lastToken?.type || CategoryType.CUSTOM, }; } @@ -268,11 +298,11 @@ export function convertListToString(labelList: string[] | string): string { export async function convertFilterToTokenEquation( filter: Filter, filterIndex: number, - createSuggestions: ( - previousToken: SearchBarToken | undefined, - previousEquation: SearchBarTokenEquation | undefined, - filterIndex?: number, - ) => Promise, + createSuggestions: ({ + previousToken, + previousEquation, + equationIndex, + }: CreateSuggestionsParams) => Promise, ): Promise { const newEquation: SearchBarTokenEquation = { items: [ @@ -280,30 +310,25 @@ export async function convertFilterToTokenEquation( label: filter.parameter, nature: SearchBarTokenNature.CATEGORY, type: CategoryType.UNKNOWN, - hideSuggestion: true, }, { label: Operators.getDisplayFromInternal(filter.operator), nature: SearchBarTokenNature.OPERATOR, type: CategoryType.UNKNOWN, - hideSuggestion: true, }, { label: filter.value || filter.values || "", nature: SearchBarTokenNature.VALUE, type: CategoryType.UNKNOWN, - hideSuggestion: true, }, ], status: EquationStatus.VALID, }; // For the category - const suggestions_categories = await createSuggestions( - undefined, - undefined, - filterIndex, - ); + const suggestions_categories = await createSuggestions({ + equationIndex: filterIndex, + }); newEquation.items[0].type = suggestions_categories.type[ @@ -311,11 +336,11 @@ export async function convertFilterToTokenEquation( ] || SearchBarTokenNature.CATEGORY; // For the operator - const suggestions_operators = await createSuggestions( - newEquation.items[0], - newEquation, - filterIndex, - ); + const suggestions_operators = await createSuggestions({ + previousToken: newEquation.items[0], + previousEquation: newEquation, + equationIndex: filterIndex, + }); newEquation.items[1].type = suggestions_operators.type[ @@ -324,12 +349,12 @@ export async function convertFilterToTokenEquation( ) ] || SearchBarTokenNature.OPERATOR; - // For the value(s) - const suggestions_values = await createSuggestions( - newEquation.items[1], - newEquation, - filterIndex, - ); + // For the value + const suggestions_values = await createSuggestions({ + previousToken: newEquation.items[1], + previousEquation: newEquation, + equationIndex: filterIndex, + }); newEquation.items[1].suggestions = suggestions_operators; newEquation.items[2].suggestions = suggestions_values; diff --git a/packages/diracx-web-components/src/types/SearchBarSuggestions.ts b/packages/diracx-web-components/src/types/SearchBarSuggestions.ts index c04e2f45..d4a8c54f 100644 --- a/packages/diracx-web-components/src/types/SearchBarSuggestions.ts +++ b/packages/diracx-web-components/src/types/SearchBarSuggestions.ts @@ -8,6 +8,4 @@ export type SearchBarSuggestions = { nature: SearchBarTokenNature[]; /** The type of each suggestion (string, number, bool)*/ type: CategoryType[]; - /** Booleans indicating if the suggestions should be hidden */ - hideSuggestion: boolean[]; }; diff --git a/packages/diracx-web-components/src/types/SearchBarToken.ts b/packages/diracx-web-components/src/types/SearchBarToken.ts index edfa7c6b..f37361fe 100644 --- a/packages/diracx-web-components/src/types/SearchBarToken.ts +++ b/packages/diracx-web-components/src/types/SearchBarToken.ts @@ -9,8 +9,6 @@ export type SearchBarToken = { type: CategoryType; /** The nature of the token, e.g., "category", "operator", "value" */ nature: SearchBarTokenNature; - /** Boolean indicating if we should hide the suggestions */ - hideSuggestion: boolean; /** Optional suggestions for the token, useful for auto-completion or hints */ suggestions?: SearchBarSuggestions; }; diff --git a/packages/diracx-web-components/src/types/operators.ts b/packages/diracx-web-components/src/types/operators.ts index 6ca4660b..93087784 100644 --- a/packages/diracx-web-components/src/types/operators.ts +++ b/packages/diracx-web-components/src/types/operators.ts @@ -7,6 +7,8 @@ export class Operators { static readonly NOT_IN = new Operators("is not in", "not in"); static readonly LIKE = new Operators("like", "like"); static readonly LAST = new Operators("in the last", "last"); + static readonly NOT_LIKE = new Operators("not like", "not like"); + static readonly REGEX = new Operators("match", "regex"); private constructor( private readonly display: string, @@ -51,6 +53,8 @@ export class Operators { Operators.NOT_IN, Operators.LIKE, Operators.LAST, + Operators.NOT_LIKE, + Operators.REGEX, ]; } @@ -69,6 +73,8 @@ export class Operators { Operators.IN, Operators.NOT_IN, Operators.LIKE, + Operators.NOT_LIKE, + Operators.REGEX, ]; } @@ -81,6 +87,7 @@ export class Operators { Operators.IN, Operators.NOT_IN, Operators.LIKE, + Operators.NOT_LIKE, ]; } @@ -99,17 +106,21 @@ export class Operators { Operators.GREATER_THAN, Operators.LESS_THAN, Operators.LIKE, + Operators.NOT_LIKE, + Operators.REGEX, ]; } static getFreeTextOperators(): Operators[] { return [ Operators.LIKE, + Operators.NOT_LIKE, Operators.IN, Operators.NOT_IN, Operators.LAST, Operators.GREATER_THAN, Operators.LESS_THAN, + Operators.REGEX, ]; } } diff --git a/packages/diracx-web-components/stories/SearchBar.stories.tsx b/packages/diracx-web-components/stories/SearchBar.stories.tsx index cb54dfcc..3f9f282a 100644 --- a/packages/diracx-web-components/stories/SearchBar.stories.tsx +++ b/packages/diracx-web-components/stories/SearchBar.stories.tsx @@ -39,10 +39,13 @@ function customClearFunction( ); } -const createSuggestions = async ( - previousToken: SearchBarToken | undefined, - previousEquation: SearchBarTokenEquation | undefined, -): Promise => { +const createSuggestions = async ({ + previousToken, + previousEquation, +}: { + previousToken?: SearchBarToken; + previousEquation?: SearchBarTokenEquation; +}): Promise => { // Simulate fetching suggestions based on the previous token and equation if ( !previousToken || @@ -62,7 +65,6 @@ const createSuggestions = async ( ], type: Array(7).fill("string"), nature: Array(7).fill("category"), - hideSuggestion: Array(7).fill(false), }; if (previousToken.nature === "category") @@ -70,7 +72,6 @@ const createSuggestions = async ( items: ["=", "!=", "in", "not in", "like", "<", ">"], type: Array(50).fill("string"), nature: Array(50).fill("operator"), - hideSuggestion: Array(50).fill(false), }; let items: string[] = []; @@ -104,7 +105,6 @@ const createSuggestions = async ( items: items, type: Array(items.length).fill("string"), nature: Array(items.length).fill("value"), - hideSuggestion: Array(items.length).fill(previousToken.hideSuggestion), }; }; diff --git a/packages/diracx-web-components/test/SearchBar.test.tsx b/packages/diracx-web-components/test/SearchBar.test.tsx index c8745bcd..fc0d61a5 100644 --- a/packages/diracx-web-components/test/SearchBar.test.tsx +++ b/packages/diracx-web-components/test/SearchBar.test.tsx @@ -76,11 +76,9 @@ describe("SearchBar", () => { await user.type(searchInput, "Status"); await user.keyboard("{Enter}"); - // Type operator - const operatorInput = screen.getByPlaceholderText("Enter an operator"); - await user.type(operatorInput, "="); - // Check if operator suggestions appear + const operatorInput = screen.getByPlaceholderText("Enter an operator"); + await user.type(operatorInput, "{downArrow}"); await waitFor(() => { expect(screen.getByText("=")).toBeInTheDocument(); }); diff --git a/packages/diracx-web/test/e2e/jobMonitor.cy.ts b/packages/diracx-web/test/e2e/jobMonitor.cy.ts index 889cd132..0f79f125 100644 --- a/packages/diracx-web/test/e2e/jobMonitor.cy.ts +++ b/packages/diracx-web/test/e2e/jobMonitor.cy.ts @@ -441,16 +441,16 @@ describe("Job Monitor", () => { it("should handle filter editing", () => { cy.get("table").should("be.visible"); - cy.get("[data-testid=search-field]").type("Name{enter}={enter}test{enter}"); + cy.get("[data-testid=search-field]").type("ID{enter}={enter}1{enter}"); cy.get("[data-testid=search-field]").type("{leftArrow}2{enter}"); - cy.get('[role="group"]').find("button").contains("test2").should("exist"); + cy.get('[role="group"]').find("button").contains("12").should("exist"); }); it("should handle filter clear", () => { cy.get("table").should("be.visible"); - cy.get("[data-testid=search-field]").type("Name{enter}={enter}test{enter}"); + cy.get("[data-testid=search-field]").type("ID{enter}={enter}1{enter}"); cy.get('[role="group"]').find("button").should("have.length", 5); @@ -533,7 +533,7 @@ describe("Job Monitor", () => { it("should render the sunburst chart", () => { // Click on the sunburst button - cy.get('[role="group"]').last().click(); + cy.get('[role="group"]').get("[data-testid='DonutSmallIcon']").click(); // Make sure the sunburst chart is visible cy.get('[data-testid="sunburst-chart"]').should("be.visible");