Merge branch 'dev' into boards
Some checks failed
Node.js CI / build (push) Has been cancelled

# Conflicts:
#	pnpm-lock.yaml
This commit is contained in:
Mike Cao 2026-01-07 01:01:39 -08:00
commit aefc36b476
64 changed files with 699 additions and 319 deletions

View file

@ -4,13 +4,14 @@ import {
Form,
FormField,
FormSubmitButton,
Grid,
Icon,
Label,
Loading,
Row,
TextField,
} from '@umami/react-zen';
import { useEffect, useState } from 'react';
import { useState } from 'react';
import { useConfig, useLinkQuery, useMessages } from '@/components/hooks';
import { useUpdateQuery } from '@/components/hooks/queries/useUpdateQuery';
import { RefreshCw } from '@/components/icons';
@ -42,7 +43,7 @@ export function LinkEditForm({
const { linksUrl } = useConfig();
const hostUrl = linksUrl || LINKS_URL;
const { data, isLoading } = useLinkQuery(linkId);
const [slug, setSlug] = useState(generateId());
const [defaultSlug] = useState(generateId());
const handleSubmit = async (data: any) => {
await mutateAsync(data, {
@ -55,14 +56,6 @@ export function LinkEditForm({
});
};
const handleSlug = () => {
const slug = generateId();
setSlug(slug);
return slug;
};
const checkUrl = (url: string) => {
if (!isValidUrl(url)) {
return formatMessage(labels.invalidUrl);
@ -70,19 +63,19 @@ export function LinkEditForm({
return true;
};
useEffect(() => {
if (data) {
setSlug(data.slug);
}
}, [data]);
if (linkId && isLoading) {
return <Loading placement="absolute" />;
}
return (
<Form onSubmit={handleSubmit} error={getErrorMessage(error)} defaultValues={{ slug, ...data }}>
{({ setValue }) => {
<Form
onSubmit={handleSubmit}
error={getErrorMessage(error)}
defaultValues={{ slug: defaultSlug, ...data }}
>
{({ setValue, watch }) => {
const slug = watch('slug');
return (
<>
<FormField
@ -101,15 +94,25 @@ export function LinkEditForm({
<TextField placeholder="https://example.com" autoComplete="off" />
</FormField>
<FormField
name="slug"
rules={{
required: formatMessage(labels.required),
}}
style={{ display: 'none' }}
>
<input type="hidden" />
</FormField>
<Grid columns="1fr auto" alignItems="end" gap>
<FormField
name="slug"
label={formatMessage({ id: 'label.slug', defaultMessage: 'Slug' })}
rules={{
required: formatMessage(labels.required),
}}
>
<TextField autoComplete="off" />
</FormField>
<Button
variant="quiet"
onPress={() => setValue('slug', generateId(), { shouldDirty: true })}
>
<Icon>
<RefreshCw />
</Icon>
</Button>
</Grid>
<Column>
<Label>{formatMessage(labels.link)}</Label>
@ -121,14 +124,6 @@ export function LinkEditForm({
allowCopy
style={{ width: '100%' }}
/>
<Button
variant="quiet"
onPress={() => setValue('slug', handleSlug(), { shouldDirty: true })}
>
<Icon>
<RefreshCw />
</Icon>
</Button>
</Row>
</Column>

View file

@ -1,8 +1,14 @@
import type { Metadata } from 'next';
import { getLink } from '@/queries/prisma';
import { LinkPage } from './LinkPage';
export default async function ({ params }: { params: Promise<{ linkId: string }> }) {
const { linkId } = await params;
const link = await getLink(linkId);
if (!link || link?.deletedAt) {
return null;
}
return <LinkPage linkId={linkId} />;
}

View file

@ -1,8 +1,14 @@
import type { Metadata } from 'next';
import { getPixel } from '@/queries/prisma';
import { PixelPage } from './PixelPage';
export default async function ({ params }: { params: { pixelId: string } }) {
const { pixelId } = await params;
const pixel = await getPixel(pixelId);
if (!pixel || pixel?.deletedAt) {
return null;
}
return <PixelPage pixelId={pixelId} />;
}

View file

@ -4,6 +4,7 @@ import { DateRangeSetting } from './DateRangeSetting';
import { LanguageSetting } from './LanguageSetting';
import { ThemeSetting } from './ThemeSetting';
import { TimezoneSetting } from './TimezoneSetting';
import { VersionSetting } from './VersionSetting';
export function PreferenceSettings() {
const { user } = useLoginQuery();
@ -31,6 +32,10 @@ export function PreferenceSettings() {
<Label>{formatMessage(labels.theme)}</Label>
<ThemeSetting />
</Column>
<Column>
<Label>{formatMessage(labels.version)}</Label>
<VersionSetting />
</Column>
</Column>
);
}

View file

@ -0,0 +1,8 @@
'use client';
import { Text } from '@umami/react-zen';
import { CURRENT_VERSION } from '@/lib/constants';
export function VersionSetting() {
return <Text>{CURRENT_VERSION}</Text>;
}

View file

@ -12,9 +12,10 @@ import { ListTable } from '@/components/metrics/ListTable';
import { MetricCard } from '@/components/metrics/MetricCard';
import { MetricsBar } from '@/components/metrics/MetricsBar';
import { renderDateLabels } from '@/lib/charts';
import { CHART_COLORS } from '@/lib/constants';
import { CHART_COLORS, CURRENCY_CONFIG, DEFAULT_CURRENCY } from '@/lib/constants';
import { generateTimeSeries } from '@/lib/date';
import { formatLongCurrency, formatLongNumber } from '@/lib/format';
import { getItem, setItem } from '@/lib/storage';
export interface RevenueProps {
websiteId: string;
@ -24,7 +25,15 @@ export interface RevenueProps {
}
export function Revenue({ websiteId, startDate, endDate, unit }: RevenueProps) {
const [currency, setCurrency] = useState('USD');
const [currency, setCurrency] = useState(
getItem(CURRENCY_CONFIG) || process.env.defaultCurrency || DEFAULT_CURRENCY,
);
const handleCurrencyChange = (value: string) => {
setCurrency(value);
setItem(CURRENCY_CONFIG, value);
};
const { formatMessage, labels } = useMessages();
const { locale, dateLocale } = useLocale();
const { countryNames } = useCountryNames(locale);
@ -107,7 +116,7 @@ export function Revenue({ websiteId, startDate, endDate, unit }: RevenueProps) {
return (
<Column gap>
<Grid columns="280px" gap>
<CurrencySelect value={currency} onChange={setCurrency} />
<CurrencySelect value={currency} onChange={handleCurrencyChange} />
</Grid>
<LoadingPanel data={data} isLoading={isLoading} error={error}>
{data && (

View file

@ -1,5 +1,6 @@
import type { Metadata } from 'next';
import { WebsiteLayout } from '@/app/(main)/websites/[websiteId]/WebsiteLayout';
import { getWebsite } from '@/queries/prisma';
export default async function ({
children,
@ -9,6 +10,11 @@ export default async function ({
params: Promise<{ websiteId: string }>;
}) {
const { websiteId } = await params;
const website = await getWebsite(websiteId);
if (!website || website?.deletedAt) {
return null;
}
return <WebsiteLayout websiteId={websiteId}>{children}</WebsiteLayout>;
}

View file

@ -17,5 +17,6 @@ export async function GET(request: Request) {
telemetryDisabled: !!process.env.DISABLE_TELEMETRY,
trackerScriptName: process.env.TRACKER_SCRIPT_NAME,
updatesDisabled: !!process.env.DISABLE_UPDATES,
currentVersion: !!process.env.currentVersion,
});
}

View file

@ -17,8 +17,8 @@ export async function POST(request: Request) {
return unauthorized();
}
const parameters = await setWebsiteDate(websiteId, body.parameters);
const filters = await getQueryFilters(body.filters, websiteId);
const parameters = await setWebsiteDate(websiteId, auth.user.id, body.parameters);
const filters = await getQueryFilters(body.filters, websiteId, auth.user?.id);
const data = await getAttribution(websiteId, parameters as AttributionParameters, filters);

View file

@ -17,8 +17,8 @@ export async function POST(request: Request) {
return unauthorized();
}
const parameters = await setWebsiteDate(websiteId, body.parameters);
const filters = await getQueryFilters(body.filters, websiteId);
const parameters = await setWebsiteDate(websiteId, auth.user.id, body.parameters);
const filters = await getQueryFilters(body.filters, websiteId, auth.user?.id);
const data = await getBreakdown(websiteId, parameters as BreakdownParameters, filters);

View file

@ -17,8 +17,8 @@ export async function POST(request: Request) {
return unauthorized();
}
const parameters = await setWebsiteDate(websiteId, body.parameters);
const filters = await getQueryFilters(body.filters, websiteId);
const parameters = await setWebsiteDate(websiteId, auth.user.id, body.parameters);
const filters = await getQueryFilters(body.filters, websiteId, auth.user?.id);
const data = await getFunnel(websiteId, parameters as FunnelParameters, filters);

View file

@ -17,8 +17,8 @@ export async function POST(request: Request) {
return unauthorized();
}
const parameters = await setWebsiteDate(websiteId, body.parameters);
const filters = await getQueryFilters(body.filters, websiteId);
const parameters = await setWebsiteDate(websiteId, auth.user.id, body.parameters);
const filters = await getQueryFilters(body.filters, websiteId, auth.user?.id);
const data = await getGoal(websiteId, parameters as GoalParameters, filters);

View file

@ -17,8 +17,8 @@ export async function POST(request: Request) {
return unauthorized();
}
const filters = await getQueryFilters(body.filters, websiteId);
const parameters = await setWebsiteDate(websiteId, body.parameters);
const filters = await getQueryFilters(body.filters, websiteId, auth.user?.id);
const parameters = await setWebsiteDate(websiteId, auth.user.id, body.parameters);
const data = await getRetention(websiteId, parameters as RetentionParameters, filters);

View file

@ -17,8 +17,8 @@ export async function POST(request: Request) {
return unauthorized();
}
const parameters = await setWebsiteDate(websiteId, body.parameters);
const filters = await getQueryFilters(body.filters, websiteId);
const parameters = await setWebsiteDate(websiteId, auth.user.id, body.parameters);
const filters = await getQueryFilters(body.filters, websiteId, auth.user?.id);
const data = await getRevenue(websiteId, parameters as RevenuParameters, filters);

View file

@ -18,8 +18,8 @@ export async function POST(request: Request) {
return unauthorized();
}
const filters = await getQueryFilters(body.filters, websiteId);
const parameters = await setWebsiteDate(websiteId, body.parameters);
const filters = await getQueryFilters(body.filters, websiteId, auth.user?.id);
const parameters = await setWebsiteDate(websiteId, auth.user.id, body.parameters);
const data = {
utm_source: [],

View file

@ -27,7 +27,7 @@ export async function GET(
return unauthorized();
}
const filters = await getQueryFilters(query, websiteId);
const filters = await getQueryFilters(query, websiteId, auth.user?.id);
const data = await getEventDataEvents(websiteId, {
...filters,

View file

@ -27,7 +27,7 @@ export async function GET(
return unauthorized();
}
const filters = await getQueryFilters(query, websiteId);
const filters = await getQueryFilters(query, websiteId, auth.user?.id);
const data = await getEventDataFields(websiteId, filters);

View file

@ -27,7 +27,7 @@ export async function GET(
return unauthorized();
}
const filters = await getQueryFilters(query, websiteId);
const filters = await getQueryFilters(query, websiteId, auth.user?.id);
const data = await getEventDataProperties(websiteId, filters);

View file

@ -27,7 +27,7 @@ export async function GET(
return unauthorized();
}
const filters = await getQueryFilters(query, websiteId);
const filters = await getQueryFilters(query, websiteId, auth.user?.id);
const data = await getEventDataStats(websiteId, filters);

View file

@ -30,7 +30,7 @@ export async function GET(
}
const { propertyName } = query;
const filters = await getQueryFilters(query, websiteId);
const filters = await getQueryFilters(query, websiteId, auth.user?.id);
const data = await getEventDataValues(websiteId, {
...filters,

View file

@ -29,7 +29,7 @@ export async function GET(
return unauthorized();
}
const filters = await getQueryFilters(query, websiteId);
const filters = await getQueryFilters(query, websiteId, auth.user?.id);
const data = await getWebsiteEvents(websiteId, filters);

View file

@ -29,7 +29,7 @@ export async function GET(
return unauthorized();
}
const filters = await getQueryFilters(query, websiteId);
const filters = await getQueryFilters(query, websiteId, auth.user?.id);
const data = await getEventStats(websiteId, filters);

View file

@ -28,7 +28,7 @@ export async function GET(
return unauthorized();
}
const filters = await getQueryFilters(query, websiteId);
const filters = await getQueryFilters(query, websiteId, auth.user?.id);
const [events, pages, referrers, browsers, os, devices, countries] = await Promise.all([
getEventMetrics(websiteId, { type: 'event' }, filters),

View file

@ -37,7 +37,7 @@ export async function GET(
}
const { type, limit, offset, search } = query;
const filters = await getQueryFilters(query, websiteId);
const filters = await getQueryFilters(query, websiteId, auth.user?.id);
if (search) {
filters[type] = `c.${search}`;

View file

@ -37,7 +37,7 @@ export async function GET(
}
const { type, limit, offset, search } = query;
const filters = await getQueryFilters(query, websiteId);
const filters = await getQueryFilters(query, websiteId, auth.user?.id);
if (search) {
filters[type] = `c.${search}`;

View file

@ -27,7 +27,7 @@ export async function GET(
return unauthorized();
}
const filters = await getQueryFilters(query, websiteId);
const filters = await getQueryFilters(query, websiteId, auth.user?.id);
const [pageviews, sessions] = await Promise.all([
getPageviewStats(websiteId, filters),

View file

@ -27,7 +27,7 @@ export async function GET(
return unauthorized();
}
const filters = await getQueryFilters(query, websiteId);
const filters = await getQueryFilters(query, websiteId, auth.user?.id);
const data = await getSessionDataProperties(websiteId, filters);

View file

@ -29,7 +29,7 @@ export async function GET(
}
const { propertyName } = query;
const filters = await getQueryFilters(query, websiteId);
const filters = await getQueryFilters(query, websiteId, auth.user?.id);
const data = await getSessionDataValues(websiteId, {
...filters,

View file

@ -25,7 +25,7 @@ export async function GET(
return unauthorized();
}
const filters = await getQueryFilters(query, websiteId);
const filters = await getQueryFilters(query, websiteId, auth.user?.id);
const data = await getSessionActivity(websiteId, sessionId, filters);

View file

@ -28,7 +28,7 @@ export async function GET(
return unauthorized();
}
const filters = await getQueryFilters(query, websiteId);
const filters = await getQueryFilters(query, websiteId, auth.user?.id);
const data = await getWebsiteSessions(websiteId, filters);

View file

@ -27,7 +27,7 @@ export async function GET(
return unauthorized();
}
const filters = await getQueryFilters(query, websiteId);
const filters = await getQueryFilters(query, websiteId, auth.user?.id);
const metrics = await getWebsiteSessionStats(websiteId, filters);

View file

@ -28,7 +28,7 @@ export async function GET(
return unauthorized();
}
const filters = await getQueryFilters(query, websiteId);
const filters = await getQueryFilters(query, websiteId, auth.user?.id);
const data = await getWeeklyTraffic(websiteId, filters);

View file

@ -27,7 +27,7 @@ export async function GET(
return unauthorized();
}
const filters = await getQueryFilters(query, websiteId);
const filters = await getQueryFilters(query, websiteId, auth.user?.id);
const data = await getWebsiteStats(websiteId, filters);

View file

@ -42,7 +42,7 @@ export async function GET(
value: segment.name,
}));
} else {
const filters = await getQueryFilters(query, websiteId);
const filters = await getQueryFilters(query, websiteId, auth.user?.id);
values = await getValues(websiteId, FILTER_COLUMNS[type], filters);
}

View file

@ -1,6 +1,6 @@
import { z } from 'zod';
import { uuid } from '@/lib/crypto';
import redis from '@/lib/redis';
import { fetchAccount } from '@/lib/load';
import { getQueryFilters, parseRequest } from '@/lib/request';
import { json, unauthorized } from '@/lib/response';
import { pagingParams, searchParams } from '@/lib/schema';
@ -52,7 +52,7 @@ export async function POST(request: Request) {
const { id, name, domain, shareId, teamId } = body;
if (process.env.CLOUD_MODE && !teamId) {
const account = await redis.client.get(`account:${auth.user.id}`);
const account = await fetchAccount(auth.user.id);
if (!account?.hasSubscription) {
const count = await getWebsiteCount(auth.user.id);