mirror of
https://github.com/umami-software/umami.git
synced 2026-02-15 10:05:36 +01:00
Add link and pixel selectors to top nav
This commit is contained in:
parent
dd2d6bca45
commit
a1f3ad89f6
6 changed files with 255 additions and 17 deletions
|
|
@ -2,16 +2,26 @@
|
|||
import { Icon, Row } from '@umami/react-zen';
|
||||
import { useNavigation } from '@/components/hooks';
|
||||
import { Slash } from '@/components/icons';
|
||||
import { LinkSelect } from '@/components/input/LinkSelect';
|
||||
import { PixelSelect } from '@/components/input/PixelSelect';
|
||||
import { TeamsButton } from '@/components/input/TeamsButton';
|
||||
import { WebsiteSelect } from '@/components/input/WebsiteSelect';
|
||||
|
||||
export function TopNav() {
|
||||
const { websiteId, teamId, router, renderUrl } = useNavigation();
|
||||
const { websiteId, linkId, pixelId, teamId, router, renderUrl } = useNavigation();
|
||||
|
||||
const handleWebsiteChange = (value: string) => {
|
||||
router.push(renderUrl(`/websites/${value}`));
|
||||
};
|
||||
|
||||
const handleLinkChange = (value: string) => {
|
||||
router.push(renderUrl(`/links/${value}`));
|
||||
};
|
||||
|
||||
const handlePixelChange = (value: string) => {
|
||||
router.push(renderUrl(`/pixels/${value}`));
|
||||
};
|
||||
|
||||
return (
|
||||
<Row
|
||||
position="sticky"
|
||||
|
|
@ -26,20 +36,44 @@ export function TopNav() {
|
|||
>
|
||||
<Row alignItems="center" backgroundColor="surface-raised" borderRadius>
|
||||
<TeamsButton />
|
||||
{websiteId && (
|
||||
{(websiteId || linkId || pixelId) && (
|
||||
<>
|
||||
<Icon size="sm" color="muted" style={{ opacity: 0.7, margin: '0 6px' }}>
|
||||
<Slash />
|
||||
</Icon>
|
||||
<WebsiteSelect
|
||||
websiteId={websiteId}
|
||||
teamId={teamId}
|
||||
onChange={handleWebsiteChange}
|
||||
buttonProps={{
|
||||
variant: 'quiet',
|
||||
style: { minHeight: 40, minWidth: 200, maxWidth: 200 },
|
||||
}}
|
||||
/>
|
||||
{websiteId && (
|
||||
<WebsiteSelect
|
||||
websiteId={websiteId}
|
||||
teamId={teamId}
|
||||
onChange={handleWebsiteChange}
|
||||
buttonProps={{
|
||||
variant: 'quiet',
|
||||
style: { minHeight: 40, minWidth: 200, maxWidth: 200 },
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{linkId && (
|
||||
<LinkSelect
|
||||
linkId={linkId}
|
||||
teamId={teamId}
|
||||
onChange={handleLinkChange}
|
||||
buttonProps={{
|
||||
variant: 'quiet',
|
||||
style: { minHeight: 40, minWidth: 200, maxWidth: 200 },
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{pixelId && (
|
||||
<PixelSelect
|
||||
pixelId={pixelId}
|
||||
teamId={teamId}
|
||||
onChange={handlePixelChange}
|
||||
buttonProps={{
|
||||
variant: 'quiet',
|
||||
style: { minHeight: 40, minWidth: 200, maxWidth: 200 },
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Row>
|
||||
|
|
|
|||
|
|
@ -3,14 +3,21 @@ import { useApi } from '../useApi';
|
|||
import { useModified } from '../useModified';
|
||||
import { usePagedQuery } from '../usePagedQuery';
|
||||
|
||||
export function useLinksQuery({ teamId }: { teamId?: string }, options?: ReactQueryOptions) {
|
||||
export function useLinksQuery(
|
||||
{ teamId }: { teamId?: string },
|
||||
params?: Record<string, any>,
|
||||
options?: ReactQueryOptions,
|
||||
) {
|
||||
const { modified } = useModified('links');
|
||||
const { get } = useApi();
|
||||
|
||||
return usePagedQuery({
|
||||
queryKey: ['links', { teamId, modified }],
|
||||
queryKey: ['links', { teamId, modified, ...params }],
|
||||
queryFn: pageParams => {
|
||||
return get(teamId ? `/teams/${teamId}/links` : '/links', pageParams);
|
||||
return get(teamId ? `/teams/${teamId}/links` : '/links', {
|
||||
...pageParams,
|
||||
...params,
|
||||
});
|
||||
},
|
||||
...options,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,14 +3,21 @@ import { useApi } from '../useApi';
|
|||
import { useModified } from '../useModified';
|
||||
import { usePagedQuery } from '../usePagedQuery';
|
||||
|
||||
export function usePixelsQuery({ teamId }: { teamId?: string }, options?: ReactQueryOptions) {
|
||||
export function usePixelsQuery(
|
||||
{ teamId }: { teamId?: string },
|
||||
params?: Record<string, any>,
|
||||
options?: ReactQueryOptions,
|
||||
) {
|
||||
const { modified } = useModified('pixels');
|
||||
const { get } = useApi();
|
||||
|
||||
return usePagedQuery({
|
||||
queryKey: ['pixels', { teamId, modified }],
|
||||
queryKey: ['pixels', { teamId, modified, ...params }],
|
||||
queryFn: pageParams => {
|
||||
return get(teamId ? `/teams/${teamId}/pixels` : '/pixels', pageParams);
|
||||
return get(teamId ? `/teams/${teamId}/pixels` : '/pixels', {
|
||||
...pageParams,
|
||||
...params,
|
||||
});
|
||||
},
|
||||
...options,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ export function useNavigation() {
|
|||
const searchParams = useSearchParams();
|
||||
const [, teamId] = pathname.match(/\/teams\/([a-f0-9-]+)/) || [];
|
||||
const [, websiteId] = pathname.match(/\/websites\/([a-f0-9-]+)/) || [];
|
||||
const [, linkId] = pathname.match(/\/links\/([a-f0-9-]+)/) || [];
|
||||
const [, pixelId] = pathname.match(/\/pixels\/([a-f0-9-]+)/) || [];
|
||||
const [queryParams, setQueryParams] = useState(Object.fromEntries(searchParams));
|
||||
|
||||
const updateParams = (params?: Record<string, string | number>) => {
|
||||
|
|
@ -36,6 +38,8 @@ export function useNavigation() {
|
|||
query: queryParams,
|
||||
teamId,
|
||||
websiteId,
|
||||
linkId,
|
||||
pixelId,
|
||||
updateParams,
|
||||
replaceParams,
|
||||
renderUrl,
|
||||
|
|
|
|||
93
src/components/input/LinkSelect.tsx
Normal file
93
src/components/input/LinkSelect.tsx
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
import { Icon, ListItem, Row, Select, type SelectProps, Text } from '@umami/react-zen';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Empty } from '@/components/common/Empty';
|
||||
import { useLinkQuery, useLinksQuery, useMessages } from '@/components/hooks';
|
||||
import { Link } from '@/components/icons';
|
||||
|
||||
export function LinkSelect({
|
||||
linkId,
|
||||
teamId,
|
||||
onChange,
|
||||
isCollapsed,
|
||||
buttonProps,
|
||||
listProps,
|
||||
...props
|
||||
}: {
|
||||
linkId?: string;
|
||||
teamId?: string;
|
||||
isCollapsed?: boolean;
|
||||
} & SelectProps) {
|
||||
const { t, messages } = useMessages();
|
||||
const { data: link } = useLinkQuery(linkId);
|
||||
const [name, setName] = useState<string>(link?.name);
|
||||
const [search, setSearch] = useState('');
|
||||
const { data, isLoading } = useLinksQuery({ teamId }, { search, pageSize: 20 });
|
||||
const listItems: { id: string; name: string }[] = data?.data || [];
|
||||
|
||||
useEffect(() => {
|
||||
setName(link?.name);
|
||||
}, [link?.name]);
|
||||
|
||||
const handleOpenChange = () => {
|
||||
setSearch('');
|
||||
};
|
||||
|
||||
const handleChange = (id: string) => {
|
||||
setName(listItems.find(item => item.id === id)?.name);
|
||||
onChange?.(id);
|
||||
};
|
||||
|
||||
const renderValue = () => {
|
||||
if (isCollapsed) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return (
|
||||
<Row alignItems="center" gap>
|
||||
<Icon>
|
||||
<Link />
|
||||
</Icon>
|
||||
<Text truncate>{name}</Text>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Select
|
||||
{...props}
|
||||
value={linkId}
|
||||
isLoading={isLoading}
|
||||
allowSearch={true}
|
||||
searchValue={search}
|
||||
onSearch={setSearch}
|
||||
onChange={handleChange}
|
||||
onOpenChange={handleOpenChange}
|
||||
renderValue={renderValue}
|
||||
buttonProps={{
|
||||
...buttonProps,
|
||||
style: {
|
||||
minHeight: 40,
|
||||
gap: 0,
|
||||
justifyContent: isCollapsed ? 'start' : undefined,
|
||||
...buttonProps?.style,
|
||||
},
|
||||
}}
|
||||
listProps={{
|
||||
...listProps,
|
||||
renderEmptyState:
|
||||
listProps?.renderEmptyState || (() => <Empty message={t(messages.noResultsFound)} />),
|
||||
style: {
|
||||
maxHeight: 'calc(42vh - 65px)',
|
||||
width: 280,
|
||||
...listProps?.style,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{listItems.map(({ id, name }) => (
|
||||
<ListItem key={id} id={id}>
|
||||
{name}
|
||||
</ListItem>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
93
src/components/input/PixelSelect.tsx
Normal file
93
src/components/input/PixelSelect.tsx
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
import { Icon, ListItem, Row, Select, type SelectProps, Text } from '@umami/react-zen';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Empty } from '@/components/common/Empty';
|
||||
import { useMessages, usePixelQuery, usePixelsQuery } from '@/components/hooks';
|
||||
import { Grid2x2 } from '@/components/icons';
|
||||
|
||||
export function PixelSelect({
|
||||
pixelId,
|
||||
teamId,
|
||||
onChange,
|
||||
isCollapsed,
|
||||
buttonProps,
|
||||
listProps,
|
||||
...props
|
||||
}: {
|
||||
pixelId?: string;
|
||||
teamId?: string;
|
||||
isCollapsed?: boolean;
|
||||
} & SelectProps) {
|
||||
const { t, messages } = useMessages();
|
||||
const { data: pixel } = usePixelQuery(pixelId);
|
||||
const [name, setName] = useState<string>(pixel?.name);
|
||||
const [search, setSearch] = useState('');
|
||||
const { data, isLoading } = usePixelsQuery({ teamId }, { search, pageSize: 20 });
|
||||
const listItems: { id: string; name: string }[] = data?.data || [];
|
||||
|
||||
useEffect(() => {
|
||||
setName(pixel?.name);
|
||||
}, [pixel?.name]);
|
||||
|
||||
const handleOpenChange = () => {
|
||||
setSearch('');
|
||||
};
|
||||
|
||||
const handleChange = (id: string) => {
|
||||
setName(listItems.find(item => item.id === id)?.name);
|
||||
onChange?.(id);
|
||||
};
|
||||
|
||||
const renderValue = () => {
|
||||
if (isCollapsed) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return (
|
||||
<Row alignItems="center" gap>
|
||||
<Icon>
|
||||
<Grid2x2 />
|
||||
</Icon>
|
||||
<Text truncate>{name}</Text>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Select
|
||||
{...props}
|
||||
value={pixelId}
|
||||
isLoading={isLoading}
|
||||
allowSearch={true}
|
||||
searchValue={search}
|
||||
onSearch={setSearch}
|
||||
onChange={handleChange}
|
||||
onOpenChange={handleOpenChange}
|
||||
renderValue={renderValue}
|
||||
buttonProps={{
|
||||
...buttonProps,
|
||||
style: {
|
||||
minHeight: 40,
|
||||
gap: 0,
|
||||
justifyContent: isCollapsed ? 'start' : undefined,
|
||||
...buttonProps?.style,
|
||||
},
|
||||
}}
|
||||
listProps={{
|
||||
...listProps,
|
||||
renderEmptyState:
|
||||
listProps?.renderEmptyState || (() => <Empty message={t(messages.noResultsFound)} />),
|
||||
style: {
|
||||
maxHeight: 'calc(42vh - 65px)',
|
||||
width: 280,
|
||||
...listProps?.style,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{listItems.map(({ id, name }) => (
|
||||
<ListItem key={id} id={id}>
|
||||
{name}
|
||||
</ListItem>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue