From ca965076d2635d3b6b3e20894cd1272895660a3c Mon Sep 17 00:00:00 2001 From: Caio Pizzol Date: Thu, 9 Oct 2025 14:40:45 -0300 Subject: [PATCH] feat: enhance FieldMenu with grouped field display and auto-expand functionality for categories --- src/defaults/FieldMenu.tsx | 216 +++++++++++++++++++++++++++---------- src/index.tsx | 2 +- 2 files changed, 160 insertions(+), 58 deletions(-) diff --git a/src/defaults/FieldMenu.tsx b/src/defaults/FieldMenu.tsx index 2341925..bd0d684 100644 --- a/src/defaults/FieldMenu.tsx +++ b/src/defaults/FieldMenu.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import type { FieldDefinition, FieldMenuProps } from "../types"; export const FieldMenu: React.FC = ({ @@ -37,11 +37,61 @@ export const FieldMenu: React.FC = ({ }; }, [position]); - if (!isVisible) return null; - - const visibleFields = filteredFields ?? availableFields; + const fieldsToDisplay = filteredFields ?? availableFields; const hasFilter = Boolean(filterQuery); - const hasVisibleFields = visibleFields.length > 0; + + const groupedFields = useMemo(() => { + const groups: { category: string; fields: FieldDefinition[] }[] = []; + const categoryIndex = new Map(); + + fieldsToDisplay.forEach((field) => { + const categoryName = field.category?.trim() || "Uncategorized"; + const existingIndex = categoryIndex.get(categoryName); + + if (existingIndex !== undefined) { + groups[existingIndex].fields.push(field); + return; + } + + categoryIndex.set(categoryName, groups.length); + groups.push({ category: categoryName, fields: [field] }); + }); + + return groups; + }, [fieldsToDisplay]); + + const [expandedCategories, setExpandedCategories] = useState>({}); + + useEffect(() => { + setExpandedCategories((previous) => { + if (groupedFields.length === 0) { + return Object.keys(previous).length === 0 ? previous : {}; + } + + const next: Record = {}; + let hasChanges = Object.keys(previous).length !== groupedFields.length; + + groupedFields.forEach(({ category }, index) => { + // Auto-expand all categories when filtering is active + const target = hasFilter ? true : (previous[category] ?? index === 0); + next[category] = target; + if (!hasChanges && previous[category] !== target) { + hasChanges = true; + } + }); + + return hasChanges ? next : previous; + }); + }, [groupedFields, hasFilter]); + + const toggleCategory = useCallback((category: string) => { + setExpandedCategories((previous) => ({ + ...previous, + [category]: !previous[category], + })); + }, []); + + if (!isVisible) return null; const handleCreateField = async () => { const trimmedName = newFieldName.trim(); @@ -68,24 +118,15 @@ export const FieldMenu: React.FC = ({ return (
-
+ {hasFilter && (
- Insert Field -
- {hasFilter && ( -
+
Filtering results for = ({ {filterQuery}
- )} -
- - {hasVisibleFields ? ( - visibleFields.map((field) => ( -
onSelect(field)} - style={{ - padding: "8px 16px", - cursor: "pointer", - }} - > - {field.label} - {field.category && ( - - {field.category} - - )} -
- )) - ) : ( -
- No matching fields
)} @@ -211,6 +214,105 @@ export const FieldMenu: React.FC = ({
)} + {allowCreate && availableFields.length > 0 && ( +
+ )} + + {groupedFields.length === 0 ? ( +
+ No matching fields +
+ ) : ( + groupedFields.map(({ category, fields }, index) => { + const isExpanded = Boolean(expandedCategories[category]); + const itemsMaxHeight = `${Math.max(fields.length * 40, 0)}px`; + + return ( +
+ +
+
+ {fields.map((field) => ( +
onSelect(field)} + style={{ + padding: "8px 16px", + cursor: "pointer", + display: "flex", + alignItems: "center", + justifyContent: "space-between", + }} + > + {field.label} +
+ ))} +
+
+
+ ); + }))} +