diff options
| author | Fuwn <[email protected]> | 2026-01-24 13:09:50 +0000 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2026-01-24 13:09:50 +0000 |
| commit | 396acf3bbbe00a192cb0ea0a9ccf91b1d8d2850b (patch) | |
| tree | b9df4ca6a70db45cfffbae6fdd7252e20fb8e93c /src/components/input/FieldFilters.tsx | |
| download | umami-main.tar.xz umami-main.zip | |
Created from https://vercel.com/new
Diffstat (limited to 'src/components/input/FieldFilters.tsx')
| -rw-r--r-- | src/components/input/FieldFilters.tsx | 117 |
1 files changed, 117 insertions, 0 deletions
diff --git a/src/components/input/FieldFilters.tsx b/src/components/input/FieldFilters.tsx new file mode 100644 index 0000000..2174068 --- /dev/null +++ b/src/components/input/FieldFilters.tsx @@ -0,0 +1,117 @@ +import { + Button, + Column, + Grid, + Icon, + List, + ListItem, + Menu, + MenuItem, + MenuTrigger, + Popover, + Row, +} from '@umami/react-zen'; +import { endOfDay, subMonths } from 'date-fns'; +import type { Key } from 'react'; +import { Empty } from '@/components/common/Empty'; +import { FilterRecord } from '@/components/common/FilterRecord'; +import { useFields, useMessages, useMobile } from '@/components/hooks'; +import { Plus } from '@/components/icons'; + +export interface FieldFiltersProps { + websiteId: string; + value?: { name: string; operator: string; value: string }[]; + exclude?: string[]; + onChange?: (data: any) => void; +} + +export function FieldFilters({ websiteId, value, exclude = [], onChange }: FieldFiltersProps) { + const { formatMessage, messages } = useMessages(); + const { fields } = useFields(); + const startDate = subMonths(endOfDay(new Date()), 6); + const endDate = endOfDay(new Date()); + const { isMobile } = useMobile(); + + const updateFilter = (name: string, props: Record<string, any>) => { + onChange(value.map(filter => (filter.name === name ? { ...filter, ...props } : filter))); + }; + + const handleAdd = (name: Key) => { + onChange(value.concat({ name: name.toString(), operator: 'eq', value: '' })); + }; + + const handleChange = (name: string, value: Key) => { + updateFilter(name, { value }); + }; + + const handleSelect = (name: string, operator: Key) => { + updateFilter(name, { operator }); + }; + + const handleRemove = (name: string) => { + onChange(value.filter(filter => filter.name !== name)); + }; + + return ( + <Grid columns={{ xs: '1fr', md: '180px 1fr' }} overflow="hidden" gapY="6"> + <Row display={{ xs: 'flex', md: 'none' }}> + <MenuTrigger> + <Button> + <Icon> + <Plus /> + </Icon> + </Button> + <Popover placement={isMobile ? 'left' : 'bottom start'} shouldFlip> + <Menu + onAction={handleAdd} + style={{ maxHeight: 'calc(100vh - 2rem)', overflowY: 'auto' }} + > + {fields + .filter(({ name }) => !exclude.includes(name)) + .map(field => { + const isDisabled = !!value.find(({ name }) => name === field.name); + return ( + <MenuItem key={field.name} id={field.name} isDisabled={isDisabled}> + {field.label} + </MenuItem> + ); + })} + </Menu> + </Popover> + </MenuTrigger> + </Row> + <Column display={{ xs: 'none', md: 'flex' }} border="right" paddingRight="3" marginRight="6"> + <List onAction={handleAdd}> + {fields + .filter(({ name }) => !exclude.includes(name)) + .map(field => { + const isDisabled = !!value.find(({ name }) => name === field.name); + return ( + <ListItem key={field.name} id={field.name} isDisabled={isDisabled}> + {field.label} + </ListItem> + ); + })} + </List> + </Column> + <Column overflow="auto" gapY="4" style={{ contain: 'layout' }}> + {value.map(filter => { + return ( + <FilterRecord + key={filter.name} + websiteId={websiteId} + type={filter.name} + startDate={startDate} + endDate={endDate} + {...filter} + onSelect={handleSelect} + onRemove={handleRemove} + onChange={handleChange} + /> + ); + })} + {!value.length && <Empty message={formatMessage(messages.nothingSelected)} />} + </Column> + </Grid> + ); +} |