diff --git a/apps/shade/src/components/ui/filters.tsx b/apps/shade/src/components/ui/filters.tsx index a85749b5be3..65a60280f7e 100644 --- a/apps/shade/src/components/ui/filters.tsx +++ b/apps/shade/src/components/ui/filters.tsx @@ -102,7 +102,7 @@ export interface FilterI18nConfig { // Default English i18n configuration export const DEFAULT_I18N: FilterI18nConfig = { // UI Labels - addFilter: 'Add filter', + addFilter: '', searchFields: 'Search fields...', noFieldsFound: 'No fields found.', noResultsFound: 'No results found.', @@ -119,7 +119,7 @@ export const DEFAULT_I18N: FilterI18nConfig = { percent: '%', defaultCurrency: '$', defaultColor: '#000000', - addFilterTitle: 'Add filter', + addFilterTitle: '', // Operators operators: { @@ -236,7 +236,7 @@ const filterInputVariants = cva( size: { lg: 'h-10 px-2.5 text-sm has-[[data-slot=filters-prefix]]:ps-0 has-[[data-slot=filters-suffix]]:pe-0', md: 'h-[34px] px-2 text-sm has-[[data-slot=filters-prefix]]:ps-0 has-[[data-slot=filters-suffix]]:pe-0', - sm: 'h-8 px-1.5 text-xs has-[[data-slot=filters-prefix]]:ps-0 has-[[data-slot=filters-suffix]]:pe-0' + sm: 'h-8 px-2 text-xs has-[[data-slot=filters-prefix]]:ps-0 has-[[data-slot=filters-suffix]]:pe-0' }, cursorPointer: { true: 'cursor-pointer', @@ -747,6 +747,8 @@ export interface FilterFieldConfig { // Controlled values support for this field value?: T[]; onValueChange?: (values: T[]) => void; + // Auto-close dropdown after selection (even for multiselect types) + autoCloseOnSelect?: boolean; } // Helper functions to handle both flat and grouped field configurations @@ -941,7 +943,7 @@ function FilterOperatorDropdown({field, operator, values, onChange} // If hideOperatorSelect is true, just render the operator as plain text if (field.hideOperatorSelect) { return ( -
+
{operatorLabel}
); @@ -1046,6 +1048,7 @@ function SelectOptionsPopover({ const handleClose = () => { setOpen(false); + setTimeout(() => setSearchInput(''), 200); onClose?.(); }; @@ -1128,6 +1131,10 @@ function SelectOptionsPopover({ } else { onChange(newValues); } + // Auto-close if configured + if (field.autoCloseOnSelect) { + onClose?.(); + } // For multiselect, don't close the popover to allow multiple selections } else { if (field.onValueChange) { @@ -1191,7 +1198,13 @@ function SelectOptionsPopover({ )}
- + {field.searchable !== false && ( ({ return; // Don't exceed max selections } onChange(newValues); + // Auto-close if configured + if (field.autoCloseOnSelect) { + handleClose(); + } } else { onChange([option.value] as T[]); setOpen(false); @@ -1625,7 +1642,7 @@ function FilterValueSelector({field, values, onChange, operator}: F )} - + {field.searchable !== false && ( ({ )} - + {selectedFieldForOptions ? ( // Show original select/multiselect rendering without back button // SelectOptionsPopover renders its own Command component when inline={true} @@ -2073,7 +2096,11 @@ export function Filters({ const shouldClosePopover = selectedFieldForOptions.type === 'select'; addFilterWithOption(selectedFieldForOptions, values as unknown[], shouldClosePopover); }} - onClose={() => setAddFilterOpen(false)} + onClose={() => { + setAddFilterOpen(false); + setSelectedFieldKeyForOptions(null); + setTempSelectedValues([]); + }} /> ) : ( // Show field selection - needs Command wrapper for search/list diff --git a/apps/stats/src/views/Stats/components/stats-filter.tsx b/apps/stats/src/views/Stats/components/stats-filter.tsx index 0f5e1f21fda..aaa41cfdb10 100644 --- a/apps/stats/src/views/Stats/components/stats-filter.tsx +++ b/apps/stats/src/views/Stats/components/stats-filter.tsx @@ -90,7 +90,7 @@ const useUtmOptionsForField = (fieldKey: string, currentFilters: Filter[] = []) value: value, // Add a custom icon element that shows the count badge icon: ( - + {visits.toLocaleString()} ) @@ -156,7 +156,7 @@ const useSourceOptions = (currentFilters: Filter[] = []) => { value, // Add a custom icon element that shows the count badge icon: ( - + {visits.toLocaleString()} ) @@ -185,7 +185,7 @@ const usePostOptions = () => { // When searching, filter by title containing the search query // When not searching, fetch latest 20 published posts const hasSearchQuery = debouncedSearchQuery.trim().length > 0; - + const filter = hasSearchQuery ? `title:~'${debouncedSearchQuery.replace(/'/g, '\\\'')}'+status:[published,sent]` : 'status:[published,sent]'; @@ -211,7 +211,7 @@ const usePostOptions = () => { value: post.uuid })); }, [browseData]); - + // Memoize the callback to avoid recreating the function on each render const setSearchQuery = useCallback((query: string) => { setSearchQueryInternal(query); @@ -383,7 +383,8 @@ function StatsFilter({filters, utmTrackingEnabled = false, onChange, ...props}: defaultOperator: 'is', hideOperatorSelect: true, options: utmSourceOptions, - searchable: true + searchable: true, + selectedOptionsClassName: 'hidden' }, { key: 'utm_medium', @@ -395,7 +396,8 @@ function StatsFilter({filters, utmTrackingEnabled = false, onChange, ...props}: defaultOperator: 'is', hideOperatorSelect: true, options: utmMediumOptions, - searchable: true + searchable: true, + selectedOptionsClassName: 'hidden' }, { key: 'utm_campaign', @@ -407,7 +409,8 @@ function StatsFilter({filters, utmTrackingEnabled = false, onChange, ...props}: defaultOperator: 'is', hideOperatorSelect: true, options: utmCampaignOptions, - searchable: true + searchable: true, + selectedOptionsClassName: 'hidden' }, { key: 'utm_content', @@ -419,7 +422,8 @@ function StatsFilter({filters, utmTrackingEnabled = false, onChange, ...props}: defaultOperator: 'is', hideOperatorSelect: true, options: utmContentOptions, - searchable: true + searchable: true, + selectedOptionsClassName: 'hidden' }, { key: 'utm_term', @@ -431,7 +435,8 @@ function StatsFilter({filters, utmTrackingEnabled = false, onChange, ...props}: defaultOperator: 'is', hideOperatorSelect: true, options: utmTermOptions, - searchable: true + searchable: true, + selectedOptionsClassName: 'hidden' } ] : []; @@ -444,13 +449,16 @@ function StatsFilter({filters, utmTrackingEnabled = false, onChange, ...props}: label: 'Audience', type: 'multiselect', icon: , - options: audienceOptions.map(({value, label, icon}) => ({value, label, icon})) + options: audienceOptions.map(({value, label, icon}) => ({value, label, icon})), + defaultOperator: 'is any of', + hideOperatorSelect: true, + autoCloseOnSelect: true }, { key: 'post', label: 'Post or page', type: 'select', - icon: , + icon: , options: postOptions, searchable: true, asyncSearch: true, @@ -458,6 +466,8 @@ function StatsFilter({filters, utmTrackingEnabled = false, onChange, ...props}: onSearchChange: setSearchQuery, operators: supportedOperators, defaultOperator: 'is', + className: 'w-80', + popoverContentClassName: 'w-80', hideOperatorSelect: true }, { @@ -470,7 +480,8 @@ function StatsFilter({filters, utmTrackingEnabled = false, onChange, ...props}: defaultOperator: 'is', hideOperatorSelect: true, options: sourceOptions, - searchable: true + searchable: true, + selectedOptionsClassName: 'hidden' } ] }, @@ -483,12 +494,13 @@ function StatsFilter({filters, utmTrackingEnabled = false, onChange, ...props}: return ( : } - addButtonText={filters.length ? 'Add filter' : 'Filter'} - className='mb-6 mt-0.5 [&>button]:order-last' + addButtonIcon={} + addButtonText={filters.length ? '' : ''} + className='mb-6 [&>button]:order-last' fields={groupedFields} filters={filters} showSearchInput={false} + size='sm' onChange={handleFilterChange} {...props} /> diff --git a/apps/stats/src/views/Stats/layout/stats-header.tsx b/apps/stats/src/views/Stats/layout/stats-header.tsx index 4a12fdf3c2c..96b2408693c 100644 --- a/apps/stats/src/views/Stats/layout/stats-header.tsx +++ b/apps/stats/src/views/Stats/layout/stats-header.tsx @@ -64,7 +64,7 @@ const StatsHeader:React.FC = ({ )} - + { navigate('/analytics/');