From 14012ed68e668e022cbfed38dc01bae5ad4de4a3 Mon Sep 17 00:00:00 2001 From: Indra Gunawan Date: Wed, 21 Jan 2026 22:28:55 +0800 Subject: [PATCH 1/6] fix inconsistent date format --- src/lib/prisma.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts index bfd007d1..cbabe03b 100644 --- a/src/lib/prisma.ts +++ b/src/lib/prisma.ts @@ -20,14 +20,6 @@ const PRISMA_LOG_OPTIONS = { }; const DATE_FORMATS = { - minute: 'YYYY-MM-DD HH24:MI:00', - hour: 'YYYY-MM-DD HH24:00:00', - day: 'YYYY-MM-DD HH24:00:00', - month: 'YYYY-MM-01 HH24:00:00', - year: 'YYYY-01-01 HH24:00:00', -}; - -const DATE_FORMATS_UTC = { minute: 'YYYY-MM-DD"T"HH24:MI:00"Z"', hour: 'YYYY-MM-DD"T"HH24:00:00"Z"', day: 'YYYY-MM-DD"T"HH24:00:00"Z"', @@ -52,7 +44,7 @@ function getDateSQL(field: string, unit: string, timezone?: string): string { return `to_char(date_trunc('${unit}', ${field} at time zone '${timezone}'), '${DATE_FORMATS[unit]}')`; } - return `to_char(date_trunc('${unit}', ${field}), '${DATE_FORMATS_UTC[unit]}')`; + return `to_char(date_trunc('${unit}', ${field}), '${DATE_FORMATS[unit]}')`; } function getDateWeeklySQL(field: string, timezone?: string) { From 7f43a0d41a1b1418fcde01fa7b2f69f504f917f2 Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Mon, 26 Jan 2026 10:00:31 -0800 Subject: [PATCH 2/6] =?UTF-8?q?Improve=20team=20admin=20screen=20workflow?= =?UTF-8?q?=20for=20team/members.=20Closes=20=C2=A0#2767?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(main)/admin/teams/AdminTeamsPage.tsx | 7 +- src/app/(main)/teams/TeamAddForm.tsx | 16 +++- src/app/(main)/teams/TeamMemberAddForm.tsx | 76 +++++++++++++++++++ src/app/(main)/teams/TeamsAddButton.tsx | 10 ++- src/app/(main)/teams/TeamsMemberAddButton.tsx | 40 ++++++++++ .../(main)/teams/[teamId]/TeamSettings.tsx | 11 ++- src/app/api/teams/route.ts | 5 +- src/components/input/UserSelect.tsx | 71 +++++++++++++++++ src/index.ts | 2 + 9 files changed, 230 insertions(+), 8 deletions(-) create mode 100644 src/app/(main)/teams/TeamMemberAddForm.tsx create mode 100644 src/app/(main)/teams/TeamsMemberAddButton.tsx create mode 100644 src/components/input/UserSelect.tsx diff --git a/src/app/(main)/admin/teams/AdminTeamsPage.tsx b/src/app/(main)/admin/teams/AdminTeamsPage.tsx index 41e6f4af..7905f7f6 100644 --- a/src/app/(main)/admin/teams/AdminTeamsPage.tsx +++ b/src/app/(main)/admin/teams/AdminTeamsPage.tsx @@ -3,14 +3,19 @@ import { Column } from '@umami/react-zen'; import { PageHeader } from '@/components/common/PageHeader'; import { Panel } from '@/components/common/Panel'; import { useMessages } from '@/components/hooks'; +import { TeamsAddButton } from '../../teams/TeamsAddButton'; import { AdminTeamsDataTable } from './AdminTeamsDataTable'; export function AdminTeamsPage() { const { formatMessage, labels } = useMessages(); + const handleSave = () => {}; + return ( - + + + diff --git a/src/app/(main)/teams/TeamAddForm.tsx b/src/app/(main)/teams/TeamAddForm.tsx index c95259f4..3b827776 100644 --- a/src/app/(main)/teams/TeamAddForm.tsx +++ b/src/app/(main)/teams/TeamAddForm.tsx @@ -7,8 +7,17 @@ import { TextField, } from '@umami/react-zen'; import { useMessages, useUpdateQuery } from '@/components/hooks'; +import { UserSelect } from '@/components/input/UserSelect'; -export function TeamAddForm({ onSave, onClose }: { onSave: () => void; onClose: () => void }) { +export function TeamAddForm({ + onSave, + onClose, + isAdmin, +}: { + onSave: () => void; + onClose: () => void; + isAdmin: boolean; +}) { const { formatMessage, labels, getErrorMessage } = useMessages(); const { mutateAsync, error, isPending } = useUpdateQuery('/teams'); @@ -26,6 +35,11 @@ export function TeamAddForm({ onSave, onClose }: { onSave: () => void; onClose: + {isAdmin && ( + + + + )} + + {formatMessage(labels.save)} + + + + ); +} diff --git a/src/app/(main)/teams/TeamsAddButton.tsx b/src/app/(main)/teams/TeamsAddButton.tsx index 578a273a..13873088 100644 --- a/src/app/(main)/teams/TeamsAddButton.tsx +++ b/src/app/(main)/teams/TeamsAddButton.tsx @@ -4,7 +4,13 @@ import { Plus } from '@/components/icons'; import { messages } from '@/components/messages'; import { TeamAddForm } from './TeamAddForm'; -export function TeamsAddButton({ onSave }: { onSave?: () => void }) { +export function TeamsAddButton({ + onSave, + isAdmin = false, +}: { + onSave?: () => void; + isAdmin?: boolean; +}) { const { formatMessage, labels } = useMessages(); const { toast } = useToast(); const { touch } = useModified(); @@ -25,7 +31,7 @@ export function TeamsAddButton({ onSave }: { onSave?: () => void }) { - {({ close }) => } + {({ close }) => } diff --git a/src/app/(main)/teams/TeamsMemberAddButton.tsx b/src/app/(main)/teams/TeamsMemberAddButton.tsx new file mode 100644 index 00000000..f1bbf258 --- /dev/null +++ b/src/app/(main)/teams/TeamsMemberAddButton.tsx @@ -0,0 +1,40 @@ +import { Button, Dialog, DialogTrigger, Icon, Modal, Text, useToast } from '@umami/react-zen'; +import { useMessages, useModified } from '@/components/hooks'; +import { Plus } from '@/components/icons'; +import { messages } from '@/components/messages'; +import { TeamMemberAddForm } from './TeamMemberAddForm'; + +export function TeamsMemberAddButton({ + teamId, + onSave, +}: { + teamId: string; + onSave?: () => void; + isAdmin?: boolean; +}) { + const { formatMessage, labels } = useMessages(); + const { toast } = useToast(); + const { touch } = useModified(); + + const handleSave = async () => { + toast(formatMessage(messages.saved)); + touch('teams:members'); + onSave?.(); + }; + + return ( + + + + + {({ close }) => } + + + + ); +} diff --git a/src/app/(main)/teams/[teamId]/TeamSettings.tsx b/src/app/(main)/teams/[teamId]/TeamSettings.tsx index 3ddbe000..4bbb8905 100644 --- a/src/app/(main)/teams/[teamId]/TeamSettings.tsx +++ b/src/app/(main)/teams/[teamId]/TeamSettings.tsx @@ -1,10 +1,12 @@ -import { Column } from '@umami/react-zen'; +import { Column, Heading, Row } from '@umami/react-zen'; import { TeamLeaveButton } from '@/app/(main)/teams/TeamLeaveButton'; import { PageHeader } from '@/components/common/PageHeader'; import { Panel } from '@/components/common/Panel'; -import { useLoginQuery, useNavigation, useTeam } from '@/components/hooks'; +import { useLoginQuery, useMessages, useNavigation, useTeam } from '@/components/hooks'; import { Users } from '@/components/icons'; +import { labels } from '@/components/messages'; import { ROLES } from '@/lib/constants'; +import { TeamsMemberAddButton } from '../TeamsMemberAddButton'; import { TeamEditForm } from './TeamEditForm'; import { TeamManage } from './TeamManage'; import { TeamMembersDataTable } from './TeamMembersDataTable'; @@ -13,6 +15,7 @@ export function TeamSettings({ teamId }: { teamId: string }) { const team: any = useTeam(); const { user } = useLoginQuery(); const { pathname } = useNavigation(); + const { formatMessage } = useMessages(); const isAdmin = pathname.includes('/admin'); @@ -37,6 +40,10 @@ export function TeamSettings({ teamId }: { teamId: string }) { + + {formatMessage(labels.members)} + {isAdmin && } + {isTeamOwner && ( diff --git a/src/app/api/teams/route.ts b/src/app/api/teams/route.ts index 53ef5923..c571f405 100644 --- a/src/app/api/teams/route.ts +++ b/src/app/api/teams/route.ts @@ -28,6 +28,7 @@ export async function GET(request: Request) { export async function POST(request: Request) { const schema = z.object({ name: z.string().max(50), + ownerId: z.uuid().optional(), }); const { auth, body, error } = await parseRequest(request, schema); @@ -40,7 +41,7 @@ export async function POST(request: Request) { return unauthorized(); } - const { name } = body; + const { name, ownerId } = body; const team = await createTeam( { @@ -48,7 +49,7 @@ export async function POST(request: Request) { name, accessCode: `team_${getRandomChars(16)}`, }, - auth.user.id, + ownerId ?? auth.user.id, ); return json(team); diff --git a/src/components/input/UserSelect.tsx b/src/components/input/UserSelect.tsx new file mode 100644 index 00000000..ccb3d432 --- /dev/null +++ b/src/components/input/UserSelect.tsx @@ -0,0 +1,71 @@ +import { ListItem, Row, Select, type SelectProps, Text } from '@umami/react-zen'; +import { useMemo, useState } from 'react'; +import { Empty } from '@/components/common/Empty'; +import { useMessages, useTeamMembersQuery, useUsersQuery } from '@/components/hooks'; + +export function UserSelect({ + teamId, + onChange, + ...props +}: { + teamId?: string; +} & SelectProps) { + const { formatMessage, messages } = useMessages(); + const { data: users, isLoading: usersLoading } = useUsersQuery(); + const { data: teamMembers, isLoading: teamMembersLoading } = useTeamMembersQuery(teamId); + const [username, setUsername] = useState(); + const [search, setSearch] = useState(''); + + const listItems = useMemo(() => { + if (!users) { + return []; + } + if (!teamId || !teamMembers) { + return users.data; + } + const teamMemberIds = teamMembers.data.map(({ userId }) => userId); + return users.data.filter(({ id }) => !teamMemberIds.includes(id)); + }, [users, teamMembers, teamId]); + + const handleSearch = (value: string) => { + setSearch(value); + }; + + const handleOpenChange = () => { + setSearch(''); + }; + + const handleChange = (id: string) => { + setUsername(listItems.find(item => item.id === id)?.username); + onChange(id); + }; + + const renderValue = () => { + return ( + + {username} + + ); + }; + + return ( + + ); +} diff --git a/src/index.ts b/src/index.ts index 907c5623..df164b9d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,11 +19,13 @@ export * from '@/app/(main)/teams/TeamAddForm'; export * from '@/app/(main)/teams/TeamJoinForm'; export * from '@/app/(main)/teams/TeamLeaveButton'; export * from '@/app/(main)/teams/TeamLeaveForm'; +export * from '@/app/(main)/teams/TeamMemberAddForm'; export * from '@/app/(main)/teams/TeamProvider'; export * from '@/app/(main)/teams/TeamsAddButton'; export * from '@/app/(main)/teams/TeamsDataTable'; export * from '@/app/(main)/teams/TeamsHeader'; export * from '@/app/(main)/teams/TeamsJoinButton'; +export * from '@/app/(main)/teams/TeamsMemberAddButton'; export * from '@/app/(main)/teams/TeamsTable'; export * from '@/app/(main)/websites/[websiteId]/settings/WebsiteData'; export * from '@/app/(main)/websites/[websiteId]/settings/WebsiteDeleteForm'; From 7bb30443a8db45dea34e01a201b68cc265225899 Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Mon, 26 Jan 2026 11:05:20 -0800 Subject: [PATCH 3/6] Add distinct ID to filters/expanded metrics. Closes #3861 --- src/app/(main)/websites/[websiteId]/WebsiteExpandedMenu.tsx | 6 ++++++ .../(main)/websites/[websiteId]/compare/CompareTables.tsx | 5 +++++ src/components/hooks/useFields.ts | 1 + src/components/hooks/useFilterParameters.ts | 3 +++ src/components/input/FilterEditForm.tsx | 4 +++- src/lib/constants.ts | 2 ++ src/lib/schema.ts | 2 ++ src/queries/sql/events/getEventExpandedMetrics.ts | 1 + src/queries/sql/sessions/getSessionExpandedMetrics.ts | 1 + 9 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/app/(main)/websites/[websiteId]/WebsiteExpandedMenu.tsx b/src/app/(main)/websites/[websiteId]/WebsiteExpandedMenu.tsx index 29c3954f..4bac4ff6 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteExpandedMenu.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteExpandedMenu.tsx @@ -169,6 +169,12 @@ export function WebsiteExpandedMenu({ path: updateParams({ view: 'hostname' }), icon: , }, + { + id: 'distinctId', + label: formatMessage(labels.distinctId), + path: updateParams({ view: 'distinctId' }), + icon: , + }, { id: 'tag', label: formatMessage(labels.tag), diff --git a/src/app/(main)/websites/[websiteId]/compare/CompareTables.tsx b/src/app/(main)/websites/[websiteId]/compare/CompareTables.tsx index 13c05160..4daf17fc 100644 --- a/src/app/(main)/websites/[websiteId]/compare/CompareTables.tsx +++ b/src/app/(main)/websites/[websiteId]/compare/CompareTables.tsx @@ -93,6 +93,11 @@ export function CompareTables({ websiteId }: { websiteId: string }) { label: formatMessage(labels.hostname), path: renderPath('hostname'), }, + { + id: 'distinctId', + label: formatMessage(labels.distinctId), + path: renderPath('distinctId'), + }, { id: 'tag', label: formatMessage(labels.tags), diff --git a/src/components/hooks/useFields.ts b/src/components/hooks/useFields.ts index 22a1dcf3..039b7157 100644 --- a/src/components/hooks/useFields.ts +++ b/src/components/hooks/useFields.ts @@ -15,6 +15,7 @@ export function useFields() { { name: 'region', type: 'string', label: formatMessage(labels.region) }, { name: 'city', type: 'string', label: formatMessage(labels.city) }, { name: 'hostname', type: 'string', label: formatMessage(labels.hostname) }, + { name: 'distinctId', type: 'string', label: formatMessage(labels.distinctId) }, { name: 'tag', type: 'string', label: formatMessage(labels.tag) }, { name: 'event', type: 'string', label: formatMessage(labels.event) }, ]; diff --git a/src/components/hooks/useFilterParameters.ts b/src/components/hooks/useFilterParameters.ts index 54032120..c141a3be 100644 --- a/src/components/hooks/useFilterParameters.ts +++ b/src/components/hooks/useFilterParameters.ts @@ -18,6 +18,7 @@ export function useFilterParameters() { event, tag, hostname, + distinctId, page, pageSize, search, @@ -42,6 +43,7 @@ export function useFilterParameters() { event, tag, hostname, + distinctId, search, segment, cohort, @@ -61,6 +63,7 @@ export function useFilterParameters() { event, tag, hostname, + distinctId, page, pageSize, search, diff --git a/src/components/input/FilterEditForm.tsx b/src/components/input/FilterEditForm.tsx index 44f43844..9221e3a2 100644 --- a/src/components/input/FilterEditForm.tsx +++ b/src/components/input/FilterEditForm.tsx @@ -61,7 +61,9 @@ export function FilterEditForm({ websiteId, onChange, onClose }: FilterEditFormP websiteId={websiteId} value={currentFilters} onChange={setCurrentFilters} - exclude={excludeFilters ? ['path', 'title', 'hostname', 'tag', 'event'] : []} + exclude={ + excludeFilters ? ['path', 'title', 'hostname', 'distinctId', 'tag', 'event'] : [] + } /> diff --git a/src/lib/constants.ts b/src/lib/constants.ts index bfc80a13..3da177c0 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -55,6 +55,7 @@ export const SESSION_COLUMNS = [ 'country', 'city', 'region', + 'distinctId', ]; export const SEGMENT_TYPES = { @@ -69,6 +70,7 @@ export const FILTER_COLUMNS = { referrer: 'referrer_domain', domain: 'referrer_domain', hostname: 'hostname', + distinctId: 'distinct_id', title: 'page_title', query: 'url_query', os: 'os', diff --git a/src/lib/schema.ts b/src/lib/schema.ts index 247a89ae..a3c56a0f 100644 --- a/src/lib/schema.ts +++ b/src/lib/schema.ts @@ -36,6 +36,7 @@ export const filterParams = { city: z.string().optional(), tag: z.string().optional(), hostname: z.string().optional(), + distinctId: z.string().optional(), language: z.string().optional(), event: z.string().optional(), segment: z.uuid().optional(), @@ -89,6 +90,7 @@ export const fieldsParam = z.enum([ 'city', 'tag', 'hostname', + 'distinctId', 'language', 'event', ]); diff --git a/src/queries/sql/events/getEventExpandedMetrics.ts b/src/queries/sql/events/getEventExpandedMetrics.ts index dec444e8..86bda850 100644 --- a/src/queries/sql/events/getEventExpandedMetrics.ts +++ b/src/queries/sql/events/getEventExpandedMetrics.ts @@ -72,6 +72,7 @@ async function relationalQuery( ${filterQuery} group by name, website_event.session_id, website_event.visit_id ) as t + where name != '' group by name order by visitors desc, visits desc limit ${limit} diff --git a/src/queries/sql/sessions/getSessionExpandedMetrics.ts b/src/queries/sql/sessions/getSessionExpandedMetrics.ts index c8d20d84..6b85cd45 100644 --- a/src/queries/sql/sessions/getSessionExpandedMetrics.ts +++ b/src/queries/sql/sessions/getSessionExpandedMetrics.ts @@ -82,6 +82,7 @@ async function relationalQuery( group by name, website_event.session_id, website_event.visit_id ${includeCountry ? ', country' : ''} ) as t + where name != '' group by name ${includeCountry ? ', country' : ''} order by visitors desc, visits desc From 29a373467ae0e1d7137d2e8b14b8ff50cc464165 Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Mon, 26 Jan 2026 11:42:09 -0800 Subject: [PATCH 4/6] Revert "Merge pull request #3972 from IndraGunawan/fix-inconsistent-date-format" This reverts commit 5f316a79e508564ea5029eac67a7cdcf3fc35351, reversing changes made to 7bb30443a8db45dea34e01a201b68cc265225899. --- src/lib/prisma.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts index cbabe03b..bfd007d1 100644 --- a/src/lib/prisma.ts +++ b/src/lib/prisma.ts @@ -20,6 +20,14 @@ const PRISMA_LOG_OPTIONS = { }; const DATE_FORMATS = { + minute: 'YYYY-MM-DD HH24:MI:00', + hour: 'YYYY-MM-DD HH24:00:00', + day: 'YYYY-MM-DD HH24:00:00', + month: 'YYYY-MM-01 HH24:00:00', + year: 'YYYY-01-01 HH24:00:00', +}; + +const DATE_FORMATS_UTC = { minute: 'YYYY-MM-DD"T"HH24:MI:00"Z"', hour: 'YYYY-MM-DD"T"HH24:00:00"Z"', day: 'YYYY-MM-DD"T"HH24:00:00"Z"', @@ -44,7 +52,7 @@ function getDateSQL(field: string, unit: string, timezone?: string): string { return `to_char(date_trunc('${unit}', ${field} at time zone '${timezone}'), '${DATE_FORMATS[unit]}')`; } - return `to_char(date_trunc('${unit}', ${field}), '${DATE_FORMATS[unit]}')`; + return `to_char(date_trunc('${unit}', ${field}), '${DATE_FORMATS_UTC[unit]}')`; } function getDateWeeklySQL(field: string, timezone?: string) { From 4867492ca3052cd13f0d431ece9906771bf01f84 Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Mon, 26 Jan 2026 13:52:46 -0800 Subject: [PATCH 5/6] Fix breakdown alias column not found bug --- src/queries/sql/reports/getBreakdown.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/queries/sql/reports/getBreakdown.ts b/src/queries/sql/reports/getBreakdown.ts index 51773d86..c84db769 100644 --- a/src/queries/sql/reports/getBreakdown.ts +++ b/src/queries/sql/reports/getBreakdown.ts @@ -131,5 +131,5 @@ function parseFields(fields: string[]) { } function parseFieldsByName(fields: string[]) { - return `${fields.map(name => name).join(',')}`; + return `${fields.map(name => `"${name}"`).join(',')}`; } From 1498da2d02b3b8f4bdd53a9e0d834fc690ba0f2e Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Mon, 26 Jan 2026 15:26:02 -0800 Subject: [PATCH 6/6] fix FilterButton import error --- .../websites/[websiteId]/(reports)/journeys/JourneysPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/(main)/websites/[websiteId]/(reports)/journeys/JourneysPage.tsx b/src/app/(main)/websites/[websiteId]/(reports)/journeys/JourneysPage.tsx index c2dd8349..f1a8976f 100644 --- a/src/app/(main)/websites/[websiteId]/(reports)/journeys/JourneysPage.tsx +++ b/src/app/(main)/websites/[websiteId]/(reports)/journeys/JourneysPage.tsx @@ -1,10 +1,10 @@ 'use client'; import { Column, Grid, ListItem, Row, SearchField, Select } from '@umami/react-zen'; -import { FilterButtons } from 'dist'; import { useState } from 'react'; import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls'; import { Panel } from '@/components/common/Panel'; import { useDateRange, useMessages } from '@/components/hooks'; +import { FilterButtons } from '@/components/input/FilterButtons'; import { Journey } from './Journey'; const JOURNEY_STEPS = [2, 3, 4, 5, 6, 7];