Added DialogButton to handle mobile.
Some checks are pending
Create docker images (cloud) / Build, push, and deploy (push) Waiting to run
Node.js CI / build (postgresql, 18.18, 10) (push) Waiting to run

This commit is contained in:
Mike Cao 2025-10-16 23:59:18 -07:00
parent 036748cdeb
commit 40492ec7c4
32 changed files with 2146 additions and 1807 deletions

View file

@ -1,24 +1,19 @@
import { useMessages } from '@/components/hooks';
import { Button, Icon, Modal, Dialog, DialogTrigger, Text } from '@umami/react-zen';
import { Plus } from '@/components/icons';
import { LinkEditForm } from './LinkEditForm';
import { DialogButton } from '@/components/input/DialogButton';
export function LinkAddButton({ teamId }: { teamId?: string }) {
const { formatMessage, labels } = useMessages();
return (
<DialogTrigger>
<Button data-test="button-website-add" variant="primary">
<Icon>
<Plus />
</Icon>
<Text>{formatMessage(labels.addLink)}</Text>
</Button>
<Modal>
<Dialog title={formatMessage(labels.addLink)} style={{ width: 600 }}>
{({ close }) => <LinkEditForm teamId={teamId} onClose={close} />}
</Dialog>
</Modal>
</DialogTrigger>
<DialogButton
icon={<Plus />}
label={formatMessage(labels.addLink)}
variant="primary"
width="600px"
>
{({ close }) => <LinkEditForm teamId={teamId} onClose={close} />}
</DialogButton>
);
}

View file

