mirror of
https://github.com/umami-software/umami.git
synced 2026-02-15 18:15:35 +01:00
Add board selector to top navigation
This commit is contained in:
parent
a1f3ad89f6
commit
e2b391af28
5 changed files with 128 additions and 6 deletions
|
|
@ -2,13 +2,14 @@
|
||||||
import { Icon, Row } from '@umami/react-zen';
|
import { Icon, Row } from '@umami/react-zen';
|
||||||
import { useNavigation } from '@/components/hooks';
|
import { useNavigation } from '@/components/hooks';
|
||||||
import { Slash } from '@/components/icons';
|
import { Slash } from '@/components/icons';
|
||||||
|
import { BoardSelect } from '@/components/input/BoardSelect';
|
||||||
import { LinkSelect } from '@/components/input/LinkSelect';
|
import { LinkSelect } from '@/components/input/LinkSelect';
|
||||||
import { PixelSelect } from '@/components/input/PixelSelect';
|
import { PixelSelect } from '@/components/input/PixelSelect';
|
||||||
import { TeamsButton } from '@/components/input/TeamsButton';
|
import { TeamsButton } from '@/components/input/TeamsButton';
|
||||||
import { WebsiteSelect } from '@/components/input/WebsiteSelect';
|
import { WebsiteSelect } from '@/components/input/WebsiteSelect';
|
||||||
|
|
||||||
export function TopNav() {
|
export function TopNav() {
|
||||||
const { websiteId, linkId, pixelId, teamId, router, renderUrl } = useNavigation();
|
const { websiteId, linkId, pixelId, boardId, teamId, router, renderUrl } = useNavigation();
|
||||||
|
|
||||||
const handleWebsiteChange = (value: string) => {
|
const handleWebsiteChange = (value: string) => {
|
||||||
router.push(renderUrl(`/websites/${value}`));
|
router.push(renderUrl(`/websites/${value}`));
|
||||||
|
|
@ -22,6 +23,10 @@ export function TopNav() {
|
||||||
router.push(renderUrl(`/pixels/${value}`));
|
router.push(renderUrl(`/pixels/${value}`));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleBoardChange = (value: string) => {
|
||||||
|
router.push(renderUrl(`/boards/${value}`));
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row
|
<Row
|
||||||
position="sticky"
|
position="sticky"
|
||||||
|
|
@ -36,7 +41,7 @@ export function TopNav() {
|
||||||
>
|
>
|
||||||
<Row alignItems="center" backgroundColor="surface-raised" borderRadius>
|
<Row alignItems="center" backgroundColor="surface-raised" borderRadius>
|
||||||
<TeamsButton />
|
<TeamsButton />
|
||||||
{(websiteId || linkId || pixelId) && (
|
{(websiteId || linkId || pixelId || boardId) && (
|
||||||
<>
|
<>
|
||||||
<Icon size="sm" color="muted" style={{ opacity: 0.7, margin: '0 6px' }}>
|
<Icon size="sm" color="muted" style={{ opacity: 0.7, margin: '0 6px' }}>
|
||||||
<Slash />
|
<Slash />
|
||||||
|
|
@ -74,6 +79,17 @@ export function TopNav() {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{boardId && (
|
||||||
|
<BoardSelect
|
||||||
|
boardId={boardId}
|
||||||
|
teamId={teamId}
|
||||||
|
onChange={handleBoardChange}
|
||||||
|
buttonProps={{
|
||||||
|
variant: 'quiet',
|
||||||
|
style: { minHeight: 40, minWidth: 200, maxWidth: 200 },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,21 @@ import { useApi } from '../useApi';
|
||||||
import { useModified } from '../useModified';
|
import { useModified } from '../useModified';
|
||||||
import { usePagedQuery } from '../usePagedQuery';
|
import { usePagedQuery } from '../usePagedQuery';
|
||||||
|
|
||||||
export function useBoardsQuery({ teamId }: { teamId?: string }, options?: ReactQueryOptions) {
|
export function useBoardsQuery(
|
||||||
|
{ teamId }: { teamId?: string },
|
||||||
|
params?: Record<string, any>,
|
||||||
|
options?: ReactQueryOptions,
|
||||||
|
) {
|
||||||
const { modified } = useModified('boards');
|
const { modified } = useModified('boards');
|
||||||
const { get } = useApi();
|
const { get } = useApi();
|
||||||
|
|
||||||
return usePagedQuery({
|
return usePagedQuery({
|
||||||
queryKey: ['boards', { teamId, modified }],
|
queryKey: ['boards', { teamId, modified, ...params }],
|
||||||
queryFn: pageParams => {
|
queryFn: pageParams => {
|
||||||
return get(teamId ? `/teams/${teamId}/boards` : '/boards', pageParams);
|
return get(teamId ? `/teams/${teamId}/boards` : '/boards', {
|
||||||
|
...pageParams,
|
||||||
|
...params,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
...options,
|
...options,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ export function useNavigation() {
|
||||||
const [, websiteId] = pathname.match(/\/websites\/([a-f0-9-]+)/) || [];
|
const [, websiteId] = pathname.match(/\/websites\/([a-f0-9-]+)/) || [];
|
||||||
const [, linkId] = pathname.match(/\/links\/([a-f0-9-]+)/) || [];
|
const [, linkId] = pathname.match(/\/links\/([a-f0-9-]+)/) || [];
|
||||||
const [, pixelId] = pathname.match(/\/pixels\/([a-f0-9-]+)/) || [];
|
const [, pixelId] = pathname.match(/\/pixels\/([a-f0-9-]+)/) || [];
|
||||||
|
const [, boardId] = pathname.match(/\/boards\/([a-f0-9-]+)/) || [];
|
||||||
const [queryParams, setQueryParams] = useState(Object.fromEntries(searchParams));
|
const [queryParams, setQueryParams] = useState(Object.fromEntries(searchParams));
|
||||||
|
|
||||||
const updateParams = (params?: Record<string, string | number>) => {
|
const updateParams = (params?: Record<string, string | number>) => {
|
||||||
|
|
@ -40,6 +41,7 @@ export function useNavigation() {
|
||||||
websiteId,
|
websiteId,
|
||||||
linkId,
|
linkId,
|
||||||
pixelId,
|
pixelId,
|
||||||
|
boardId,
|
||||||
updateParams,
|
updateParams,
|
||||||
replaceParams,
|
replaceParams,
|
||||||
renderUrl,
|
renderUrl,
|
||||||
|
|
|
||||||
93
src/components/input/BoardSelect.tsx
Normal file
93
src/components/input/BoardSelect.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 { useBoardQuery, useBoardsQuery, useMessages } from '@/components/hooks';
|
||||||
|
import { LayoutDashboard } from '@/components/icons';
|
||||||
|
|
||||||
|
export function BoardSelect({
|
||||||
|
boardId,
|
||||||
|
teamId,
|
||||||
|
onChange,
|
||||||
|
isCollapsed,
|
||||||
|
buttonProps,
|
||||||
|
listProps,
|
||||||
|
...props
|
||||||
|
}: {
|
||||||
|
boardId?: string;
|
||||||
|
teamId?: string;
|
||||||
|
isCollapsed?: boolean;
|
||||||
|
} & SelectProps) {
|
||||||
|
const { t, messages } = useMessages();
|
||||||
|
const { data: board } = useBoardQuery(boardId);
|
||||||
|
const [name, setName] = useState<string>(board?.name);
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
|
const { data, isLoading } = useBoardsQuery({ teamId }, { search, pageSize: 20 });
|
||||||
|
const listItems: { id: string; name: string }[] = data?.data || [];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setName(board?.name);
|
||||||
|
}, [board?.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>
|
||||||
|
<LayoutDashboard />
|
||||||
|
</Icon>
|
||||||
|
<Text truncate>{name}</Text>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
{...props}
|
||||||
|
value={boardId}
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Icon, ListItem, Row, Select, type SelectProps, Text } from '@umami/react-zen';
|
import { Icon, ListItem, Row, Select, type SelectProps, Text } from '@umami/react-zen';
|
||||||
import { useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Empty } from '@/components/common/Empty';
|
import { Empty } from '@/components/common/Empty';
|
||||||
import {
|
import {
|
||||||
useLoginQuery,
|
useLoginQuery,
|
||||||
|
|
@ -35,6 +35,10 @@ export function WebsiteSelect({
|
||||||
);
|
);
|
||||||
const listItems: { id: string; name: string }[] = data?.data || [];
|
const listItems: { id: string; name: string }[] = data?.data || [];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setName(website?.name);
|
||||||
|
}, [website?.name]);
|
||||||
|
|
||||||
const handleSearch = (value: string) => {
|
const handleSearch = (value: string) => {
|
||||||
setSearch(value);
|
setSearch(value);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue