mirror of
https://github.com/supabase/supabase.git
synced 2026-10-12 04:45:08 +03:00
filter styles and variant
This commit is contained in:
1 parent
5dcc1e6999
commit
6137785fc0
7 files changed
+90
-54
No files matched your search
@@ -64,8 +64,8 @@ export type HeaderProps = {
|
||||
|
||||
const DefaultHeader = () => {
|
||||
return (
|
||||
<div className="flex items-center gap-2 flex-1">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex-1 min-w-0 flex items-center gap-2 flex-1 h-full">
|
||||
<div className="flex-1 min-w-0 h-full">
|
||||
<FilterPopover />
|
||||
</div>
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
@@ -202,7 +202,7 @@ export const Header = ({ customHeader, isRefetching, tableQueriesEnabled = true
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex h-10 items-center justify-between bg-dash-sidebar dark:bg-surface-100 px-1.5 py-1.5 gap-2 overflow-x-auto ">
|
||||
<div className="flex h-10 items-stretch justify-between bg-dash-sidebar dark:bg-surface-100 px-1.5 py-0 gap-2 overflow-x-auto ">
|
||||
{customHeader ? (
|
||||
customHeader
|
||||
) : snap.selectedRows.size > 0 ? (
|
||||
|
||||
@@ -139,7 +139,10 @@ export const FilterPopover = ({ portal = true }: FilterPopoverProps) => {
|
||||
// Create filter properties from table columns
|
||||
const filterProperties: FilterProperty[] = useMemo(() => {
|
||||
const columns = snap.table?.columns ?? []
|
||||
const defaultOperators = FilterOperatorOptions.map((op) => ({ value: op.value, label: op.label }))
|
||||
const defaultOperators = FilterOperatorOptions.map((op) => ({
|
||||
value: op.value,
|
||||
label: op.label,
|
||||
}))
|
||||
const stringOperators = [
|
||||
{ value: '~~*', label: 'ilike operator' },
|
||||
...defaultOperators.filter((op) => op.value !== '~~*'),
|
||||
@@ -267,6 +270,7 @@ export const FilterPopover = ({ portal = true }: FilterPopoverProps) => {
|
||||
onFreeformTextChange={setFreeformText}
|
||||
actions={actions}
|
||||
isLoading={isGenerating}
|
||||
variant="pill"
|
||||
className="bg-transparent border-0"
|
||||
/>
|
||||
)
|
||||
|
||||
+2
-2
@@ -274,7 +274,7 @@ const FunctionsList = () => {
|
||||
disabled={!canCreateFunctions}
|
||||
onClick={() => createFunction()}
|
||||
className="flex-grow"
|
||||
size="small"
|
||||
size="tiny"
|
||||
tooltip={{
|
||||
content: {
|
||||
side: 'bottom',
|
||||
@@ -288,7 +288,7 @@ const FunctionsList = () => {
|
||||
</ButtonTooltip>
|
||||
<ButtonTooltip
|
||||
type="default"
|
||||
size="small"
|
||||
size="tiny"
|
||||
disabled={!canCreateFunctions}
|
||||
className="px-1 pointer-events-auto"
|
||||
icon={<AiIconAnimation size={16} />}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { motion } from 'framer-motion'
|
||||
import { Search } from 'lucide-react'
|
||||
import { cn } from 'ui'
|
||||
import { FilterBarRoot, useFilterBar } from './FilterBarContext'
|
||||
import { FilterBarRoot, useFilterBar, type FilterBarVariant } from './FilterBarContext'
|
||||
import { FilterGroup } from './FilterGroup'
|
||||
import { FilterBarAction, FilterGroup as FilterGroupType, FilterProperty } from './types'
|
||||
|
||||
@@ -17,22 +17,30 @@ export type FilterBarProps = {
|
||||
isLoading?: boolean
|
||||
className?: string
|
||||
supportsOperators?: boolean
|
||||
variant?: FilterBarVariant
|
||||
}
|
||||
|
||||
function FilterBarContent({ className }: { className?: string }) {
|
||||
const { filters, error, optionsError, isLoading } = useFilterBar()
|
||||
const { filters, error, optionsError, isLoading, variant } = useFilterBar()
|
||||
|
||||
return (
|
||||
<div className="w-full space-y-2 relative">
|
||||
<div
|
||||
className={cn(
|
||||
'relative flex items-center gap-2 w-full border rounded-md h-[34px] bg-foreground/[.026] cursor-text p-0 px-2 overflow-auto',
|
||||
'relative flex items-stretch gap-0 w-full border rounded-md h-full bg-foreground/[.026] cursor-text p-0 pr-2 overflow-auto',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<Search className="text-foreground-muted w-4 h-4 sticky left-0 shrink-0" />
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center shrink-0 px-2 bg-surface-200',
|
||||
variant === 'pill' ? 'bg-transparent border-r-0' : 'border-r'
|
||||
)}
|
||||
>
|
||||
<Search className="text-foreground-muted w-4 h-4 sticky" />
|
||||
</div>
|
||||
<motion.div
|
||||
className="flex-1 flex flex-wrap items-center gap-1 h-full"
|
||||
className="flex-1 flex flex-wrap items-stretch gap-0"
|
||||
animate={{ opacity: isLoading ? 0.5 : 1 }}
|
||||
transition={{
|
||||
duration: 1,
|
||||
@@ -88,6 +96,7 @@ export function FilterBar({
|
||||
isLoading,
|
||||
className,
|
||||
supportsOperators = false,
|
||||
variant = 'default',
|
||||
}: FilterBarProps) {
|
||||
return (
|
||||
<FilterBarRoot
|
||||
@@ -99,6 +108,7 @@ export function FilterBar({
|
||||
actions={actions}
|
||||
isLoading={isLoading}
|
||||
supportsOperators={supportsOperators}
|
||||
variant={variant}
|
||||
>
|
||||
<FilterBarContent className={className} />
|
||||
</FilterBarRoot>
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
'use client'
|
||||
|
||||
import React, { createContext, useContext, useRef, useCallback } from 'react'
|
||||
import { FilterBarAction, FilterProperty, FilterGroup } from './types'
|
||||
import React, { createContext, useCallback, useContext, useRef } from 'react'
|
||||
import { ActiveInput, useFilterBarState, useOptionsCache } from './hooks'
|
||||
import { useKeyboardNavigation } from './useKeyboardNavigation'
|
||||
import { useCommandHandling } from './useCommandHandling'
|
||||
import { MenuItem } from './menuItems'
|
||||
import { FilterBarAction, FilterGroup, FilterProperty } from './types'
|
||||
import { useCommandHandling } from './useCommandHandling'
|
||||
import { useKeyboardNavigation } from './useKeyboardNavigation'
|
||||
import {
|
||||
findConditionByPath,
|
||||
isAsyncOptionsFunction,
|
||||
removeFromGroup,
|
||||
updateNestedValue,
|
||||
updateNestedOperator,
|
||||
updateNestedLogicalOperator,
|
||||
updateNestedOperator,
|
||||
updateNestedValue,
|
||||
} from './utils'
|
||||
|
||||
export type FilterBarContextValue = {
|
||||
@@ -49,6 +49,7 @@ export type FilterBarContextValue = {
|
||||
|
||||
// Config
|
||||
supportsOperators: boolean
|
||||
variant: FilterBarVariant
|
||||
actions?: FilterBarAction[]
|
||||
|
||||
// Refs
|
||||
@@ -75,8 +76,11 @@ export type FilterBarRootProps = {
|
||||
actions?: FilterBarAction[]
|
||||
isLoading?: boolean
|
||||
supportsOperators?: boolean
|
||||
variant?: FilterBarVariant
|
||||
}
|
||||
|
||||
export type FilterBarVariant = 'default' | 'pill'
|
||||
|
||||
export function FilterBarRoot({
|
||||
children,
|
||||
filterProperties,
|
||||
@@ -87,6 +91,7 @@ export function FilterBarRoot({
|
||||
actions,
|
||||
isLoading: externalLoading,
|
||||
supportsOperators = false,
|
||||
variant = 'default',
|
||||
}: FilterBarRootProps) {
|
||||
const rootRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
@@ -100,12 +105,8 @@ export function FilterBarRoot({
|
||||
setIsCommandMenuVisible,
|
||||
} = useFilterBarState()
|
||||
|
||||
const {
|
||||
loadingOptions,
|
||||
propertyOptionsCache,
|
||||
loadPropertyOptions,
|
||||
optionsError,
|
||||
} = useOptionsCache()
|
||||
const { loadingOptions, propertyOptionsCache, loadPropertyOptions, optionsError } =
|
||||
useOptionsCache()
|
||||
|
||||
const handleInputChange = useCallback(
|
||||
(path: number[], value: string) => {
|
||||
@@ -178,7 +179,14 @@ export function FilterBarRoot({
|
||||
}
|
||||
}
|
||||
},
|
||||
[filters, filterProperties, loadPropertyOptions, setActiveInput, setIsCommandMenuVisible, hideTimeoutRef]
|
||||
[
|
||||
filters,
|
||||
filterProperties,
|
||||
loadPropertyOptions,
|
||||
setActiveInput,
|
||||
setIsCommandMenuVisible,
|
||||
hideTimeoutRef,
|
||||
]
|
||||
)
|
||||
|
||||
const handleOperatorFocus = useCallback(
|
||||
@@ -284,6 +292,7 @@ export function FilterBarRoot({
|
||||
|
||||
// Config
|
||||
supportsOperators,
|
||||
variant,
|
||||
actions,
|
||||
|
||||
// Refs
|
||||
@@ -292,7 +301,7 @@ export function FilterBarRoot({
|
||||
|
||||
return (
|
||||
<FilterBarContext.Provider value={contextValue}>
|
||||
<div ref={rootRef} data-filterbar-root>
|
||||
<div ref={rootRef} data-filterbar-root className="h-full min-h-[26px] flex items-stretch">
|
||||
{children}
|
||||
</div>
|
||||
</FilterBarContext.Provider>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { X } from 'lucide-react'
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import {
|
||||
Button,
|
||||
cn,
|
||||
Input_Shadcn_,
|
||||
Popover_Shadcn_,
|
||||
PopoverAnchor_Shadcn_,
|
||||
@@ -44,6 +45,7 @@ export function FilterCondition({
|
||||
handleRemoveCondition,
|
||||
handleSelectMenuItem,
|
||||
setActiveInput,
|
||||
variant,
|
||||
} = useFilterBar()
|
||||
|
||||
const operatorRef = useRef<HTMLInputElement>(null)
|
||||
@@ -190,17 +192,20 @@ export function FilterCondition({
|
||||
return (
|
||||
<div
|
||||
ref={wrapperRef}
|
||||
className="flex items-center rounded px-2 h-6 bg-muted border group shrink-0"
|
||||
className={cn(
|
||||
'flex items-stretch px-0 bg-muted group shrink-0',
|
||||
variant === 'pill' ? 'rounded border' : 'border-r'
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className="text-xs mr-1 cursor-pointer shrink-0 whitespace-nowrap text-foreground-light"
|
||||
className="text-xs pl-2 pr-1 cursor-pointer shrink-0 whitespace-nowrap text-foreground-light h-full flex items-center"
|
||||
onClick={() => handleLabelClick(path)}
|
||||
>
|
||||
{property.label}
|
||||
</span>
|
||||
<Popover_Shadcn_ open={isOperatorActive && !isLoading && operatorItems.length > 0}>
|
||||
<PopoverAnchor_Shadcn_ asChild>
|
||||
<div className="relative inline-block mr-1">
|
||||
<div className="relative inline-block">
|
||||
<Input_Shadcn_
|
||||
ref={operatorRef}
|
||||
type="text"
|
||||
@@ -209,11 +214,11 @@ export function FilterCondition({
|
||||
onFocus={() => handleOperatorFocus(path)}
|
||||
onBlur={handleOperatorBlur}
|
||||
onKeyDown={handleOperatorKeyDown}
|
||||
className="border-none bg-transparent p-0 text-xs focus:outline-none focus:ring-0 focus:shadow-none focus-visible:ring-0 focus-visible:ring-offset-0 h-6 text-foreground-light w-full absolute left-0 top-0"
|
||||
className="h-full border-none bg-transparent py-0 px-1 text-center text-xs focus:outline-none focus:ring-0 focus:shadow-none focus-visible:ring-0 focus-visible:ring-offset-0 text-foreground-light w-full absolute left-0 top-0"
|
||||
disabled={isLoading}
|
||||
aria-label={`Operator for ${property.label}`}
|
||||
/>
|
||||
<span className="invisible whitespace-pre text-xs block h-6">
|
||||
<span className="invisible whitespace-pre text-xs block px-1 shrink-0 px-1">
|
||||
{condition.operator || ' '}
|
||||
</span>
|
||||
</div>
|
||||
@@ -242,7 +247,7 @@ export function FilterCondition({
|
||||
</Popover_Shadcn_>
|
||||
<Popover_Shadcn_ open={isActive && !isLoading && (showValueCustom || valueItems.length > 0)}>
|
||||
<PopoverAnchor_Shadcn_ asChild>
|
||||
<div className="relative inline-block mr-1 max-w-[150px]">
|
||||
<div className="relative inline-block max-w-[150px]">
|
||||
<Input_Shadcn_
|
||||
ref={valueRef}
|
||||
type="text"
|
||||
@@ -251,11 +256,11 @@ export function FilterCondition({
|
||||
onFocus={() => handleInputFocus(path)}
|
||||
onBlur={handleValueBlur}
|
||||
onKeyDown={handleValueKeyDown}
|
||||
className="border-none bg-transparent p-0 text-xs focus:outline-none focus:ring-0 focus:shadow-none focus-visible:ring-0 focus-visible:ring-offset-0 h-6 w-full absolute left-0 top-0"
|
||||
className="h-full border-none bg-transparent py-0 px-1 text-xs focus:outline-none focus:ring-0 focus:shadow-none focus-visible:ring-0 focus-visible:ring-offset-0 w-full absolute left-0 top-0"
|
||||
disabled={isLoading}
|
||||
aria-label={`Value for ${property.label}`}
|
||||
/>
|
||||
<span className="invisible whitespace-pre text-xs block h-6">
|
||||
<span className="invisible whitespace-pre text-xs block px-1">
|
||||
{(condition.value ?? '').toString() || ' '}
|
||||
</span>
|
||||
</div>
|
||||
@@ -306,13 +311,13 @@ export function FilterCondition({
|
||||
size="tiny"
|
||||
icon={
|
||||
<X
|
||||
strokeWidth={1.5}
|
||||
size={12}
|
||||
className="group-hover:text-foreground text-foreground-light"
|
||||
strokeWidth={1}
|
||||
size={10}
|
||||
className="group-hover:text-foreground text-foreground-lighter"
|
||||
/>
|
||||
}
|
||||
onClick={onRemove}
|
||||
className="group hover:text-foreground !hover:bg-surface-600 p-0"
|
||||
className="group hover:text-foreground !hover:bg-surface-600 rounded-none px-1 h-auto py-0 px-1"
|
||||
aria-label={`Remove ${property.label} filter`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
'use client'
|
||||
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Input_Shadcn_, Popover_Shadcn_, PopoverAnchor_Shadcn_, PopoverContent_Shadcn_ } from 'ui'
|
||||
import {
|
||||
cn,
|
||||
Input_Shadcn_,
|
||||
Popover_Shadcn_,
|
||||
PopoverAnchor_Shadcn_,
|
||||
PopoverContent_Shadcn_,
|
||||
} from 'ui'
|
||||
import { DefaultCommandList } from './DefaultCommandList'
|
||||
import { useFilterBar } from './FilterBarContext'
|
||||
import { FilterCondition } from './FilterCondition'
|
||||
@@ -23,6 +29,7 @@ export function FilterGroup({ group, path }: FilterGroupProps) {
|
||||
isLoading,
|
||||
supportsOperators,
|
||||
actions,
|
||||
variant,
|
||||
handleInputBlur,
|
||||
handleGroupFreeformFocus,
|
||||
handleGroupFreeformChange,
|
||||
@@ -83,14 +90,7 @@ export function FilterGroup({ group, path }: FilterGroupProps) {
|
||||
actions,
|
||||
supportsOperators,
|
||||
}),
|
||||
[
|
||||
filterProperties,
|
||||
isActive,
|
||||
freeformText,
|
||||
localFreeformValue,
|
||||
actions,
|
||||
supportsOperators,
|
||||
]
|
||||
[filterProperties, isActive, freeformText, localFreeformValue, actions, supportsOperators]
|
||||
)
|
||||
|
||||
// Only the root group should expand to fill available space
|
||||
@@ -115,13 +115,19 @@ export function FilterGroup({ group, path }: FilterGroupProps) {
|
||||
return (
|
||||
<div
|
||||
ref={wrapperRef}
|
||||
className={`flex items-center gap-1 rounded shrink-0 ${
|
||||
className={`flex items-stretch gap-0 shrink-0 ${
|
||||
path.length > 0
|
||||
? "before:content-['('] before:text-foreground-muted after:content-[')'] after:text-foreground-muted"
|
||||
: ''
|
||||
} ${isRootGroup ? 'flex-1 min-w-0' : ''}`}
|
||||
} ${isRootGroup ? 'flex-1 min-w-0' : ''} ${variant === 'pill' ? 'py-2' : ''}`}
|
||||
>
|
||||
<div className={`flex items-center gap-1 ${isRootGroup ? 'flex-1 min-w-0' : ''}`}>
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-stretch',
|
||||
isRootGroup ? 'flex-1 min-w-0' : '',
|
||||
variant === 'pill' ? 'gap-1' : 'gap-0'
|
||||
)}
|
||||
>
|
||||
{group.conditions.map((condition, index) => {
|
||||
const currentPath = [...path, index]
|
||||
|
||||
@@ -165,8 +171,10 @@ export function FilterGroup({ group, path }: FilterGroupProps) {
|
||||
onFocus={() => handleGroupFreeformFocus(path)}
|
||||
onBlur={handleFreeformBlur}
|
||||
onKeyDown={handleFreeformKeyDown}
|
||||
className="border-none bg-transparent p-0 text-xs focus:outline-none focus:ring-0 focus:shadow-none focus-visible:ring-0 focus-visible:ring-offset-0 h-6 w-full flex-1 min-w-0"
|
||||
placeholder={group.conditions.length === 0 ? 'Search or filter...' : '+'}
|
||||
className="border-none bg-transparent text-xs focus:outline-none focus:ring-0 focus:shadow-none focus-visible:ring-0 focus-visible:ring-offset-0 w-full flex-1 h-auto min-w-0 px-2 py-0"
|
||||
placeholder={
|
||||
group.conditions.length === 0 ? 'Search or filter...' : '+ Search or filter...'
|
||||
}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
) : (
|
||||
@@ -179,11 +187,11 @@ export function FilterGroup({ group, path }: FilterGroupProps) {
|
||||
onFocus={() => handleGroupFreeformFocus(path)}
|
||||
onBlur={handleFreeformBlur}
|
||||
onKeyDown={handleFreeformKeyDown}
|
||||
className="border-none bg-transparent p-0 text-xs focus:outline-none focus:ring-0 focus:shadow-none focus-visible:ring-0 focus-visible:ring-offset-0 h-6 w-full absolute left-0 top-0"
|
||||
placeholder="+"
|
||||
className="h-full border-none bg-transparent py-0 text-xs focus:outline-none focus:ring-0 focus:shadow-none focus-visible:ring-0 focus-visible:ring-offset-0 w-full absolute left-0 top-0 px-2"
|
||||
placeholder="+ Add filter"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<span className="invisible whitespace-pre text-xs block h-6">
|
||||
<span className="invisible whitespace-pre text-xs block">
|
||||
{(isActive ? freeformText : localFreeformValue) || '+'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user