Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 35 additions & 8 deletions apps/shade/src/components/ui/filters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
Expand All @@ -119,7 +119,7 @@ export const DEFAULT_I18N: FilterI18nConfig = {
percent: '%',
defaultCurrency: '$',
defaultColor: '#000000',
addFilterTitle: 'Add filter',
addFilterTitle: '',

// Operators
operators: {
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -747,6 +747,8 @@ export interface FilterFieldConfig<T = unknown> {
// 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
Expand Down Expand Up @@ -941,7 +943,7 @@ function FilterOperatorDropdown<T = unknown>({field, operator, values, onChange}
// If hideOperatorSelect is true, just render the operator as plain text
if (field.hideOperatorSelect) {
return (
<div className="flex items-center border-x px-3 text-sm text-muted-foreground">
<div className="flex items-center self-stretch border border-r-[0px] px-3 text-sm text-muted-foreground">
{operatorLabel}
</div>
);
Expand Down Expand Up @@ -1046,6 +1048,7 @@ function SelectOptionsPopover<T = unknown>({

const handleClose = () => {
setOpen(false);
setTimeout(() => setSearchInput(''), 200);
onClose?.();
};

Expand Down Expand Up @@ -1128,6 +1131,10 @@ function SelectOptionsPopover<T = unknown>({
} 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) {
Expand Down Expand Up @@ -1191,7 +1198,13 @@ function SelectOptionsPopover<T = unknown>({
)}
</div>
</PopoverTrigger>
<PopoverContent align="start" className={cn('w-[200px] p-0', field.className)}>
<PopoverContent
align="start"
className={cn(
'p-0 data-[state=closed]:!animation-none data-[state=closed]:!duration-0',
field.className || 'w-[200px]'
)}
>
<Command shouldFilter={!isAsyncSearch}>
{field.searchable !== false && (
<CommandInput
Expand Down Expand Up @@ -1255,6 +1268,10 @@ function SelectOptionsPopover<T = unknown>({
return; // Don't exceed max selections
}
onChange(newValues);
// Auto-close if configured
if (field.autoCloseOnSelect) {
handleClose();
}
} else {
onChange([option.value] as T[]);
setOpen(false);
Expand Down Expand Up @@ -1625,7 +1642,7 @@ function FilterValueSelector<T = unknown>({field, values, onChange, operator}: F
)}
</div>
</PopoverTrigger>
<PopoverContent className={cn('w-36 p-0', field.popoverContentClassName)}>
<PopoverContent className={cn('w-36 p-0 data-[state=closed]:!animation-none data-[state=closed]:!duration-0', field.popoverContentClassName)}>
<Command shouldFilter={!isAsyncSearch}>
{field.searchable !== false && (
<CommandInput
Expand Down Expand Up @@ -2059,7 +2076,13 @@ export function Filters<T = unknown>({
</button>
)}
</PopoverTrigger>
<PopoverContent align={popoverAlign} className={cn('w-[200px] p-0', popoverContentClassName)}>
<PopoverContent
align={popoverAlign}
className={cn(
'p-0 data-[state=closed]:!animation-none data-[state=closed]:!duration-0',
selectedFieldForOptions?.className || popoverContentClassName || 'w-[200px]'
)}
>
{selectedFieldForOptions ? (
// Show original select/multiselect rendering without back button
// SelectOptionsPopover renders its own Command component when inline={true}
Expand All @@ -2073,7 +2096,11 @@ export function Filters<T = unknown>({
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
Expand Down
42 changes: 27 additions & 15 deletions apps/stats/src/views/Stats/components/stats-filter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ const useUtmOptionsForField = (fieldKey: string, currentFilters: Filter[] = [])
value: value,
// Add a custom icon element that shows the count badge
icon: (
<span className="flex items-center justify-center rounded-full bg-grey-200 px-2 py-0.5 text-xs font-medium text-grey-900 dark:bg-grey-800 dark:text-grey-100">
<span className="order-2 font-mono text-xs text-muted-foreground">
{visits.toLocaleString()}
</span>
)
Expand Down Expand Up @@ -156,7 +156,7 @@ const useSourceOptions = (currentFilters: Filter[] = []) => {
value,
// Add a custom icon element that shows the count badge
icon: (
<span className="flex items-center justify-center rounded-full bg-grey-200 px-2 py-0.5 text-xs font-medium text-grey-900 dark:bg-grey-800 dark:text-grey-100">
<span className="order-2 font-mono text-xs text-muted-foreground">
{visits.toLocaleString()}
</span>
)
Expand Down Expand Up @@ -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]';
Expand All @@ -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);
Expand Down Expand Up @@ -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',
Expand All @@ -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',
Expand All @@ -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',
Expand All @@ -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',
Expand All @@ -431,7 +435,8 @@ function StatsFilter({filters, utmTrackingEnabled = false, onChange, ...props}:
defaultOperator: 'is',
hideOperatorSelect: true,
options: utmTermOptions,
searchable: true
searchable: true,
selectedOptionsClassName: 'hidden'
}
] : [];

Expand All @@ -444,20 +449,25 @@ function StatsFilter({filters, utmTrackingEnabled = false, onChange, ...props}:
label: 'Audience',
type: 'multiselect',
icon: <LucideIcon.Users />,
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: <LucideIcon.File />,
icon: <LucideIcon.PenLine />,
options: postOptions,
searchable: true,
asyncSearch: true,
isLoading: postLoading,
onSearchChange: setSearchQuery,
operators: supportedOperators,
defaultOperator: 'is',
className: 'w-80',
popoverContentClassName: 'w-80',
hideOperatorSelect: true
},
{
Expand All @@ -470,7 +480,8 @@ function StatsFilter({filters, utmTrackingEnabled = false, onChange, ...props}:
defaultOperator: 'is',
hideOperatorSelect: true,
options: sourceOptions,
searchable: true
searchable: true,
selectedOptionsClassName: 'hidden'
}
]
},
Expand All @@ -483,12 +494,13 @@ function StatsFilter({filters, utmTrackingEnabled = false, onChange, ...props}:

return (
<Filters
addButtonIcon={filters.length ? <LucideIcon.Plus /> : <LucideIcon.ListFilter />}
addButtonText={filters.length ? 'Add filter' : 'Filter'}
className='mb-6 mt-0.5 [&>button]:order-last'
addButtonIcon={<LucideIcon.FunnelPlus />}
addButtonText={filters.length ? '' : ''}
className='mb-6 [&>button]:order-last'
fields={groupedFields}
filters={filters}
showSearchInput={false}
size='sm'
onChange={handleFilterChange}
{...props}
/>
Expand Down
2 changes: 1 addition & 1 deletion apps/stats/src/views/Stats/layout/stats-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ const StatsHeader:React.FC<StatsHeaderProps> = ({
)}
</div>
</header>
<Navbar className='sticky top-0 z-40 flex-col items-start gap-y-5 border-none bg-white/70 py-8 backdrop-blur-md lg:flex-row lg:items-center dark:bg-black'>
<Navbar className='sticky top-0 z-40 flex-col items-start gap-y-5 border-none bg-white/70 pb-6 pt-9 backdrop-blur-md lg:flex-row lg:items-center dark:bg-black'>
<PageMenu defaultValue={normalizedPath} responsive>
<PageMenuItem value="/analytics/" onClick={() => {
navigate('/analytics/');
Expand Down
Loading