Compare commits

...

5 commits

Author SHA1 Message Date
Mike Cao
ee70baaeb7 Fixed admin link.
Some checks are pending
Node.js CI / build (postgresql, 18.18) (push) Waiting to run
2025-09-23 09:56:38 -07:00
Mike Cao
b115131f4c Fixed async issues. 2025-09-23 09:20:51 -07:00
Mike Cao
f639bb07f4 Added menu options for cloud mode. Async fixes. 2025-09-23 09:14:01 -07:00
Mike Cao
4df6f06485 Added scrolling menu. 2025-09-22 22:45:37 -07:00
Mike Cao
6faf16e9aa Convert all mutate to mutateAsync. 2025-09-22 22:39:25 -07:00
41 changed files with 126 additions and 125 deletions

View file

@ -13,11 +13,11 @@ import { useMessages, useUpdateQuery } from '@/components/hooks';
import { ROLES } from '@/lib/constants'; import { ROLES } from '@/lib/constants';
export function UserAddForm({ onSave, onClose }) { export function UserAddForm({ onSave, onClose }) {
const { mutate, error, isPending } = useUpdateQuery(`/users`); const { mutateAsync, error, isPending } = useUpdateQuery(`/users`);
const { formatMessage, labels, getErrorMessage } = useMessages(); const { formatMessage, labels, getErrorMessage } = useMessages();
const handleSubmit = async (data: any) => { const handleSubmit = async (data: any) => {
mutate(data, { await mutateAsync(data, {
onSuccess: async () => { onSuccess: async () => {
onSave(data); onSave(data);
onClose(); onClose();

View file

@ -13,11 +13,11 @@ export function UserDeleteForm({
onClose?: () => void; onClose?: () => void;
}) { }) {
const { messages, labels, formatMessage } = useMessages(); const { messages, labels, formatMessage } = useMessages();
const { mutate } = useDeleteQuery(`/users/${userId}`); const { mutateAsync } = useDeleteQuery(`/users/${userId}`);
const { touch } = useModified(); const { touch } = useModified();
const handleConfirm = async () => { const handleConfirm = async () => {
mutate(null, { await mutateAsync(null, {
onSuccess: async () => { onSuccess: async () => {
touch('users'); touch('users');
touch(`users:${userId}`); touch(`users:${userId}`);

View file

@ -16,10 +16,10 @@ export function UserEditForm({ userId, onSave }: { userId: string; onSave?: () =
const user = useUser(); const user = useUser();
const { user: login } = useLoginQuery(); const { user: login } = useLoginQuery();
const { mutate, error, toast, touch } = useUpdateQuery(`/users/${userId}`); const { mutateAsync, error, toast, touch } = useUpdateQuery(`/users/${userId}`);
const handleSubmit = async (data: any) => { const handleSubmit = async (data: any) => {
mutate(data, { await mutateAsync(data, {
onSuccess: async () => { onSuccess: async () => {
toast(formatMessage(messages.saved)); toast(formatMessage(messages.saved));
touch(`user:${user.id}`); touch(`user:${user.id}`);

View file

@ -13,10 +13,10 @@ export function BoardAddForm({
onClose?: () => void; onClose?: () => void;
}) { }) {
const { formatMessage, labels, messages } = useMessages(); const { formatMessage, labels, messages } = useMessages();
const { mutate, error, isPending } = useUpdateQuery('/websites', { teamId }); const { mutateAsync, error, isPending } = useUpdateQuery('/websites', { teamId });
const handleSubmit = async (data: any) => { const handleSubmit = async (data: any) => {
mutate(data, { await mutateAsync(data, {
onSuccess: async () => { onSuccess: async () => {
onSave?.(); onSave?.();
onClose?.(); onClose?.();

View file

@ -16,10 +16,10 @@ export function LinkDeleteButton({
onSave?: () => void; onSave?: () => void;
}) { }) {
const { formatMessage, labels, getErrorMessage } = useMessages(); const { formatMessage, labels, getErrorMessage } = useMessages();
const { mutate, isPending, error, touch } = useDeleteQuery(`/links/${linkId}`); const { mutateAsync, isPending, error, touch } = useDeleteQuery(`/links/${linkId}`);
const handleConfirm = (close: () => void) => { const handleConfirm = async (close: () => void) => {
mutate(null, { await mutateAsync(null, {
onSuccess: () => { onSuccess: () => {
touch('links'); touch('links');
onSave?.(); onSave?.();

View file

@ -33,7 +33,7 @@ export function LinkEditForm({
onClose?: () => void; onClose?: () => void;
}) { }) {
const { formatMessage, labels, messages, getErrorMessage } = useMessages(); const { formatMessage, labels, messages, getErrorMessage } = useMessages();
const { mutate, error, isPending, touch, toast } = useUpdateQuery( const { mutateAsync, error, isPending, touch, toast } = useUpdateQuery(
linkId ? `/links/${linkId}` : '/links', linkId ? `/links/${linkId}` : '/links',
{ {
id: linkId, id: linkId,
@ -46,7 +46,7 @@ export function LinkEditForm({
const [slug, setSlug] = useState(generateId()); const [slug, setSlug] = useState(generateId());
const handleSubmit = async (data: any) => { const handleSubmit = async (data: any) => {
mutate(data, { await mutateAsync(data, {
onSuccess: async () => { onSuccess: async () => {
toast(formatMessage(messages.saved)); toast(formatMessage(messages.saved));
touch('links'); touch('links');
@ -139,9 +139,7 @@ export function LinkEditForm({
{formatMessage(labels.cancel)} {formatMessage(labels.cancel)}
</Button> </Button>
)} )}
<FormSubmitButton isDisabled={false} isLoading={isPending}> <FormSubmitButton>{formatMessage(labels.save)}</FormSubmitButton>
{formatMessage(labels.save)}
</FormSubmitButton>
</Row> </Row>
</> </>
); );

View file

@ -14,11 +14,11 @@ export function PixelDeleteButton({
onSave?: () => void; onSave?: () => void;
}) { }) {
const { formatMessage, labels, getErrorMessage } = useMessages(); const { formatMessage, labels, getErrorMessage } = useMessages();
const { mutate, isPending, error } = useDeleteQuery(`/pixels/${pixelId}`); const { mutateAsync, isPending, error } = useDeleteQuery(`/pixels/${pixelId}`);
const { touch } = useModified(); const { touch } = useModified();
const handleConfirm = (close: () => void) => { const handleConfirm = async (close: () => void) => {
mutate(null, { await mutateAsync(null, {
onSuccess: () => { onSuccess: () => {
touch('pixels'); touch('pixels');
onSave?.(); onSave?.();

View file

@ -9,7 +9,7 @@ export function PixelEditButton({ pixelId }: { pixelId: string }) {
return ( return (
<ActionButton title={formatMessage(labels.edit)} icon={<Edit />}> <ActionButton title={formatMessage(labels.edit)} icon={<Edit />}>
<Dialog title={formatMessage(labels.pixel)} style={{ width: 800, minHeight: 300 }}> <Dialog title={formatMessage(labels.pixel)} style={{ width: 600, minHeight: 300 }}>
{({ close }) => { {({ close }) => {
return <PixelEditForm pixelId={pixelId} onClose={close} />; return <PixelEditForm pixelId={pixelId} onClose={close} />;
}} }}

View file

@ -32,7 +32,7 @@ export function PixelEditForm({
onClose?: () => void; onClose?: () => void;
}) { }) {
const { formatMessage, labels, messages, getErrorMessage } = useMessages(); const { formatMessage, labels, messages, getErrorMessage } = useMessages();
const { mutate, error, isPending, touch, toast } = useUpdateQuery( const { mutateAsync, error, isPending, touch, toast } = useUpdateQuery(
pixelId ? `/pixels/${pixelId}` : '/pixels', pixelId ? `/pixels/${pixelId}` : '/pixels',
{ {
id: pixelId, id: pixelId,
@ -45,7 +45,7 @@ export function PixelEditForm({
const [slug, setSlug] = useState(generateId()); const [slug, setSlug] = useState(generateId());
const handleSubmit = async (data: any) => { const handleSubmit = async (data: any) => {
mutate(data, { await mutateAsync(data, {
onSuccess: async () => { onSuccess: async () => {
toast(formatMessage(messages.saved)); toast(formatMessage(messages.saved));
touch('pixels'); touch('pixels');
@ -106,10 +106,7 @@ export function PixelEditForm({
allowCopy allowCopy
style={{ width: '100%' }} style={{ width: '100%' }}
/> />
<Button <Button onPress={() => setValue('slug', handleSlug(), { shouldDirty: true })}>
variant="quiet"
onPress={() => setValue('slug', handleSlug(), { shouldDirty: true })}
>
<Icon> <Icon>
<RefreshCw /> <RefreshCw />
</Icon> </Icon>

View file

@ -1,7 +1,7 @@
import { usePixel, useMessages, useSlug } from '@/components/hooks'; import { usePixel, useMessages, useSlug } from '@/components/hooks';
import { PageHeader } from '@/components/common/PageHeader'; import { PageHeader } from '@/components/common/PageHeader';
import { Icon, Text } from '@umami/react-zen'; import { Icon, Text } from '@umami/react-zen';
import { ExternalLink, Pixel } from '@/components/icons'; import { ExternalLink, Grid2x2 } from '@/components/icons';
import { LinkButton } from '@/components/common/LinkButton'; import { LinkButton } from '@/components/common/LinkButton';
export function PixelHeader() { export function PixelHeader() {
@ -10,7 +10,7 @@ export function PixelHeader() {
const pixel = usePixel(); const pixel = usePixel();
return ( return (
<PageHeader title={pixel.name} description={pixel.url} icon={<Pixel />}> <PageHeader title={pixel.name} description={pixel.slug} icon={<Grid2x2 />}>
<LinkButton href={getSlugUrl(pixel.slug)} target="_blank"> <LinkButton href={getSlugUrl(pixel.slug)} target="_blank">
<Icon> <Icon>
<ExternalLink /> <ExternalLink />

View file

@ -1,8 +1,8 @@
import { PixelPage } from './PixelPage'; import { PixelPage } from './PixelPage';
import { Metadata } from 'next'; import { Metadata } from 'next';
export default async function ({ params }: { params: Promise<{ pixelId: string }> }) { export default function ({ params }: { params: { pixelId: string } }) {
const { pixelId } = await params; const { pixelId } = params;
return <PixelPage pixelId={pixelId} />; return <PixelPage pixelId={pixelId} />;
} }

View file

@ -4,7 +4,7 @@ import { Grid, Column } from '@umami/react-zen';
import { useMessages, useNavigation } from '@/components/hooks'; import { useMessages, useNavigation } from '@/components/hooks';
import { PageBody } from '@/components/common/PageBody'; import { PageBody } from '@/components/common/PageBody';
import { SideMenu } from '@/components/common/SideMenu'; import { SideMenu } from '@/components/common/SideMenu';
import { UserCircle, Users, Knobs } from '@/components/icons'; import { UserCircle, Users, Settings2 } from '@/components/icons';
export function SettingsLayout({ children }: { children: ReactNode }) { export function SettingsLayout({ children }: { children: ReactNode }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
@ -18,7 +18,7 @@ export function SettingsLayout({ children }: { children: ReactNode }) {
id: 'preferences', id: 'preferences',
label: formatMessage(labels.preferences), label: formatMessage(labels.preferences),
path: renderUrl('/settings/preferences'), path: renderUrl('/settings/preferences'),
icon: <Knobs />, icon: <Settings2 />,
}, },
], ],
}, },

View file

@ -10,10 +10,10 @@ import { useMessages, useUpdateQuery } from '@/components/hooks';
export function PasswordEditForm({ onSave, onClose }) { export function PasswordEditForm({ onSave, onClose }) {
const { formatMessage, labels, messages, getErrorMessage } = useMessages(); const { formatMessage, labels, messages, getErrorMessage } = useMessages();
const { mutate, error, isPending } = useUpdateQuery('/me/password'); const { mutateAsync, error, isPending } = useUpdateQuery('/me/password');
const handleSubmit = async (data: any) => { const handleSubmit = async (data: any) => {
mutate(data, { await mutateAsync(data, {
onSuccess: async () => { onSuccess: async () => {
onSave(); onSave();
onClose(); onClose();

View file

@ -10,10 +10,10 @@ import {
export function TeamAddForm({ onSave, onClose }: { onSave: () => void; onClose: () => void }) { export function TeamAddForm({ onSave, onClose }: { onSave: () => void; onClose: () => void }) {
const { formatMessage, labels, getErrorMessage } = useMessages(); const { formatMessage, labels, getErrorMessage } = useMessages();
const { mutate, error, isPending } = useUpdateQuery('/teams'); const { mutateAsync, error, isPending } = useUpdateQuery('/teams');
const handleSubmit = async (data: any) => { const handleSubmit = async (data: any) => {
mutate(data, { await mutateAsync(data, {
onSuccess: async () => { onSuccess: async () => {
onSave?.(); onSave?.();
onClose?.(); onClose?.();

View file

@ -10,10 +10,10 @@ import { useMessages, useUpdateQuery } from '@/components/hooks';
export function TeamJoinForm({ onSave, onClose }: { onSave: () => void; onClose: () => void }) { export function TeamJoinForm({ onSave, onClose }: { onSave: () => void; onClose: () => void }) {
const { formatMessage, labels, getErrorMessage } = useMessages(); const { formatMessage, labels, getErrorMessage } = useMessages();
const { mutate, error, isPending, touch } = useUpdateQuery('/teams/join'); const { mutateAsync, error, touch } = useUpdateQuery('/teams/join');
const handleSubmit = async (data: any) => { const handleSubmit = async (data: any) => {
mutate(data, { await mutateAsync(data, {
onSuccess: async () => { onSuccess: async () => {
touch('teams:members'); touch('teams:members');
onSave?.(); onSave?.();
@ -33,9 +33,7 @@ export function TeamJoinForm({ onSave, onClose }: { onSave: () => void; onClose:
</FormField> </FormField>
<FormButtons> <FormButtons>
<Button onPress={onClose}>{formatMessage(labels.cancel)}</Button> <Button onPress={onClose}>{formatMessage(labels.cancel)}</Button>
<FormSubmitButton variant="primary" isLoading={isPending} isDisabled={isPending}> <FormSubmitButton variant="primary">{formatMessage(labels.join)}</FormSubmitButton>
{formatMessage(labels.join)}
</FormSubmitButton>
</FormButtons> </FormButtons>
</Form> </Form>
); );

View file

@ -15,11 +15,11 @@ export function TeamLeaveForm({
onClose: () => void; onClose: () => void;
}) { }) {
const { formatMessage, labels, messages, getErrorMessage } = useMessages(); const { formatMessage, labels, messages, getErrorMessage } = useMessages();
const { mutate, error, isPending } = useDeleteQuery(`/teams/${teamId}/users/${userId}`); const { mutateAsync, error, isPending } = useDeleteQuery(`/teams/${teamId}/users/${userId}`);
const { touch } = useModified(); const { touch } = useModified();
const handleConfirm = async () => { const handleConfirm = async () => {
mutate(null, { await mutateAsync(null, {
onSuccess: async () => { onSuccess: async () => {
touch('teams:members'); touch('teams:members');
onSave(); onSave();

View file

@ -13,10 +13,10 @@ export function TeamDeleteForm({
onClose?: () => void; onClose?: () => void;
}) { }) {
const { labels, formatMessage, getErrorMessage } = useMessages(); const { labels, formatMessage, getErrorMessage } = useMessages();
const { mutate, error, isPending, touch } = useDeleteQuery(`/teams/${teamId}`); const { mutateAsync, error, isPending, touch } = useDeleteQuery(`/teams/${teamId}`);
const handleConfirm = async () => { const handleConfirm = async () => {
mutate(null, { await mutateAsync(null, {
onSuccess: async () => { onSuccess: async () => {
touch('teams'); touch('teams');
onSave?.(); onSave?.();

View file

@ -25,10 +25,10 @@ export function TeamEditForm({
const team = useTeam(); const team = useTeam();
const { formatMessage, labels, messages, getErrorMessage } = useMessages(); const { formatMessage, labels, messages, getErrorMessage } = useMessages();
const { mutate, error, isPending, touch, toast } = useUpdateQuery(`/teams/${teamId}`); const { mutateAsync, error, isPending, touch, toast } = useUpdateQuery(`/teams/${teamId}`);
const handleSubmit = async (data: any) => { const handleSubmit = async (data: any) => {
mutate(data, { await mutateAsync(data, {
onSuccess: async () => { onSuccess: async () => {
toast(formatMessage(messages.saved)); toast(formatMessage(messages.saved));
touch('teams'); touch('teams');

View file

@ -23,11 +23,11 @@ export function TeamMemberEditForm({
onSave?: () => void; onSave?: () => void;
onClose?: () => void; onClose?: () => void;
}) { }) {
const { mutate, error, isPending } = useUpdateQuery(`/teams/${teamId}/users/${userId}`); const { mutateAsync, error, isPending } = useUpdateQuery(`/teams/${teamId}/users/${userId}`);
const { formatMessage, labels, getErrorMessage } = useMessages(); const { formatMessage, labels, getErrorMessage } = useMessages();
const handleSubmit = async (data: any) => { const handleSubmit = async (data: any) => {
mutate(data, { await mutateAsync(data, {
onSuccess: async () => { onSuccess: async () => {
onSave(); onSave();
onClose(); onClose();

View file

@ -18,11 +18,11 @@ export function TeamMemberRemoveButton({
onSave?: () => void; onSave?: () => void;
}) { }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { mutate, isPending, error } = useDeleteQuery(`/teams/${teamId}/users/${userId}`); const { mutateAsync, isPending, error } = useDeleteQuery(`/teams/${teamId}/users/${userId}`);
const { touch } = useModified(); const { touch } = useModified();
const handleConfirm = (close: () => void) => { const handleConfirm = async (close: () => void) => {
mutate(null, { await mutateAsync(null, {
onSuccess: () => { onSuccess: () => {
touch('teams:members'); touch('teams:members');
onSave?.(); onSave?.();

View file

@ -1,13 +1,13 @@
import { useDeleteQuery, useMessages } from '@/components/hooks'; import { useDeleteQuery, useMessages } from '@/components/hooks';
import { Icon, LoadingButton, Text } from '@umami/react-zen'; import { Icon, LoadingButton, Text } from '@umami/react-zen';
import { Close } from '@/components/icons'; import { X } from '@/components/icons';
export function TeamWebsiteRemoveButton({ teamId, websiteId, onSave }) { export function TeamWebsiteRemoveButton({ teamId, websiteId, onSave }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { mutate, isPending } = useDeleteQuery(`/teams/${teamId}/websites/${websiteId}`); const { mutateAsync } = useDeleteQuery(`/teams/${teamId}/websites/${websiteId}`);
const handleRemoveTeamMember = async () => { const handleRemoveTeamMember = async () => {
mutate(null, { await mutateAsync(null, {
onSuccess: () => { onSuccess: () => {
onSave(); onSave();
}, },
@ -15,9 +15,9 @@ export function TeamWebsiteRemoveButton({ teamId, websiteId, onSave }) {
}; };
return ( return (
<LoadingButton variant="quiet" onClick={() => handleRemoveTeamMember()} isLoading={isPending}> <LoadingButton variant="quiet" onClick={() => handleRemoveTeamMember()}>
<Icon> <Icon>
<Close /> <X />
</Icon> </Icon>
<Text>{formatMessage(labels.remove)}</Text> <Text>{formatMessage(labels.remove)}</Text>
</LoadingButton> </LoadingButton>

View file

@ -13,10 +13,10 @@ export function WebsiteAddForm({
onClose?: () => void; onClose?: () => void;
}) { }) {
const { formatMessage, labels, messages } = useMessages(); const { formatMessage, labels, messages } = useMessages();
const { mutate, error, isPending } = useUpdateQuery('/websites', { teamId }); const { mutateAsync, error, isPending } = useUpdateQuery('/websites', { teamId });
const handleSubmit = async (data: any) => { const handleSubmit = async (data: any) => {
mutate(data, { await mutateAsync(data, {
onSuccess: async () => { onSuccess: async () => {
onSave?.(); onSave?.();
onClose?.(); onClose?.();

View file

@ -33,12 +33,10 @@ export function FunnelEditForm({
}) { }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { data } = useReportQuery(id); const { data } = useReportQuery(id);
const { mutate, error, isPending, touch } = useUpdateQuery(`/reports${id ? `/${id}` : ''}`); const { mutateAsync, error, isPending, touch } = useUpdateQuery(`/reports${id ? `/${id}` : ''}`);
const handleSubmit = async ({ name, ...parameters }) => { const handleSubmit = async ({ name, ...parameters }) => {
// await mutateAsync(
mutate(
{ ...data, id, name, type: 'funnel', websiteId, parameters }, { ...data, id, name, type: 'funnel', websiteId, parameters },
{ {
onSuccess: async () => { onSuccess: async () => {

View file

@ -27,10 +27,10 @@ export function GoalEditForm({
}) { }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { data } = useReportQuery(id); const { data } = useReportQuery(id);
const { mutate, error, isPending, touch } = useUpdateQuery(`/reports${id ? `/${id}` : ''}`); const { mutateAsync, error, isPending, touch } = useUpdateQuery(`/reports${id ? `/${id}` : ''}`);
const handleSubmit = async (formData: Record<string, any>) => { const handleSubmit = async (formData: Record<string, any>) => {
mutate( await mutateAsync(
{ ...formData, type: 'goal', websiteId }, { ...formData, type: 'goal', websiteId },
{ {
onSuccess: async () => { onSuccess: async () => {

View file

@ -8,7 +8,7 @@ import {
Search, Search,
Type, Type,
SquareSlash, SquareSlash,
SquareArrowRight, Share2,
Megaphone, Megaphone,
Earth, Earth,
Globe, Globe,
@ -20,7 +20,7 @@ import {
Monitor, Monitor,
Cpu, Cpu,
LightningSvg, LightningSvg,
LucideCaseSensitive, Network,
Tag, Tag,
} from '@/components/icons'; } from '@/components/icons';
@ -80,7 +80,7 @@ export function WebsiteExpandedView({
id: 'referrer', id: 'referrer',
label: formatMessage(labels.referrer), label: formatMessage(labels.referrer),
path: updateParams({ view: 'referrer' }), path: updateParams({ view: 'referrer' }),
icon: <SquareArrowRight />, icon: <Share2 />,
}, },
{ {
id: 'channel', id: 'channel',
@ -167,7 +167,7 @@ export function WebsiteExpandedView({
id: 'hostname', id: 'hostname',
label: formatMessage(labels.hostname), label: formatMessage(labels.hostname),
path: updateParams({ view: 'hostname' }), path: updateParams({ view: 'hostname' }),
icon: <LucideCaseSensitive />, icon: <Network />,
}, },
{ {
id: 'tag', id: 'tag',
@ -181,7 +181,7 @@ export function WebsiteExpandedView({
return ( return (
<Grid columns="auto 1fr" gap="6" height="100%" overflow="hidden"> <Grid columns="auto 1fr" gap="6" height="100%" overflow="hidden">
<Column gap="6" border="right" paddingRight="3"> <Column gap="6" border="right" paddingRight="3" overflowY="auto">
<SideMenu items={items} selectedKey={view} muteItems={false} /> <SideMenu items={items} selectedKey={view} muteItems={false} />
</Column> </Column>
<Column overflow="hidden"> <Column overflow="hidden">

View file

@ -10,7 +10,7 @@ import {
MagnetSvg, MagnetSvg,
Tag, Tag,
MoneySvg, MoneySvg,
Network, NetworkSvg,
ChartPie, ChartPie,
UserPlus, UserPlus,
CompareSvg, CompareSvg,
@ -137,7 +137,7 @@ export function WebsiteNav({ websiteId }: { websiteId: string }) {
{ {
id: 'attribution', id: 'attribution',
label: formatMessage(labels.attribution), label: formatMessage(labels.attribution),
icon: <Network />, icon: <NetworkSvg />,
path: renderPath('/attribution'), path: renderPath('/attribution'),
}, },
], ],

View file

@ -17,12 +17,12 @@ export function CohortDeleteButton({
onSave?: () => void; onSave?: () => void;
}) { }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { mutate, isPending, error, touch } = useDeleteQuery( const { mutateAsync, isPending, error, touch } = useDeleteQuery(
`/websites/${websiteId}/segments/${cohortId}`, `/websites/${websiteId}/segments/${cohortId}`,
); );
const handleConfirm = (close: () => void) => { const handleConfirm = async (close: () => void) => {
mutate(null, { await mutateAsync(null, {
onSuccess: () => { onSuccess: () => {
touch('cohorts'); touch('cohorts');
onSave?.(); onSave?.();

View file

@ -33,7 +33,7 @@ export function CohortEditForm({
const { data } = useWebsiteCohortQuery(websiteId, cohortId); const { data } = useWebsiteCohortQuery(websiteId, cohortId);
const { formatMessage, labels, messages, getErrorMessage } = useMessages(); const { formatMessage, labels, messages, getErrorMessage } = useMessages();
const { mutate, error, isPending, touch, toast } = useUpdateQuery( const { mutateAsync, error, isPending, touch, toast } = useUpdateQuery(
`/websites/${websiteId}/segments${cohortId ? `/${cohortId}` : ''}`, `/websites/${websiteId}/segments${cohortId ? `/${cohortId}` : ''}`,
{ {
type: 'cohort', type: 'cohort',
@ -41,7 +41,7 @@ export function CohortEditForm({
); );
const handleSubmit = async (formData: any) => { const handleSubmit = async (formData: any) => {
mutate(formData, { await mutateAsync(formData, {
onSuccess: async () => { onSuccess: async () => {
toast(formatMessage(messages.saved)); toast(formatMessage(messages.saved));
touch('cohorts'); touch('cohorts');

View file

@ -17,12 +17,12 @@ export function SegmentDeleteButton({
onSave?: () => void; onSave?: () => void;
}) { }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { mutate, isPending, error, touch } = useDeleteQuery( const { mutateAsync, isPending, error, touch } = useDeleteQuery(
`/websites/${websiteId}/segments/${segmentId}`, `/websites/${websiteId}/segments/${segmentId}`,
); );
const handleConfirm = (close: () => void) => { const handleConfirm = async (close: () => void) => {
mutate(null, { await mutateAsync(null, {
onSuccess: () => { onSuccess: () => {
touch('segments'); touch('segments');
onSave?.(); onSave?.();

View file

@ -30,7 +30,7 @@ export function SegmentEditForm({
const { data } = useWebsiteSegmentQuery(websiteId, segmentId); const { data } = useWebsiteSegmentQuery(websiteId, segmentId);
const { formatMessage, labels, getErrorMessage } = useMessages(); const { formatMessage, labels, getErrorMessage } = useMessages();
const { mutate, error, isPending, touch, toast } = useUpdateQuery( const { mutateAsync, error, isPending, touch, toast } = useUpdateQuery(
`/websites/${websiteId}/segments${segmentId ? `/${segmentId}` : ''}`, `/websites/${websiteId}/segments${segmentId ? `/${segmentId}` : ''}`,
{ {
type: 'segment', type: 'segment',
@ -38,7 +38,7 @@ export function SegmentEditForm({
); );
const handleSubmit = async (formData: any) => { const handleSubmit = async (formData: any) => {
mutate(formData, { await mutateAsync(formData, {
onSuccess: async () => { onSuccess: async () => {
toast(formatMessage(messages.saved)); toast(formatMessage(messages.saved));
touch('segments'); touch('segments');

View file

@ -13,10 +13,10 @@ export function WebsiteDeleteForm({
onClose?: () => void; onClose?: () => void;
}) { }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { mutate, isPending, error, touch } = useDeleteQuery(`/websites/${websiteId}`); const { mutateAsync, isPending, error, touch } = useDeleteQuery(`/websites/${websiteId}`);
const handleConfirm = async () => { const handleConfirm = async () => {
mutate(null, { await mutateAsync(null, {
onSuccess: async () => { onSuccess: async () => {
touch('websites'); touch('websites');
touch(`websites:${websiteId}`); touch(`websites:${websiteId}`);

View file

@ -5,10 +5,10 @@ import { DOMAIN_REGEX } from '@/lib/constants';
export function WebsiteEditForm({ websiteId, onSave }: { websiteId: string; onSave?: () => void }) { export function WebsiteEditForm({ websiteId, onSave }: { websiteId: string; onSave?: () => void }) {
const website = useWebsite(); const website = useWebsite();
const { formatMessage, labels, messages, getErrorMessage } = useMessages(); const { formatMessage, labels, messages, getErrorMessage } = useMessages();
const { mutate, error, touch, toast, isPending } = useUpdateQuery(`/websites/${websiteId}`); const { mutateAsync, error, touch, toast } = useUpdateQuery(`/websites/${websiteId}`);
const handleSubmit = async (data: any) => { const handleSubmit = async (data: any) => {
mutate(data, { await mutateAsync(data, {
onSuccess: async () => { onSuccess: async () => {
toast(formatMessage(messages.saved)); toast(formatMessage(messages.saved));
touch(`website:${website.id}`); touch(`website:${website.id}`);
@ -45,12 +45,7 @@ export function WebsiteEditForm({ websiteId, onSave }: { websiteId: string; onSa
<TextField /> <TextField />
</FormField> </FormField>
<FormButtons> <FormButtons>
<FormSubmitButton <FormSubmitButton data-test="button-submit" variant="primary">
data-test="button-submit"
variant="primary"
isLoading={isPending}
isDisabled={isPending}
>
{formatMessage(labels.save)} {formatMessage(labels.save)}
</FormSubmitButton> </FormSubmitButton>
</FormButtons> </FormButtons>

View file

@ -13,10 +13,10 @@ export function WebsiteResetForm({
onClose?: () => void; onClose?: () => void;
}) { }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { mutate, isPending, error } = useUpdateQuery(`/websites/${websiteId}/reset`); const { mutateAsync, isPending, error } = useUpdateQuery(`/websites/${websiteId}/reset`);
const handleConfirm = async () => { const handleConfirm = async () => {
mutate(null, { await mutateAsync(null, {
onSuccess: async () => { onSuccess: async () => {
onSave?.(); onSave?.();
onClose?.(); onClose?.();

View file

@ -25,7 +25,7 @@ export interface WebsiteShareFormProps {
export function WebsiteShareForm({ websiteId, shareId, onSave, onClose }: WebsiteShareFormProps) { export function WebsiteShareForm({ websiteId, shareId, onSave, onClose }: WebsiteShareFormProps) {
const { formatMessage, labels, messages, getErrorMessage } = useMessages(); const { formatMessage, labels, messages, getErrorMessage } = useMessages();
const [id, setId] = useState(shareId); const [id, setId] = useState(shareId);
const { mutate, error, isPending, touch, toast } = useUpdateQuery(`/websites/${websiteId}`); const { mutateAsync, error, touch, toast } = useUpdateQuery(`/websites/${websiteId}`);
const url = `${window?.location.origin || ''}${process.env.basePath || ''}/share/${id}`; const url = `${window?.location.origin || ''}${process.env.basePath || ''}/share/${id}`;
@ -37,11 +37,11 @@ export function WebsiteShareForm({ websiteId, shareId, onSave, onClose }: Websit
setId(id ? null : generateId()); setId(id ? null : generateId());
}; };
const handleSave = () => { const handleSave = async () => {
const data = { const data = {
shareId: id, shareId: id,
}; };
mutate(data, { await mutateAsync(data, {
onSuccess: async () => { onSuccess: async () => {
toast(formatMessage(messages.saved)); toast(formatMessage(messages.saved));
touch(`website:${websiteId}`); touch(`website:${websiteId}`);
@ -69,9 +69,7 @@ export function WebsiteShareForm({ websiteId, shareId, onSave, onClose }: Websit
</Row> </Row>
<Row alignItems="center" gap> <Row alignItems="center" gap>
{onClose && <Button onPress={onClose}>{formatMessage(labels.cancel)}</Button>} {onClose && <Button onPress={onClose}>{formatMessage(labels.cancel)}</Button>}
<FormSubmitButton isDisabled={false} isLoading={isPending}> <FormSubmitButton isDisabled={false}>{formatMessage(labels.save)}</FormSubmitButton>
{formatMessage(labels.save)}
</FormSubmitButton>
</Row> </Row>
</FormButtons> </FormButtons>
</Column> </Column>

View file

@ -32,7 +32,7 @@ export function WebsiteTransferForm({
const website = useWebsite(); const website = useWebsite();
const [teamId, setTeamId] = useState<string>(null); const [teamId, setTeamId] = useState<string>(null);
const { formatMessage, labels, messages, getErrorMessage } = useMessages(); const { formatMessage, labels, messages, getErrorMessage } = useMessages();
const { mutate, error, isPending } = useUpdateQuery(`/websites/${websiteId}/transfer`); const { mutateAsync, error, isPending } = useUpdateQuery(`/websites/${websiteId}/transfer`);
const { data: teams, isLoading } = useUserTeamsQuery(user.id); const { data: teams, isLoading } = useUserTeamsQuery(user.id);
const isTeamWebsite = !!website?.teamId; const isTeamWebsite = !!website?.teamId;
@ -45,7 +45,7 @@ export function WebsiteTransferForm({
) || []; ) || [];
const handleSubmit = async () => { const handleSubmit = async () => {
mutate( await mutateAsync(
{ {
userId: website.teamId ? user.id : undefined, userId: website.teamId ? user.id : undefined,
teamId: website.userId ? teamId : undefined, teamId: website.userId ? teamId : undefined,

View file

@ -18,10 +18,10 @@ import { LogoSvg } from '@/components/icons';
export function LoginForm() { export function LoginForm() {
const { formatMessage, labels, getErrorMessage } = useMessages(); const { formatMessage, labels, getErrorMessage } = useMessages();
const router = useRouter(); const router = useRouter();
const { mutate, error, isPending } = useUpdateQuery('/auth/login'); const { mutateAsync, error } = useUpdateQuery('/auth/login');
const handleSubmit = async (data: any) => { const handleSubmit = async (data: any) => {
mutate(data, { await mutateAsync(data, {
onSuccess: async ({ token, user }) => { onSuccess: async ({ token, user }) => {
setClientAuthToken(token); setClientAuthToken(token);
setUser(user); setUser(user);
@ -55,13 +55,7 @@ export function LoginForm() {
<PasswordField /> <PasswordField />
</FormField> </FormField>
<FormButtons> <FormButtons>
<FormSubmitButton <FormSubmitButton data-test="button-submit" variant="primary" style={{ flex: 1 }}>
data-test="button-submit"
variant="primary"
isLoading={isPending}
isDisabled={isPending}
style={{ flex: 1 }}
>
{formatMessage(labels.login)} {formatMessage(labels.login)}
</FormSubmitButton> </FormSubmitButton>
</FormButtons> </FormButtons>

View file

@ -31,7 +31,7 @@ export function ReportEditButton({
const { formatMessage, labels, messages } = useMessages(); const { formatMessage, labels, messages } = useMessages();
const [showEdit, setShowEdit] = useState(false); const [showEdit, setShowEdit] = useState(false);
const [showDelete, setShowDelete] = useState(false); const [showDelete, setShowDelete] = useState(false);
const { mutate, touch } = useDeleteQuery(`/reports/${id}`); const { mutateAsync, touch } = useDeleteQuery(`/reports/${id}`);
const handleAction = (id: any) => { const handleAction = (id: any) => {
if (id === 'edit') { if (id === 'edit') {
@ -47,7 +47,7 @@ export function ReportEditButton({
}; };
const handleDelete = async () => { const handleDelete = async () => {
mutate(null, { await mutateAsync(null, {
onSuccess: async () => { onSuccess: async () => {
touch(`reports:${type}`); touch(`reports:${type}`);
setShowDelete(false); setShowDelete(false);

View file

@ -1,6 +1,7 @@
import { List, ListItem } from '@umami/react-zen'; import { List, ListItem } from '@umami/react-zen';
import { useWebsiteSegmentsQuery } from '@/components/hooks'; import { useWebsiteSegmentsQuery } from '@/components/hooks';
import { LoadingPanel } from '@/components/common/LoadingPanel'; import { LoadingPanel } from '@/components/common/LoadingPanel';
import { Empty } from '@/components/common/Empty';
export interface SegmentFiltersProps { export interface SegmentFiltersProps {
websiteId: string; websiteId: string;
@ -23,6 +24,7 @@ export function SegmentFilters({
return ( return (
<LoadingPanel data={data} isLoading={isLoading} isFetching={isFetching} overflowY="auto"> <LoadingPanel data={data} isLoading={isLoading} isFetching={isFetching} overflowY="auto">
{data?.data?.length === 0 && <Empty />}
<List selectionMode="single" value={[segmentId]} onChange={id => handleChange(id[0])}> <List selectionMode="single" value={[segmentId]} onChange={id => handleChange(id[0])}>
{data?.data?.map(item => { {data?.data?.map(item => {
return ( return (

View file

@ -10,23 +10,27 @@ import {
MenuSection, MenuSection,
} from '@umami/react-zen'; } from '@umami/react-zen';
import { useMessages, useLoginQuery, useNavigation, useConfig } from '@/components/hooks'; import { useMessages, useLoginQuery, useNavigation, useConfig } from '@/components/hooks';
import { LogOut, LockKeyhole, Settings, UserCircle } from '@/components/icons'; import { LogOut, LockKeyhole, Settings, UserCircle, LifeBuoy, BookText } from '@/components/icons';
import { DOCS_URL } from '@/lib/constants';
export function SettingsButton() { export function SettingsButton() {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { user } = useLoginQuery(); const { user } = useLoginQuery();
const { router, renderUrl } = useNavigation(); const { router } = useNavigation();
const { cloudMode } = useConfig(); const { cloudMode } = useConfig();
const handleAction = (id: Key) => { const handleAction = (id: Key) => {
if (id === 'settings') { const url = id.toString();
if (cloudMode) {
window.location.href = `/settings`;
return;
}
}
router.push(renderUrl(`/${id}`)); if (cloudMode) {
if (url === '/docs') {
window.open(DOCS_URL, '_blank');
} else {
window.location.href = url;
}
} else {
router.push(url);
}
}; };
return ( return (
@ -40,12 +44,26 @@ export function SettingsButton() {
<Menu autoFocus="last" onAction={handleAction}> <Menu autoFocus="last" onAction={handleAction}>
<MenuSection title={user.username}> <MenuSection title={user.username}>
<MenuSeparator /> <MenuSeparator />
<MenuItem id="settings" icon={<Settings />} label={formatMessage(labels.settings)} /> <MenuItem id="/settings" icon={<Settings />} label={formatMessage(labels.settings)} />
{!cloudMode && user.isAdmin && ( {!cloudMode && user.isAdmin && (
<MenuItem id="admin" icon={<LockKeyhole />} label={formatMessage(labels.admin)} /> <MenuItem id="/admin" icon={<LockKeyhole />} label={formatMessage(labels.admin)} />
)}
{cloudMode && (
<>
<MenuItem
id="/docs"
icon={<BookText />}
label={formatMessage(labels.documentation)}
/>
<MenuItem
id="/settings/support"
icon={<LifeBuoy />}
label={formatMessage(labels.support)}
/>
</>
)} )}
<MenuSeparator /> <MenuSeparator />
<MenuItem id="logout" icon={<LogOut />} label={formatMessage(labels.logout)} /> <MenuItem id="/logout" icon={<LogOut />} label={formatMessage(labels.logout)} />
</MenuSection> </MenuSection>
</Menu> </Menu>
</Popover> </Popover>

View file

@ -360,6 +360,8 @@ export const labels = defineMessages({
environment: { id: 'label.environment', defaultMessage: 'Environment' }, environment: { id: 'label.environment', defaultMessage: 'Environment' },
criteria: { id: 'label.criteria', defaultMessage: 'Criteria' }, criteria: { id: 'label.criteria', defaultMessage: 'Criteria' },
share: { id: 'label.share', defaultMessage: 'Share' }, share: { id: 'label.share', defaultMessage: 'Share' },
support: { id: 'label.support', defaultMessage: 'Support' },
documentation: { id: 'label.documentation', defaultMessage: 'Documentation' },
}); });
export const messages = defineMessages({ export const messages = defineMessages({

View file

@ -8,6 +8,7 @@ export const DASHBOARD_CONFIG = 'umami.dashboard';
export const VERSION_CHECK = 'umami.version-check'; export const VERSION_CHECK = 'umami.version-check';
export const SHARE_TOKEN_HEADER = 'x-umami-share-token'; export const SHARE_TOKEN_HEADER = 'x-umami-share-token';
export const HOMEPAGE_URL = 'https://umami.is'; export const HOMEPAGE_URL = 'https://umami.is';
export const DOCS_URL = 'https://umami.is/docs';
export const REPO_URL = 'https://github.com/umami-software/umami'; export const REPO_URL = 'https://github.com/umami-software/umami';
export const UPDATES_URL = 'https://api.umami.is/v1/updates'; export const UPDATES_URL = 'https://api.umami.is/v1/updates';
export const TELEMETRY_PIXEL = 'https://i.umami.is/a.png'; export const TELEMETRY_PIXEL = 'https://i.umami.is/a.png';