This is an automated email from the ASF dual-hosted git repository.
ppawar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/atlas.git
The following commit(s) were added to refs/heads/master by this push:
new b3175a301 ATLAS-5166: React UI: Save search, Quick search, Alignment
issue (#485)
b3175a301 is described below
commit b3175a301b04d8bd2bef12cb60160bae8671d603
Author: Prasad Pawar <[email protected]>
AuthorDate: Wed Dec 10 16:41:20 2025 +0530
ATLAS-5166: React UI: Save search, Quick search, Alignment issue (#485)
---
dashboard/src/components/FilterQuery.tsx | 32 +++-
.../src/components/GlobalSearch/QuickSearch.tsx | 172 ++++++++++++++++-----
dashboard/src/styles/classificationForm.scss | 13 ++
.../BusinessMetadata/BusinessMetadataForm.tsx | 2 +-
.../views/Classification/ClassificationForm.tsx | 1 -
.../PropertiesTab/BMAttributesFields.tsx | 34 ++--
.../RelationshipPropertiesTab.tsx | 38 +++--
dashboard/src/views/Glossary/GlossaryForm.tsx | 2 +-
.../src/views/SideBar/SideBarTree/SideBarTree.tsx | 147 ++++++++++++++++--
9 files changed, 361 insertions(+), 80 deletions(-)
diff --git a/dashboard/src/components/FilterQuery.tsx
b/dashboard/src/components/FilterQuery.tsx
index 644d8e67e..fb14e0288 100644
--- a/dashboard/src/components/FilterQuery.tsx
+++ b/dashboard/src/components/FilterQuery.tsx
@@ -108,7 +108,32 @@ export const FilterQuery = ({ value }: any) => {
searchParams.delete(currentType);
- if ([...searchParams]?.length == 1) {
+ // Check if there are any meaningful filters left after removal
+ const meaningfulFilterParams = [
+ "type",
+ "tag",
+ "query",
+ "term",
+ "relationshipName",
+ "entityFilters",
+ "tagFilters",
+ "relationshipFilters",
+ "excludeST",
+ "excludeSC",
+ "includeDE"
+ ];
+
+ const hasMeaningfulFilters = meaningfulFilterParams.some((param) =>
+ searchParams.has(param)
+ );
+
+ // If no meaningful filters remain, navigate to clear all (like Clear
button)
+ if (!hasMeaningfulFilters) {
+ navigate({
+ pathname: "/search"
+ });
+ } else if ([...searchParams]?.length <= 1) {
+ // Only searchType or other system params remain
navigate({
pathname: "/search"
});
@@ -440,6 +465,11 @@ export const FilterQuery = ({ value }: any) => {
queryArray.push(<div className="group">{queryIncludeDE}</div>);
}
+ // If no filters to display, return null (don't show empty parentheses)
+ if (queryArray.length === 0) {
+ return null;
+ }
+
return (
<>
{queryArray.length === 1 ? (
diff --git a/dashboard/src/components/GlobalSearch/QuickSearch.tsx
b/dashboard/src/components/GlobalSearch/QuickSearch.tsx
index 5f6fa12b2..1a8700755 100644
--- a/dashboard/src/components/GlobalSearch/QuickSearch.tsx
+++ b/dashboard/src/components/GlobalSearch/QuickSearch.tsx
@@ -115,9 +115,12 @@ const QuickSearch = () => {
};
const onInputChange = (_event: any, value: string) => {
- if (value) {
+ // Sanitize input: remove any potential script tags and validate
+ const sanitizedValue = value ? value.trim() : "";
+
+ if (sanitizedValue) {
setOpen(true);
- getData(value);
+ getData(sanitizedValue);
} else {
setOptions([]);
setValue("");
@@ -125,27 +128,69 @@ const QuickSearch = () => {
}
};
- const handleValues = (option: HandleValuesType) => {
- const { entityObj, title, types } = option;
- setOpen(false);
- setOptions([]);
- searchParams.set("query", title);
- searchParams.set("searchType", "basic");
-
- types == "Entities"
- ? navigate(
- {
- pathname: `/detailPage/${entityObj.guid}`
- },
- { replace: true }
- )
- : navigate(
+ const handleValues = (option: HandleValuesType | string | null) => {
+ // Handle case when option is a string (direct search query)
+ if (typeof option === "string") {
+ const queryValue = option.trim();
+ if (queryValue) {
+ setOpen(false);
+ setOptions([]);
+ // URLSearchParams automatically encodes the value, preventing XSS
+ // Additional validation: ensure query is not empty after trim
+ const sanitizedQuery = queryValue || "*";
+ searchParams.set("query", sanitizedQuery);
+ searchParams.set("searchType", "basic");
+ navigate(
{
pathname: `/search/searchResult`,
search: searchParams.toString()
},
{ replace: true }
);
+ }
+ return;
+ }
+
+ // Handle case when option is null or undefined
+ if (!option || typeof option !== "object") {
+ return;
+ }
+
+ const { entityObj, title, types } = option;
+
+ // Validate that title exists and is not undefined
+ if (!title || title === "undefined" || typeof title !== "string") {
+ return;
+ }
+
+ // Sanitize title: trim and validate
+ const sanitizedTitle = title.trim();
+ if (!sanitizedTitle) {
+ return;
+ }
+
+ setOpen(false);
+ setOptions([]);
+ // URLSearchParams automatically encodes the value, preventing XSS
+ searchParams.set("query", sanitizedTitle);
+ searchParams.set("searchType", "basic");
+
+ if (types === "Entities" && entityObj && entityObj.guid) {
+ navigate(
+ {
+ pathname: `/detailPage/${entityObj.guid}`
+ },
+ { replace: true }
+ );
+ } else {
+ navigate(
+ {
+ pathname: `/search/searchResult`,
+ search: searchParams.toString()
+ },
+ { replace: true }
+ );
+ }
};
const handleClickAway = () => {
@@ -172,18 +217,30 @@ const QuickSearch = () => {
const code = e.keyCode || e.which;
switch (code) {
- case 13:
+ case 13: // Enter key
+ e.preventDefault();
+ const inputValue = (e.target as
HTMLInputElement).value.trim();
+
+ // If input is empty, use "*" for wildcard search (matching
classic UI behavior)
+ const searchQuery = inputValue === "" ? "*" : inputValue;
+
+ // Try to find exact match in options
const activeOption = options.find(
(option: { title: any }) =>
typeof option !== "string" &&
- option.title === (e.target as HTMLInputElement).value
+ option.title === searchQuery
);
+
if (activeOption) {
+ // If exact match found, use that option
handleValues(activeOption as HandleValuesType);
+ } else {
+ // If no match found, trigger basic search with typed
value (matching classic UI behavior)
+ handleValues(searchQuery);
}
break;
- case 9:
- case 27:
+ case 9: // Tab key
+ case 27: // Escape key
setOpen(false);
break;
default:
@@ -204,9 +261,19 @@ const QuickSearch = () => {
}}
value={value}
onChange={(_event: any, newValue: any) => {
- setOptions(newValue ? [newValue, ...options] : options);
- setValue(newValue);
- handleValues(newValue);
+ if (newValue) {
+ // Only update options if newValue is a valid option object
+ if (typeof newValue === "object" && newValue.title) {
+ setOptions([newValue, ...options]);
+ }
+ setValue(newValue);
+ // Only call handleValues if newValue is a valid option
+ if (typeof newValue === "object" && newValue.title &&
newValue.title !== "undefined") {
+ handleValues(newValue);
+ }
+ } else {
+ setValue("");
+ }
}}
clearOnBlur={false}
autoComplete={true}
@@ -214,9 +281,14 @@ const QuickSearch = () => {
noOptionsText={"No Entities"}
disableClearable
onInputChange={onInputChange}
- getOptionLabel={(option: string | QuickSearchOptionListType) =>
- typeof option === "string" ? option : (option as any).title
- }
+ getOptionLabel={(option: string | QuickSearchOptionListType) => {
+ if (typeof option === "string") {
+ return option;
+ }
+ // Safely extract title, defaulting to empty string if undefined
+ const title = (option as any)?.title;
+ return title && typeof title === "string" ? title : "";
+ }}
renderOption={(props, option, { inputValue }) => {
const { entityObj, types, parent } =
typeof option !== "string" &&
@@ -238,21 +310,31 @@ const QuickSearch = () => {
typeof option !== "string" && "title" in option
? option.title
: option;
- const href = `/detailPage/${
- (entityObj as { guid: string })?.guid
- }`;
+
+ // Validate and sanitize title to prevent XSS
+ const safeTitle = typeof title === "string" ? title : "";
+
+ // Validate guid to prevent XSS in URL
+ const guid = (entityObj as { guid?: string })?.guid;
+ const safeGuid = guid && typeof guid === "string" ? guid : "";
+ const href = safeGuid ? `/detailPage/${safeGuid}` : "#";
+
const { name }: { name: string; found: boolean; key: any } =
extractKeyValueFromEntity(entityObj);
+
+ // Safely handle name extraction
+ const safeName = name && typeof name === "string" ? name : "";
+
const matches = match(
- types == "Entities" ? (name as string) : (title as string),
- inputValue,
+ types === "Entities" ? safeName : safeTitle,
+ inputValue || "",
{
findAllOccurrences: true,
insideWords: true
}
);
const parts = parse(
- types == "Entities" ? (name as string) : (title as string),
+ types === "Entities" ? safeName : safeTitle,
matches
);
return (
@@ -273,7 +355,7 @@ const QuickSearch = () => {
}
}}
>
- {types == "Entities" && !isEmpty(entityObj) ? (
+ {types === "Entities" && !isEmpty(entityObj) ? (
<Link
className="entity-name text-decoration-none"
style={{
@@ -296,10 +378,10 @@ const QuickSearch = () => {
}
>
{" "}
- {types == "Entities" && !isEmpty(entityObj) && (
+ {types === "Entities" && !isEmpty(entityObj) && (
<DisplayImage entity={entityObj} />
)}{" "}
- {types == "Entities" && !isEmpty(entityObj)
+ {types === "Entities" && !isEmpty(entityObj)
? parts.map((part, index) => (
<Stack
flexDirection="row"
@@ -308,7 +390,7 @@ const QuickSearch = () => {
fontWeight: part.highlight ? "bold" : "regular"
}}
>
- {entityObj?.guid != "-1" && !part.highlight ? (
+ {entityObj?.guid !== "-1" && !part.highlight ? (
<Link
className="entity-name text-blue
text-decoration-none"
style={{
@@ -345,7 +427,7 @@ const QuickSearch = () => {
{part.text}
</Stack>
))}
- {types == "Entities" &&
+ {types === "Entities" &&
!isEmpty(entityObj) &&
` (${parent})`}
</Link>
@@ -410,13 +492,25 @@ const QuickSearch = () => {
<CustomButton
variant="outlined"
size="small"
+ sx={{
+ backgroundColor: "white !important",
+ color: "#4a90e2 !important",
+ borderColor: "#dddddd !important",
+ "&:hover": {
+ backgroundColor: "rgba(74, 144, 226, 0.08) !important",
+ // borderColor: "#4a90e2 !important",
+ color: "#4a90e2 !important"
+ }
+ }}
onClick={() => {
setOpenAdvanceSearch(true);
}}
>
<Typography
sx={{
- color: location?.pathname == "/search" ? "blue" : "eeeeee"
+ color: "#4a90e2 !important",
+ fontWeight: "600 !important",
+ fontSize: "0.875rem !important"
}}
display="inline"
>
diff --git a/dashboard/src/styles/classificationForm.scss
b/dashboard/src/styles/classificationForm.scss
index eb81c6810..612ff3665 100644
--- a/dashboard/src/styles/classificationForm.scss
+++ b/dashboard/src/styles/classificationForm.scss
@@ -22,3 +22,16 @@
.classification-form-editor .ql-container {
min-height: 90px !important;
}
+
+/* Prevent duplicate toolbars in ReactQuill */
+.classification-form-editor .ql-toolbar:not(:first-of-type) {
+ display: none !important;
+}
+
+.classification-form-editor .ql-toolbar {
+ border-top: 1px solid #ccc;
+ border-bottom: 1px solid #ccc;
+ box-sizing: border-box;
+ font-family: 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif;
+ padding: 8px;
+}
diff --git a/dashboard/src/views/BusinessMetadata/BusinessMetadataForm.tsx
b/dashboard/src/views/BusinessMetadata/BusinessMetadataForm.tsx
index 2f6af49dc..5fcbb22ed 100644
--- a/dashboard/src/views/BusinessMetadata/BusinessMetadataForm.tsx
+++ b/dashboard/src/views/BusinessMetadata/BusinessMetadataForm.tsx
@@ -448,7 +448,6 @@ const BusinessMetaDataForm = ({
{alignment == "formatted" ? (
<div style={{ position: "relative" }}>
<ReactQuill
- {...field}
theme="snow"
placeholder={"Description required"}
onChange={(text) => {
@@ -456,6 +455,7 @@ const BusinessMetaDataForm = ({
setValue("description", text);
}}
className="classification-form-editor"
+ value={field.value || ""}
/>
</div>
) : (
diff --git a/dashboard/src/views/Classification/ClassificationForm.tsx
b/dashboard/src/views/Classification/ClassificationForm.tsx
index 61bd4582d..170f9f16c 100644
--- a/dashboard/src/views/Classification/ClassificationForm.tsx
+++ b/dashboard/src/views/Classification/ClassificationForm.tsx
@@ -314,7 +314,6 @@ const ClassificationForm = ({
{alignment == "formatted" ? (
<div style={{ position: "relative" }}>
<ReactQuill
- {...field}
theme="snow"
placeholder={"Description required"}
value={descriptionValue}
diff --git
a/dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/BMAttributesFields.tsx
b/dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/BMAttributesFields.tsx
index 2cd05d688..76f618e8d 100644
---
a/dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/BMAttributesFields.tsx
+++
b/dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/BMAttributesFields.tsx
@@ -138,21 +138,22 @@ const BMAttributesFields = ({ obj, control, index }: any)
=> {
required: true
}}
defaultValue={""}
- render={({ field }) => (
- <Stack gap="0.5rem">
- <div style={{ position: "relative", flexBasis: "100%" }}>
- {typeName == "string" ? (
- <ReactQuill
- {...field}
- theme="snow"
- placeholder={"Enter String"}
- onChange={(text) => {
- field.onChange(text);
- }}
- className="classification-form-editor"
- value={typeof field.value === "string" ? field.value : ""}
- />
- ) : (
+ render={({ field }) => {
+ return (
+ <Stack gap="0.5rem">
+ <div style={{ position: "relative", flexBasis: "100%" }}>
+ {typeName == "string" ? (
+ <ReactQuill
+ key={`quill-${index}-${name}`}
+ theme="snow"
+ placeholder={"Enter String"}
+ onChange={(text) => {
+ field.onChange(text);
+ }}
+ className="classification-form-editor"
+ value={typeof field.value === "string" ? field.value : ""}
+ />
+ ) : (
<TextField
margin="none"
fullWidth
@@ -176,7 +177,8 @@ const BMAttributesFields = ({ obj, control, index }: any)
=> {
)}
</div>
</Stack>
- )}
+ );
+ }}
/>
);
} else if (typeName === "boolean") {
diff --git
a/dashboard/src/views/DetailPage/RelationshipDetails/RelationshipPropertiesTab.tsx
b/dashboard/src/views/DetailPage/RelationshipDetails/RelationshipPropertiesTab.tsx
index 9d3b5f68a..d0e99794c 100644
---
a/dashboard/src/views/DetailPage/RelationshipDetails/RelationshipPropertiesTab.tsx
+++
b/dashboard/src/views/DetailPage/RelationshipDetails/RelationshipPropertiesTab.tsx
@@ -190,13 +190,20 @@ const RelationshipPropertiesTab = (props: {
spacing={4}
marginBottom={1}
marginTop={1}
+ sx={{
+ flexWrap: "nowrap",
+ alignItems: "flex-start"
+ }}
>
<div
style={{
- flex: 1,
- wordBreak: "break-all",
+ flexBasis: "30%",
+ flexShrink: 0,
+ minWidth: "120px",
+ wordBreak: "break-word",
textAlign: "left",
- fontWeight: "600"
+ fontWeight: "600",
+ paddingRight: "16px"
}}
>
{`${keys} ${
@@ -205,8 +212,9 @@ const RelationshipPropertiesTab = (props: {
</div>
<div
style={{
- // flex: 1,
- wordBreak: "break-all",
+ flex: 1,
+ minWidth: 0,
+ wordBreak: "break-word",
textAlign: "left"
}}
>
@@ -242,7 +250,7 @@ const RelationshipPropertiesTab = (props: {
</AccordionSummary>
<AccordionDetails>
{" "}
- {!isEmpty(end1)
+ {!isEmpty(end2)
? Object.entries(end2)
.sort()
.map(([keys, value]: [string, any]) => {
@@ -253,13 +261,20 @@ const RelationshipPropertiesTab = (props: {
spacing={4}
marginBottom={1}
marginTop={1}
+ sx={{
+ flexWrap: "nowrap",
+ alignItems: "flex-start"
+ }}
>
<div
style={{
- flex: 1,
- wordBreak: "break-all",
+ flexBasis: "30%",
+ flexShrink: 0,
+ minWidth: "120px",
+ wordBreak: "break-word",
textAlign: "left",
- fontWeight: "600"
+ fontWeight: "600",
+ paddingRight: "16px"
}}
>
{`${keys} ${
@@ -268,8 +283,9 @@ const RelationshipPropertiesTab = (props: {
</div>
<div
style={{
- // flex: 1,
- wordBreak: "break-all",
+ flex: 1,
+ minWidth: 0,
+ wordBreak: "break-word",
textAlign: "left"
}}
>
diff --git a/dashboard/src/views/Glossary/GlossaryForm.tsx
b/dashboard/src/views/Glossary/GlossaryForm.tsx
index bdbae34c8..3ab9a82fd 100644
--- a/dashboard/src/views/Glossary/GlossaryForm.tsx
+++ b/dashboard/src/views/Glossary/GlossaryForm.tsx
@@ -142,7 +142,6 @@ const GlossaryForm = (props: {
{alignment == "formatted" ? (
<div style={{ position: "relative" }}>
<ReactQuill
- {...field}
theme="snow"
placeholder={"Description required"}
onChange={(text) => {
@@ -150,6 +149,7 @@ const GlossaryForm = (props: {
setValue("description", text);
}}
className="classification-form-editor"
+ value={field.value || ""}
/>
</div>
) : (
diff --git a/dashboard/src/views/SideBar/SideBarTree/SideBarTree.tsx
b/dashboard/src/views/SideBar/SideBarTree/SideBarTree.tsx
index 2d75117cd..fd4bf92f9 100644
--- a/dashboard/src/views/SideBar/SideBarTree/SideBarTree.tsx
+++ b/dashboard/src/views/SideBar/SideBarTree/SideBarTree.tsx
@@ -61,6 +61,8 @@ import {
import Stack from "@mui/material/Stack";
import { globalSearchFilterInitialQuery, isEmpty } from "@utils/Utils";
+import { attributeFilter } from "@utils/CommonViewFunction";
+import { cloneDeep } from "@utils/Helper";
import LaunchOutlinedIcon from "@mui/icons-material/LaunchOutlined";
import { getGlossaryImportTmpl } from "@api/apiMethods/glossaryApiMethod";
import { toast } from "react-toastify";
@@ -593,12 +595,13 @@ const BarTreeView: FC<{
break;
}
- searchParams.delete("attributes");
- searchParams.delete("entityFilters");
- searchParams.delete("tagFilters");
- searchParams.delete("relationshipFilters");
- // Always reset pagination defaults on tree navigation
+ // Note: Don't delete filters here for CustomFilters - they will be set by
setCustomFiltersSearchParams
if (treeName !== "CustomFilters") {
+ searchParams.delete("attributes");
+ searchParams.delete("entityFilters");
+ searchParams.delete("tagFilters");
+ searchParams.delete("relationshipFilters");
+ // Always reset pagination defaults on tree navigation
searchParams.set("pageLimit", "25");
searchParams.set("pageOffset", "0");
}
@@ -633,6 +636,7 @@ const BarTreeView: FC<{
searchParams: URLSearchParams,
savedSearchData: any[]
) => {
+ // Clear all existing params except searchType
const keys = Array.from(searchParams.keys());
for (let i = 0; i < keys.length; i++) {
if (keys[i] !== "searchType") {
@@ -640,18 +644,137 @@ const BarTreeView: FC<{
}
}
+ // Clear globalSearchFilterInitialQuery when applying new saved search
+ globalSearchFilterInitialQuery.setQuery({});
+
if (treeName === "CustomFilters") {
const params = savedSearchData.find((obj) => obj.name === node.id);
- for (const key in params?.searchParameters) {
- if (shouldSetCustomFilterParam(node, key)) {
- setCustomFilterParam(searchParams, key,
params.searchParameters[key]);
+ if (params) {
+ const searchParamsObj = params?.searchParameters || {};
+
+ // Step 1: Set searchType based on saved search type
+ if (params.searchType) {
+ const searchTypeValue = params.searchType.toLowerCase() ===
"advanced" ? "advanced" : "basic";
+ searchParams.set("searchType", searchTypeValue);
+ }
+
+ // Step 2: Apply basic search parameters (excluding filters which are
handled separately)
+ for (const key in searchParamsObj) {
+ if (shouldSetCustomFilterParam(node, key) &&
+ !["entityFilters", "tagFilters",
"relationshipFilters"].includes(key)) {
+ setCustomFilterParam(searchParams, key, searchParamsObj[key]);
+ }
+ }
+
+ // Step 3: Convert and apply entityFilters from API format to URL
string format
+ if (searchParamsObj.entityFilters &&
!isEmpty(searchParamsObj.entityFilters)) {
+ const clonedFilter = cloneDeep(searchParamsObj.entityFilters);
+ const ruleUrl = attributeFilter.generateUrl({
+ value: clonedFilter,
+ formatedDateToLong: true
+ });
+
+ if (ruleUrl && !isEmpty(ruleUrl) && typeof ruleUrl === "string") {
+ searchParams.set("entityFilters", ruleUrl);
+
+ // Convert API format to query builder format for Filters
component UI
+ const qbFilter =
convertApiToQueryBuilder(searchParamsObj.entityFilters);
+ if (qbFilter && (qbFilter.rules || qbFilter.combinator)) {
+ globalSearchFilterInitialQuery.setQuery({
+ entityFilters: qbFilter
+ });
+ }
+ }
+ }
+
+ // Step 4: Convert and apply tagFilters from API format to URL string
format
+ if (searchParamsObj.tagFilters &&
!isEmpty(searchParamsObj.tagFilters)) {
+ const clonedFilter = cloneDeep(searchParamsObj.tagFilters);
+ const ruleUrl = attributeFilter.generateUrl({
+ value: clonedFilter,
+ formatedDateToLong: true
+ });
+
+ if (ruleUrl && !isEmpty(ruleUrl) && typeof ruleUrl === "string") {
+ searchParams.set("tagFilters", ruleUrl);
+
+ // Convert API format to query builder format for Filters
component UI
+ const qbFilter =
convertApiToQueryBuilder(searchParamsObj.tagFilters);
+ if (qbFilter && (qbFilter.rules || qbFilter.combinator)) {
+ globalSearchFilterInitialQuery.setQuery({
+ tagFilters: qbFilter
+ });
+ }
+ }
+ }
+
+ // Step 5: Convert and apply relationshipFilters from API format to
URL string format
+ if (searchParamsObj.relationshipFilters &&
!isEmpty(searchParamsObj.relationshipFilters)) {
+ const clonedFilter = cloneDeep(searchParamsObj.relationshipFilters);
+ const ruleUrl = attributeFilter.generateUrl({
+ value: clonedFilter,
+ formatedDateToLong: true
+ });
+
+ if (ruleUrl && !isEmpty(ruleUrl) && typeof ruleUrl === "string") {
+ searchParams.set("relationshipFilters", ruleUrl);
+
+ // Convert API format to query builder format for Filters
component UI
+ const qbFilter =
convertApiToQueryBuilder(searchParamsObj.relationshipFilters);
+ if (qbFilter && (qbFilter.rules || qbFilter.combinator)) {
+ globalSearchFilterInitialQuery.setQuery({
+ relationshipFilters: qbFilter
+ });
+ }
+ }
}
+
+ searchParams.set("isCF", "true");
}
- searchParams.set("isCF", "true");
} else {
searchParams.set("relationshipName", node.id);
}
};
+
+ // Helper function to convert API format filter (criterion/condition) to
query builder format (rules/combinator)
+ const convertApiToQueryBuilder = (apiFilter: any): any => {
+ if (!apiFilter || typeof apiFilter !== "object") {
+ return null;
+ }
+
+ const result: any = {};
+
+ // Convert condition to combinator
+ if (apiFilter.condition) {
+ result.combinator = apiFilter.condition.toLowerCase();
+ } else {
+ result.combinator = "and"; // default
+ }
+
+ // Convert criterion to rules
+ if (apiFilter.criterion && Array.isArray(apiFilter.criterion)) {
+ result.rules = apiFilter.criterion.map((rule: any) => {
+ // If nested condition, recurse
+ if (rule.condition || rule.criterion) {
+ return convertApiToQueryBuilder(rule);
+ }
+ // Convert API rule format to query builder format
+ return {
+ field: rule.attributeName || rule.id,
+ operator: rule.operator,
+ value: rule.attributeValue || rule.value,
+ type: rule.type || rule.attributeType
+ };
+ });
+ } else if (apiFilter.rules && Array.isArray(apiFilter.rules)) {
+ // Already in query builder format
+ result.rules = apiFilter.rules.map((rule: any) =>
+ rule.condition || rule.criterion ? convertApiToQueryBuilder(rule) :
rule
+ );
+ }
+
+ return Object.keys(result).length > 0 ? result : null;
+ };
const shouldSetCustomFilterParam = (node: TreeNode, key: string) => {
return (
@@ -673,7 +796,11 @@ const BarTreeView: FC<{
searchParams.set("pageOffset", value);
} else if (key === "typeName") {
searchParams.set("type", value);
- } else {
+ } else if (key === "classification") {
+ // Map classification to tag parameter for URL (matching classic UI)
+ searchParams.set("tag", value);
+ } else if (value !== null && value !== undefined && value !== "") {
+ // Only set parameter if value is not null, undefined, or empty string
searchParams.set(key, value);
}
};