@ -1,9 +1,8 @@
import { Dialog } from '@umami/react-zen';
import { ActionButton } from '@/components/input/ActionButton';
import { Trash } from '@/components/icons';
import { ConfirmationForm } from '@/components/common/ConfirmationForm';
import { messages } from '@/components/messages';
import { useDeleteQuery, useMessages } from '@/components/hooks';
import { DialogButton } from '@/components/input/DialogButton';
export function LinkDeleteButton({
linkId,
@ -29,27 +28,30 @@ export function LinkDeleteButton({
};
return (
<ActionButton title={formatMessage(labels.delete)} icon={<Trash />}>
<Dialog title={formatMessage(labels.confirm)} style={{ width: 400 }}>
{({ close }) => (
<ConfirmationForm
message={
<FormattedMessage
{...messages.confirmRemove}
values={{
target: <b>{name}</b>,
}}
/>
}
isLoading={isPending}
error={getErrorMessage(error)}
onConfirm={handleConfirm.bind(null, close)}
onClose={close}
buttonLabel={formatMessage(labels.delete)}
buttonVariant="danger"
/>
)}
</Dialog>
</ActionButton>
<DialogButton
icon={<Trash />}
title={formatMessage(labels.confirm)}
variant="quiet"
width="400px"
>
{({ close }) => (
<ConfirmationForm
message={
<FormattedMessage
{...messages.confirmRemove}
values={{
target: <b>{name}</b>,
}}
/>
}
isLoading={isPending}
error={getErrorMessage(error)}
onConfirm={handleConfirm.bind(null, close)}
onClose={close}
buttonLabel={formatMessage(labels.delete)}
buttonVariant="danger"
/>
)}
</DialogButton>
);
}

View file

@ -1,19 +1,16 @@
import { ActionButton } from '@/components/input/ActionButton';
import { Edit } from '@/components/icons';
import { Dialog } from '@umami/react-zen';
import { LinkEditForm } from './LinkEditForm';
import { useMessages } from '@/components/hooks';
import { DialogButton } from '@/components/input/DialogButton';
export function LinkEditButton({ linkId }: { linkId: string }) {
const { formatMessage, labels } = useMessages();
return (
<ActionButton title={formatMessage(labels.edit)} icon={<Edit />}>
<Dialog title={formatMessage(labels.link)} style={{ width: 800, minHeight: 300 }}>
{({ close }) => {
return <LinkEditForm linkId={linkId} onClose={close} />;
}}
</Dialog>
</ActionButton>
<DialogButton icon={<Edit />} title={formatMessage(labels.link)} variant="quiet" width="800px">
{({ close }) => {
return <LinkEditForm linkId={linkId} onClose={close} />;
}}
</DialogButton>
);
}

View file

@ -1,24 +1,19 @@
import { useMessages } from '@/components/hooks';
import { Button, Icon, Modal, Dialog, DialogTrigger, Text } from '@umami/react-zen';
import { Plus } from '@/components/icons';
import { PixelEditForm } from './PixelEditForm';
import { DialogButton } from '@/components/input/DialogButton';
export function PixelAddButton({ teamId }: { teamId?: string }) {
const { formatMessage, labels } = useMessages();
return (
<DialogTrigger>
<Button data-test="button-website-add" variant="primary">
<Icon>
<Plus />
</Icon>
<Text>{formatMessage(labels.addPixel)}</Text>
</Button>
<Modal>
<Dialog title={formatMessage(labels.addPixel)} style={{ width: 600 }}>
{({ close }) => <PixelEditForm teamId={teamId} onClose={close} />}
</Dialog>
</Modal>
</DialogTrigger>
<DialogButton
icon={<Plus />}
label={formatMessage(labels.addPixel)}
variant="primary"
width="600px"
>
{({ close }) => <PixelEditForm teamId={teamId} onClose={close} />}
</DialogButton>
);
}

View file

@ -1,9 +1,9 @@
import { Dialog } from '@umami/react-zen';
import { ActionButton } from '@/components/input/ActionButton';
import { Trash } from '@/components/icons';
import { ConfirmationForm } from '@/components/common/ConfirmationForm';
import { messages } from '@/components/messages';
import { useDeleteQuery, useMessages, useModified } from '@/components/hooks';
import { DialogButton } from '@/components/input/DialogButton';
export function PixelDeleteButton({
pixelId,
name,
@ -28,27 +28,30 @@ export function PixelDeleteButton({
};
return (
<ActionButton title={formatMessage(labels.delete)} icon={<Trash />}>
<Dialog title={formatMessage(labels.confirm)} style={{ width: 400 }}>
{({ close }) => (
<ConfirmationForm
message={
<FormattedMessage
{...messages.confirmRemove}
values={{
target: <b>{name}</b>,
}}
/>
}
isLoading={isPending}
error={getErrorMessage(error)}
onConfirm={handleConfirm.bind(null, close)}
onClose={close}
buttonLabel={formatMessage(labels.delete)}
buttonVariant="danger"
/>
)}
</Dialog>
</ActionButton>
<DialogButton
icon={<Trash />}
variant="quiet"
title={formatMessage(labels.confirm)}
width="400px"
>
{({ close }) => (
<ConfirmationForm
message={
<FormattedMessage
{...messages.confirmRemove}
values={{
target: <b>{name}</b>,
}}
/>
}
isLoading={isPending}
error={getErrorMessage(error)}
onConfirm={handleConfirm.bind(null, close)}
onClose={close}
buttonLabel={formatMessage(labels.delete)}
buttonVariant="danger"
/>
)}
</DialogButton>
);
}

View file

@ -1,19 +1,21 @@
import { ActionButton } from '@/components/input/ActionButton';
import { Edit } from '@/components/icons';
import { Dialog } from '@umami/react-zen';
import { PixelEditForm } from './PixelEditForm';
import { useMessages } from '@/components/hooks';
import { DialogButton } from '@/components/input/DialogButton';
export function PixelEditButton({ pixelId }: { pixelId: string }) {
const { formatMessage, labels } = useMessages();
return (
<ActionButton title={formatMessage(labels.edit)} icon={<Edit />}>
<Dialog title={formatMessage(labels.pixel)} style={{ width: 600, minHeight: 300 }}>
{({ close }) => {
return <PixelEditForm pixelId={pixelId} onClose={close} />;
}}
</Dialog>
</ActionButton>
<DialogButton
icon={<Edit />}
title={formatMessage(labels.addPixel)}
variant="quiet"
width="600px"
>
{({ close }) => {
return <PixelEditForm pixelId={pixelId} onClose={close} />;
}}
</DialogButton>
);
}

View file

@ -1,7 +1,7 @@
import { useMessages, useModified } from '@/components/hooks';
import { Dialog, useToast } from '@umami/react-zen';
import { useToast } from '@umami/react-zen';
import { TeamMemberEditForm } from './TeamMemberEditForm';
import { ActionButton } from '@/components/input/ActionButton';
import { DialogButton } from '@/components/input/DialogButton';
import { Edit } from '@/components/icons';
export function TeamMemberEditButton({
@ -26,18 +26,21 @@ export function TeamMemberEditButton({
};
return (
<ActionButton title={formatMessage(labels.edit)} icon={<Edit />}>
<Dialog title={formatMessage(labels.editMember)} style={{ width: 400 }}>
{({ close }) => (
<TeamMemberEditForm
teamId={teamId}
userId={userId}
role={role}
onSave={handleSave}
onClose={close}
/>
)}
</Dialog>
</ActionButton>
<DialogButton
icon={<Edit />}
title={formatMessage(labels.editMember)}
variant="quiet"
width="400px"
>
{({ close }) => (
<TeamMemberEditForm
teamId={teamId}
userId={userId}
role={role}
onSave={handleSave}
onClose={close}
/>
)}
</DialogButton>
);
}

View file

@ -2,8 +2,7 @@ import { ConfirmationForm } from '@/components/common/ConfirmationForm';
import { useDeleteQuery, useMessages, useModified } from '@/components/hooks';
import { messages } from '@/components/messages';
import { Trash } from '@/components/icons';
import { Dialog } from '@umami/react-zen';
import { ActionButton } from '@/components/input/ActionButton';
import { DialogButton } from '@/components/input/DialogButton';
export function TeamMemberRemoveButton({
teamId,
@ -32,27 +31,30 @@ export function TeamMemberRemoveButton({
};
return (
<ActionButton title={formatMessage(labels.delete)} icon={<Trash />}>
<Dialog title={formatMessage(labels.removeMember)} style={{ width: 400 }}>
{({ close }) => (
<ConfirmationForm
message={
<FormattedMessage
{...messages.confirmRemove}
values={{
target: <b>{userName}</b>,
}}
/>
}
isLoading={isPending}
error={error}
onConfirm={handleConfirm.bind(null, close)}
onClose={close}
buttonLabel={formatMessage(labels.remove)}
buttonVariant="danger"
/>
)}
</Dialog>
</ActionButton>
<DialogButton
icon={<Trash />}
title={formatMessage(labels.confirm)}
variant="quiet"
width="400px"
>
{({ close }) => (
<ConfirmationForm
message={
<FormattedMessage
{...messages.confirmRemove}
values={{
target: <b>{userName}</b>,
}}
/>
}
isLoading={isPending}
error={error}
onConfirm={handleConfirm.bind(null, close)}
onClose={close}
buttonLabel={formatMessage(labels.remove)}
buttonVariant="danger"
/>
)}
</DialogButton>
);
}

View file

@ -1,7 +1,8 @@
import { useMessages, useModified } from '@/components/hooks';
import { Button, Icon, Modal, Dialog, DialogTrigger, Text, useToast } from '@umami/react-zen';
import { useToast } from '@umami/react-zen';
import { Plus } from '@/components/icons';
import { WebsiteAddForm } from './WebsiteAddForm';
import { DialogButton } from '@/components/input/DialogButton';
export function WebsiteAddButton({ teamId, onSave }: { teamId: string; onSave?: () => void }) {
const { formatMessage, labels, messages } = useMessages();
@ -15,18 +16,13 @@ export function WebsiteAddButton({ teamId, onSave }: { teamId: string; onSave?:
};
return (
<DialogTrigger>
<Button data-test="button-website-add" variant="primary">
<Icon>
<Plus />
</Icon>
<Text>{formatMessage(labels.addWebsite)}</Text>
</Button>
<Modal>
<Dialog title={formatMessage(labels.addWebsite)} style={{ width: 400 }}>
{({ close }) => <WebsiteAddForm teamId={teamId} onSave={handleSave} onClose={close} />}
</Dialog>
</Modal>
</DialogTrigger>
<DialogButton
icon={<Plus />}
label={formatMessage(labels.addWebsite)}
variant="primary"
width="400px"
>
{({ close }) => <WebsiteAddForm teamId={teamId} onSave={handleSave} onClose={close} />}
</DialogButton>
);
}

View file

@ -22,7 +22,7 @@ export function GoalsPage({ websiteId }: { websiteId: string }) {
</SectionHeader>
<LoadingPanel data={data} isLoading={isLoading} error={error}>
{data && (
<Grid columns="1fr 1fr" gap>
<Grid columns={{ xs: '1fr', md: '1fr 1fr' }} gap>
{data['data'].map((report: any) => (
<Panel key={report.id}>
<Goal {...report} startDate={startDate} endDate={endDate} />

View file

@ -8,7 +8,7 @@ import { endOfMonth, startOfMonth } from 'date-fns';
export function RetentionPage({ websiteId }: { websiteId: string }) {
const {
dateRange: { startDate },
} = useDateRange(websiteId, { ignoreOffset: true });
} = useDateRange();
const monthStartDate = startOfMonth(startDate);
const monthEndDate = endOfMonth(startDate);

View file

@ -1,29 +1,16 @@
import { Button, DialogTrigger, Modal, Text, Icon, Dialog } from '@umami/react-zen';
import { useMessages } from '@/components/hooks';
import { Plus } from '@/components/icons';
import { DialogButton } from '@/components/input/DialogButton';
import { CohortEditForm } from './CohortEditForm';
export function CohortAddButton({ websiteId }: { websiteId: string }) {
const { formatMessage, labels } = useMessages();
return (
<DialogTrigger>
<Button variant="primary">
<Icon>
<Plus />
</Icon>
<Text>{formatMessage(labels.cohort)}</Text>
</Button>
<Modal>
<Dialog
title={formatMessage(labels.cohort)}
style={{ width: 800, minHeight: 300, maxHeight: '90vh' }}
>
{({ close }) => {
return <CohortEditForm websiteId={websiteId} onClose={close} />;
}}
</Dialog>
</Modal>
</DialogTrigger>
<DialogButton icon={<Plus />} label={formatMessage(labels.cohort)} variant="primary">
{({ close }) => {
return <CohortEditForm websiteId={websiteId} onClose={close} />;
}}
</DialogButton>
);
}

View file

@ -1,9 +1,8 @@
import { Dialog } from '@umami/react-zen';
import { ActionButton } from '@/components/input/ActionButton';
import { Trash } from '@/components/icons';
import { ConfirmationForm } from '@/components/common/ConfirmationForm';
import { messages } from '@/components/messages';
import { useDeleteQuery, useMessages } from '@/components/hooks';
import { ConfirmationForm } from '@/components/common/ConfirmationForm';
import { DialogButton } from '@/components/input/DialogButton';
export function CohortDeleteButton({
cohortId,
@ -32,27 +31,30 @@ export function CohortDeleteButton({
};
return (
<ActionButton title={formatMessage(labels.delete)} icon={<Trash />}>
<Dialog title={formatMessage(labels.confirm)} style={{ width: 400 }}>
{({ close }) => (
<ConfirmationForm
message={
<FormattedMessage
{...messages.confirmRemove}
values={{
target: <b>{name}</b>,
}}
/>
}
isLoading={isPending}
error={error}
onConfirm={handleConfirm.bind(null, close)}
onClose={close}
buttonLabel={formatMessage(labels.delete)}
buttonVariant="danger"
/>
)}
</Dialog>
</ActionButton>
<DialogButton
icon={<Trash />}
variant="quiet"
title={formatMessage(labels.confirm)}
width="400px"
>
{({ close }) => (
<ConfirmationForm
message={
<FormattedMessage
{...messages.confirmRemove}
values={{
target: <b>{name}</b>,
}}
/>
}
isLoading={isPending}
error={error}
onConfirm={handleConfirm.bind(null, close)}
onClose={close}
buttonLabel={formatMessage(labels.delete)}
buttonVariant="danger"
/>
)}
</DialogButton>
);
}

View file

@ -1,9 +1,8 @@
import { ActionButton } from '@/components/input/ActionButton';
import { Edit } from '@/components/icons';
import { Dialog } from '@umami/react-zen';
import { CohortEditForm } from '@/app/(main)/websites/[websiteId]/cohorts/CohortEditForm';
import { useMessages } from '@/components/hooks';
import { Filter } from '@/lib/types';
import { DialogButton } from '@/components/input/DialogButton';
export function CohortEditButton({
cohortId,
@ -17,22 +16,23 @@ export function CohortEditButton({
const { formatMessage, labels } = useMessages();
return (
<ActionButton title={formatMessage(labels.edit)} icon={<Edit />}>
<Dialog
title={formatMessage(labels.cohort)}
style={{ width: 800, minHeight: 300, maxHeight: '90vh' }}
>
{({ close }) => {
return (
<CohortEditForm
cohortId={cohortId}
websiteId={websiteId}
filters={filters}
onClose={close}
/>
);
}}
</Dialog>
</ActionButton>
<DialogButton
icon={<Edit />}
variant="quiet"
title={formatMessage(labels.cohort)}
width="800px"
minHeight="300px"
>
{({ close }) => {
return (
<CohortEditForm
cohortId={cohortId}
websiteId={websiteId}
filters={filters}
onClose={close}
/>
);
}}
</DialogButton>
);
}

View file

@ -80,7 +80,7 @@ export function CohortEditForm({
<Column>
<Label>{formatMessage(labels.action)}</Label>
<Grid columns="260px 1fr" gap>
<Grid columns={{ xs: '1fr', md: '1fr 1fr' }} gap>
<Column>
<FormField
name="parameters.action.type"

View file

@ -1,22 +1,17 @@
import { DataTable, DataColumn, Row } from '@umami/react-zen';
import { DataTable, DataColumn, Row, DataTableProps } from '@umami/react-zen';
import { useMessages, useNavigation } from '@/components/hooks';
import { Empty } from '@/components/common/Empty';
import { DateDistance } from '@/components/common/DateDistance';
import { filtersObjectToArray } from '@/lib/params';
import { CohortEditButton } from '@/app/(main)/websites/[websiteId]/cohorts/CohortEditButton';
import { CohortDeleteButton } from '@/app/(main)/websites/[websiteId]/cohorts/CohortDeleteButton';
import Link from 'next/link';
export function CohortsTable({ data = [] }) {
export function CohortsTable(props: DataTableProps) {
const { formatMessage, labels } = useMessages();
const { websiteId, renderUrl } = useNavigation();
if (data.length === 0) {
return <Empty />;
}
return (
<DataTable data={data}>
<DataTable {...props}>
<DataColumn id="name" label={formatMessage(labels.name)}>
{(row: any) => (
<Link href={renderUrl(`/websites/${websiteId}?cohort=${row.id}`, false)}>{row.name}</Link>

View file

@ -1,29 +1,16 @@
import { Button, DialogTrigger, Modal, Text, Icon, Dialog } from '@umami/react-zen';
import { useMessages } from '@/components/hooks';
import { Plus } from '@/components/icons';
import { SegmentEditForm } from './SegmentEditForm';
import { DialogButton } from '@/components/input/DialogButton';
export function SegmentAddButton({ websiteId }: { websiteId: string }) {
const { formatMessage, labels } = useMessages();
return (
<DialogTrigger>
<Button variant="primary">
<Icon>
<Plus />
</Icon>
<Text>{formatMessage(labels.segment)}</Text>
</Button>
<Modal>
<Dialog
title={formatMessage(labels.segment)}
style={{ width: 800, minHeight: 300, maxHeight: '90vh' }}
>
{({ close }) => {
return <SegmentEditForm websiteId={websiteId} onClose={close} />;
}}
</Dialog>
</Modal>
</DialogTrigger>
<DialogButton icon={<Plus />} label={formatMessage(labels.segment)} variant="primary">
{({ close }) => {
return <SegmentEditForm websiteId={websiteId} onClose={close} />;
}}
</DialogButton>
);
}

View file

@ -1,9 +1,8 @@
import { Dialog } from '@umami/react-zen';
import { ActionButton } from '@/components/input/ActionButton';
import { Trash } from '@/components/icons';
import { ConfirmationForm } from '@/components/common/ConfirmationForm';
import { messages } from '@/components/messages';
import { useDeleteQuery, useMessages } from '@/components/hooks';
import { DialogButton } from '@/components/input/DialogButton';
export function SegmentDeleteButton({
segmentId,
@ -32,27 +31,30 @@ export function SegmentDeleteButton({
};
return (
<ActionButton title={formatMessage(labels.delete)} icon={<Trash />}>
<Dialog title={formatMessage(labels.confirm)} style={{ width: 400 }}>
{({ close }) => (
<ConfirmationForm
message={
<FormattedMessage
{...messages.confirmRemove}
values={{
target: <b>{name}</b>,
}}
/>
}
isLoading={isPending}
error={error}
onConfirm={handleConfirm.bind(null, close)}
onClose={close}
buttonLabel={formatMessage(labels.delete)}
buttonVariant="danger"
/>
)}
</Dialog>
</ActionButton>
<DialogButton
icon={<Trash />}
title={formatMessage(labels.confirm)}
variant="quiet"
width="600px"
>
{({ close }) => (
<ConfirmationForm
message={
<FormattedMessage
{...messages.confirmRemove}
values={{
target: <b>{name}</b>,
}}
/>
}
isLoading={isPending}
error={error}
onConfirm={handleConfirm.bind(null, close)}
onClose={close}
buttonLabel={formatMessage(labels.delete)}
buttonVariant="danger"
/>
)}
</DialogButton>
);
}

View file

@ -1,9 +1,8 @@
import { Dialog } from '@umami/react-zen';
import { ActionButton } from '@/components/input/ActionButton';
import { Edit } from '@/components/icons';
import { useMessages } from '@/components/hooks';
import { SegmentEditForm } from './SegmentEditForm';
import { Filter } from '@/lib/types';
import { DialogButton } from '@/components/input/DialogButton';
export function SegmentEditButton({
segmentId,
@ -17,22 +16,22 @@ export function SegmentEditButton({
const { formatMessage, labels } = useMessages();
return (
<ActionButton title={formatMessage(labels.edit)} icon={<Edit />}>
<Dialog
title={formatMessage(labels.segment)}
style={{ width: 800, minHeight: 300, maxHeight: '90vh' }}
>
{({ close }) => {
return (
<SegmentEditForm
segmentId={segmentId}
websiteId={websiteId}
filters={filters}
onClose={close}
/>
);
}}
</Dialog>
</ActionButton>
<DialogButton
icon={<Edit />}
title={formatMessage(labels.segment)}
variant="quiet"
width="800px"
>
{({ close }) => {
return (
<SegmentEditForm
segmentId={segmentId}
websiteId={websiteId}
filters={filters}
onClose={close}
/>
);
}}
</DialogButton>
);
}

View file

@ -1,21 +1,16 @@
import { DataTable, DataColumn, Row } from '@umami/react-zen';
import { DataTable, DataColumn, Row, DataTableProps } from '@umami/react-zen';
import { useMessages, useNavigation } from '@/components/hooks';
import { Empty } from '@/components/common/Empty';
import { DateDistance } from '@/components/common/DateDistance';
import { SegmentEditButton } from '@/app/(main)/websites/[websiteId]/segments/SegmentEditButton';
import { SegmentDeleteButton } from '@/app/(main)/websites/[websiteId]/segments/SegmentDeleteButton';
import Link from 'next/link';
export function SegmentsTable({ data = [] }) {
export function SegmentsTable(props: DataTableProps) {
const { formatMessage, labels } = useMessages();
const { websiteId, renderUrl } = useNavigation();
if (data.length === 0) {
return <Empty />;
}
return (
<DataTable data={data}>
<DataTable {...props}>
<DataColumn id="name" label={formatMessage(labels.name)}>
{(row: any) => (
<Link href={renderUrl(`/websites/${websiteId}?segment=${row.id}`, false)}>