mirror of
https://github.com/umami-software/umami.git
synced 2026-02-05 05:07:15 +01:00
New settings layouts. Segment management screen.
This commit is contained in:
parent
2dbcf63eeb
commit
eb7b6978d3
70 changed files with 762 additions and 499 deletions
26
src/components/input/ActionButton.tsx
Normal file
26
src/components/input/ActionButton.tsx
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { ReactNode } from 'react';
|
||||
import { Button, Icon, Modal, DialogTrigger, TooltipTrigger, Tooltip } from '@umami/react-zen';
|
||||
|
||||
export function ActionButton({
|
||||
onClick,
|
||||
icon,
|
||||
tooltip,
|
||||
children,
|
||||
}: {
|
||||
onSave?: () => void;
|
||||
icon?: ReactNode;
|
||||
tooltip?: string;
|
||||
children?: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<DialogTrigger>
|
||||
<TooltipTrigger delay={0}>
|
||||
<Button variant="quiet" onPress={onClick}>
|
||||
<Icon>{icon}</Icon>
|
||||
</Button>
|
||||
<Tooltip>{tooltip}</Tooltip>
|
||||
</TooltipTrigger>
|
||||
<Modal>{children}</Modal>
|
||||
</DialogTrigger>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,4 +1,14 @@
|
|||
import { Button, Icon, Text, Row, TooltipTrigger, Tooltip } from '@umami/react-zen';
|
||||
import {
|
||||
Button,
|
||||
Icon,
|
||||
Text,
|
||||
Row,
|
||||
TooltipTrigger,
|
||||
Tooltip,
|
||||
Modal,
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
} from '@umami/react-zen';
|
||||
import {
|
||||
useNavigation,
|
||||
useMessages,
|
||||
|
|
@ -6,8 +16,9 @@ import {
|
|||
useFilters,
|
||||
useWebsiteSegmentQuery,
|
||||
} from '@/components/hooks';
|
||||
import { Close } from '@/components/icons';
|
||||
import { Close, Bookmark } from '@/components/icons';
|
||||
import { isSearchOperator } from '@/lib/params';
|
||||
import { SegmentEditForm } from '@/app/(main)/websites/[websiteId]/segments/SegmentEditForm';
|
||||
|
||||
export function FilterBar({ websiteId }: { websiteId: string }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
|
@ -65,16 +76,37 @@ export function FilterBar({ websiteId }: { websiteId: string }) {
|
|||
);
|
||||
})}
|
||||
</Row>
|
||||
<TooltipTrigger delay={0}>
|
||||
<Button variant="zero" onPress={handleResetFilter} style={{ alignSelf: 'flex-start' }}>
|
||||
<Icon>
|
||||
<Close />
|
||||
</Icon>
|
||||
</Button>
|
||||
<Tooltip>
|
||||
<Text>{formatMessage(labels.clearAll)}</Text>
|
||||
</Tooltip>
|
||||
</TooltipTrigger>
|
||||
<Row alignItems="center">
|
||||
<DialogTrigger>
|
||||
<TooltipTrigger delay={0}>
|
||||
<Button variant="zero">
|
||||
<Icon>
|
||||
<Bookmark />
|
||||
</Icon>
|
||||
</Button>
|
||||
<Tooltip>
|
||||
<Text>{formatMessage(labels.saveSegment)}</Text>
|
||||
</Tooltip>
|
||||
</TooltipTrigger>
|
||||
<Modal>
|
||||
<Dialog title={formatMessage(labels.segment)} style={{ width: 800 }}>
|
||||
{({ close }) => {
|
||||
return <SegmentEditForm websiteId={websiteId} onClose={close} />;
|
||||
}}
|
||||
</Dialog>
|
||||
</Modal>
|
||||
</DialogTrigger>
|
||||
<TooltipTrigger delay={0}>
|
||||
<Button variant="zero" onPress={handleResetFilter}>
|
||||
<Icon>
|
||||
<Close />
|
||||
</Icon>
|
||||
</Button>
|
||||
<Tooltip>
|
||||
<Text>{formatMessage(labels.clearAll)}</Text>
|
||||
</Tooltip>
|
||||
</TooltipTrigger>
|
||||
</Row>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
33
src/components/input/FilterButtons.tsx
Normal file
33
src/components/input/FilterButtons.tsx
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { useState } from 'react';
|
||||
import { ToggleGroup, ToggleGroupItem, Box } from '@umami/react-zen';
|
||||
|
||||
export interface FilterButtonsProps {
|
||||
items: { id: string; label: string }[];
|
||||
value: string;
|
||||
onChange?: (value: string) => void;
|
||||
}
|
||||
|
||||
export function FilterButtons({ items, value, onChange }: FilterButtonsProps) {
|
||||
const [selected, setSelected] = useState(value);
|
||||
|
||||
const handleChange = (value: string) => {
|
||||
setSelected(value);
|
||||
onChange?.(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<ToggleGroup
|
||||
value={[selected]}
|
||||
onChange={e => handleChange(e[0])}
|
||||
disallowEmptySelection={true}
|
||||
>
|
||||
{items.map(({ id, label }) => (
|
||||
<ToggleGroupItem key={id} id={id}>
|
||||
{label}
|
||||
</ToggleGroupItem>
|
||||
))}
|
||||
</ToggleGroup>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
79
src/components/input/FilterEditForm.tsx
Normal file
79
src/components/input/FilterEditForm.tsx
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
import { useState } from 'react';
|
||||
import { Column, Tabs, TabList, Tab, TabPanel, Row, Button } from '@umami/react-zen';
|
||||
import { useDateRange, useMessages } from '@/components/hooks';
|
||||
import { FieldFilters } from '@/components/input/FieldFilters';
|
||||
import { SegmentFilters } from '@/components/input/SegmentFilters';
|
||||
|
||||
export interface FilterEditFormProps {
|
||||
websiteId?: string;
|
||||
filters: any[];
|
||||
segmentId?: string;
|
||||
onChange?: (params: { filters: any[]; segment: any }) => void;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export function FilterEditForm({
|
||||
websiteId,
|
||||
filters = [],
|
||||
segmentId,
|
||||
onChange,
|
||||
onClose,
|
||||
}: FilterEditFormProps) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const [currentFilters, setCurrentFilters] = useState(filters);
|
||||
const [currentSegment, setCurrentSegment] = useState(segmentId);
|
||||
|
||||
const {
|
||||
dateRange: { startDate, endDate },
|
||||
} = useDateRange(websiteId);
|
||||
|
||||
const handleReset = () => {
|
||||
setCurrentFilters([]);
|
||||
setCurrentSegment(null);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
onChange?.({ filters: currentFilters.filter(f => f.value), segment: currentSegment });
|
||||
onClose?.();
|
||||
};
|
||||
|
||||
const handleSegmentChange = (id: string) => {
|
||||
setCurrentSegment(id);
|
||||
};
|
||||
|
||||
return (
|
||||
<Column>
|
||||
<Tabs>
|
||||
<TabList>
|
||||
<Tab id="fields">{formatMessage(labels.fields)}</Tab>
|
||||
<Tab id="segments">{formatMessage(labels.segments)}</Tab>
|
||||
</TabList>
|
||||
<TabPanel id="fields">
|
||||
<FieldFilters
|
||||
websiteId={websiteId}
|
||||
filters={currentFilters}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
onSave={setCurrentFilters}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel id="segments" style={{ height: 400 }}>
|
||||
<SegmentFilters
|
||||
websiteId={websiteId}
|
||||
segmentId={currentSegment}
|
||||
onSave={handleSegmentChange}
|
||||
/>
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
<Row alignItems="center" justifyContent="space-between" gridColumn="span 2" marginTop="6" gap>
|
||||
<Button onPress={handleReset}>{formatMessage(labels.reset)}</Button>
|
||||
<Row alignItems="center" justifyContent="flex-end" gridColumn="span 2" gap>
|
||||
<Button onPress={onClose}>{formatMessage(labels.cancel)}</Button>
|
||||
<Button variant="primary" onPress={handleSave}>
|
||||
{formatMessage(labels.apply)}
|
||||
</Button>
|
||||
</Row>
|
||||
</Row>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ import {
|
|||
MenuSeparator,
|
||||
MenuSection,
|
||||
Text,
|
||||
Row,
|
||||
} from '@umami/react-zen';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useMessages, useLoginQuery } from '@/components/hooks';
|
||||
|
|
@ -36,25 +37,31 @@ export function ProfileButton() {
|
|||
<MenuSection title={user.username}>
|
||||
<MenuSeparator />
|
||||
<MenuItem id="settings">
|
||||
<Icon>
|
||||
<Settings />
|
||||
</Icon>
|
||||
<Text>{formatMessage(labels.settings)}</Text>
|
||||
<Row alignItems="center" gap>
|
||||
<Icon>
|
||||
<Settings />
|
||||
</Icon>
|
||||
<Text>{formatMessage(labels.settings)}</Text>
|
||||
</Row>
|
||||
</MenuItem>
|
||||
{user.isAdmin && (
|
||||
<MenuItem id="admin">
|
||||
<Icon>
|
||||
<LockKeyhole />
|
||||
</Icon>
|
||||
<Text>{formatMessage(labels.admin)}</Text>
|
||||
<Row alignItems="center" gap>
|
||||
<Icon>
|
||||
<LockKeyhole />
|
||||
</Icon>
|
||||
<Text>{formatMessage(labels.admin)}</Text>
|
||||
</Row>
|
||||
</MenuItem>
|
||||
)}
|
||||
{!cloudMode && (
|
||||
<MenuItem data-test="item-logout" id="logout">
|
||||
<Icon>
|
||||
<LogOut />
|
||||
</Icon>
|
||||
<Text>{formatMessage(labels.logout)}</Text>
|
||||
<Row alignItems="center" gap>
|
||||
<Icon>
|
||||
<LogOut />
|
||||
</Icon>
|
||||
<Text>{formatMessage(labels.logout)}</Text>
|
||||
</Row>
|
||||
</MenuItem>
|
||||
)}
|
||||
</MenuSection>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { List, Column, ListItem } from '@umami/react-zen';
|
||||
import { List, ListItem } from '@umami/react-zen';
|
||||
import { useWebsiteSegmentsQuery } from '@/components/hooks';
|
||||
import { LoadingPanel } from '@/components/common/LoadingPanel';
|
||||
|
||||
|
|
@ -9,25 +9,23 @@ export interface SegmentFiltersProps {
|
|||
}
|
||||
|
||||
export function SegmentFilters({ websiteId, segmentId, onSave }: SegmentFiltersProps) {
|
||||
const { data, isLoading } = useWebsiteSegmentsQuery(websiteId, { type: 'segment' });
|
||||
const { data, isLoading, isFetching } = useWebsiteSegmentsQuery(websiteId, { type: 'segment' });
|
||||
|
||||
const handleChange = (id: string) => {
|
||||
onSave?.(id);
|
||||
};
|
||||
|
||||
return (
|
||||
<Column height="400px" gap>
|
||||
<LoadingPanel data={data} isLoading={isLoading} overflowY="auto">
|
||||
<List selectionMode="single" value={[segmentId]} onChange={id => handleChange(id[0])}>
|
||||
{data?.data?.map(item => {
|
||||
return (
|
||||
<ListItem key={item.id} id={item.id}>
|
||||
{item.name}
|
||||
</ListItem>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
</LoadingPanel>
|
||||
</Column>
|
||||
<LoadingPanel data={data} isLoading={isLoading} isFetching={isFetching} overflowY="auto">
|
||||
<List selectionMode="single" value={[segmentId]} onChange={id => handleChange(id[0])}>
|
||||
{data?.data?.map(item => {
|
||||
return (
|
||||
<ListItem key={item.id} id={item.id}>
|
||||
{item.name}
|
||||
</ListItem>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
</LoadingPanel>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
26
src/components/input/SegmentSaveButton.tsx
Normal file
26
src/components/input/SegmentSaveButton.tsx
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { Button, DialogTrigger, Modal, Text, Icon, Dialog } from '@umami/react-zen';
|
||||
import { useMessages } from '@/components/hooks';
|
||||
import { Plus } from '@/components/icons';
|
||||
import { SegmentEditForm } from '@/app/(main)/websites/[websiteId]/segments/SegmentEditForm';
|
||||
|
||||
export function SegmentSaveButton({ websiteId }: { websiteId: string }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
return (
|
||||
<DialogTrigger>
|
||||
<Button variant="primary">
|
||||
<Icon>
|
||||
<Plus />
|
||||
</Icon>
|
||||
<Text>{formatMessage(labels.segment)}</Text>
|
||||
</Button>
|
||||
<Modal>
|
||||
<Dialog title={formatMessage(labels.segment)} style={{ width: 800 }}>
|
||||
{({ close }) => {
|
||||
return <SegmentEditForm websiteId={websiteId} onClose={close} />;
|
||||
}}
|
||||
</Dialog>
|
||||
</Modal>
|
||||
</DialogTrigger>
|
||||
);
|
||||
}
|
||||
|
|
@ -15,7 +15,7 @@ import {
|
|||
Pressable,
|
||||
} from '@umami/react-zen';
|
||||
import { useLoginQuery, useMessages, useUserTeamsQuery, useNavigation } from '@/components/hooks';
|
||||
import { Chevron, User, Users } from '@/components/icons';
|
||||
import { Chevron, User, Users, LogOut } from '@/components/icons';
|
||||
|
||||
export function TeamsButton({ showText = true }: { showText?: boolean }) {
|
||||
const { user } = useLoginQuery();
|
||||
|
|
@ -81,6 +81,17 @@ export function TeamsButton({ showText = true }: { showText?: boolean }) {
|
|||
</MenuItem>
|
||||
))}
|
||||
</MenuSection>
|
||||
<MenuSeparator />
|
||||
<MenuSection>
|
||||
<MenuItem id="logout">
|
||||
<Row alignItems="center" gap>
|
||||
<Icon size="sm">
|
||||
<LogOut />
|
||||
</Icon>
|
||||
<Text wrap="nowrap">{formatMessage(labels.logout)}</Text>
|
||||
</Row>
|
||||
</MenuItem>
|
||||
</MenuSection>
|
||||
</Menu>
|
||||
</Box>
|
||||
</Popover>
|
||||
|
|
|
|||
|
|
@ -33,13 +33,13 @@ export function WebsiteSelect({
|
|||
items={data?.['data'] || []}
|
||||
value={websiteId}
|
||||
isLoading={isLoading}
|
||||
buttonProps={buttonProps}
|
||||
buttonProps={{ ...buttonProps }}
|
||||
allowSearch={true}
|
||||
searchValue={search}
|
||||
onSearch={handleSearch}
|
||||
onChange={handleSelect}
|
||||
renderValue={() => (
|
||||
<Text truncate style={{ maxWidth: 160 }}>
|
||||
<Text truncate weight="bold" style={{ maxWidth: 160, lineHeight: 1 }}>
|
||||
{website?.name}
|
||||
</Text>
|
||||
)}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue